diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index e928127428d..00000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,73 +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 coverage" - -# 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 coverage" - -test_script: - # Set environment variables - - set PYTHONPATH=%APPVEYOR_BUILD_FOLDER% - - set PATH=%APPVEYOR_BUILD_FOLDER%;C:\Program Files\Wireshark\;C:\Program Files\Windump\;%PATH% - - set TOX_PARALLEL_NO_SPINNER=1 - - # Main unit tests - - "%PYTHON%\\python -m tox -- %UT_FLAGS%" - -after_test: - # Run codecov - - "%PYTHON%\\python -m tox -e codecov" 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 f33d7b3ecda..5bfeb12aeb2 100755 --- a/.config/ci/install.sh +++ b/.config/ci/install.sh @@ -1,5 +1,10 @@ #!/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] @@ -14,7 +19,7 @@ then fi # Install on osx -if [ "${OSTYPE:0:6}" = "darwin" ] || [ "$TRAVIS_OS_NAME" = "osx" ] +if [ "${OSTYPE:0:6}" = "darwin" ] then if [ ! -z $SCAPY_USE_LIBPCAP ] then @@ -23,13 +28,17 @@ then fi fi -# Install wireshark data, ifconfig & vcan -if [ "$OSTYPE" = "linux-gnu" ] || [ "$TRAVIS_OS_NAME" = "linux" ] +CUR=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) + +# Install wireshark data, ifconfig, vcan, samba, openldap +if [ "$OSTYPE" = "linux-gnu" ] then 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 @@ -37,16 +46,11 @@ then fi fi -# On Travis, "osx" dependencies are installed in .travis.yml -if [ "$TRAVIS_OS_NAME" != "osx" ] -then - # Update pip & setuptools (tox uses those) - python -m pip install --upgrade pip setuptools --ignore-installed +# Update pip & setuptools (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 -fi +# Make sure tox is installed and up to date +python -m pip install -U tox --ignore-installed # Dump Environment (so that we can check PATH, UT_FLAGS, etc.) -openssl version 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 index 4a28fb932ad..58baa138c76 100755 --- a/.config/ci/openssl.py +++ b/.config/ci/openssl.py @@ -1,4 +1,6 @@ +# SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy +# See https://scapy.net/ for more information # Copyright (C) Gabriel Potter """ @@ -21,28 +23,23 @@ ).group(1).decode() OPENSSL_CONFIG = os.path.join(OPENSSL_DIR, 'openssl.cnf') -# https://askubuntu.com/a/1233456 -HEADER = b"openssl_conf = default_conf\n" -FOOTER = b""" -[ default_conf ] +# https://www.openssl.org/docs/manmaster/man5/config.html +DATA = b""" +openssl_conf = openssl_init -ssl_conf = ssl_sect +[openssl_init] +ssl_conf = ssl_configuration -[ssl_sect] +[ssl_configuration] +system_default = tls_system_default -system_default = system_default_sect - -[system_default_sect] -MinProtocol = TLSv1.2 -CipherString = DEFAULT:@SECLEVEL=1 -""" +[tls_system_default] +MinProtocol = TLSv1 +CipherString = DEFAULT:@SECLEVEL=0 +Options = UnsafeLegacyRenegotiation +""".strip() # Copy and edit -with open(OPENSSL_CONFIG, 'rb') as fd: - DATA = fd.read() - -DATA = HEADER + DATA + FOOTER - 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 e2ed053c58e..baa2e4e1b5f 100755 --- a/.config/ci/test.sh +++ b/.config/ci/test.sh @@ -1,46 +1,65 @@ #!/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)] +# ./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" - 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 if [ -z "$SIMPLE_TESTS" ] then # check vcan sudo modprobe -n -v vcan if [[ $? -ne 0 ]] then - # The vcan module is currently unavailable on Travis-CI xenial builds + # 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="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 @@ -70,26 +89,36 @@ if [ -z $TOXENV ] then case ${SCAPY_TOX_CHOSEN} in both) - export TOXENV="${TESTVER}_non_root,${TESTVER}_root" + export TOXENV="${TESTVER}-non_root,${TESTVER}-root" ;; root) - export TOXENV="${TESTVER}_root" + export TOXENV="${TESTVER}-root" ;; *) - export TOXENV="${TESTVER}_non_root" + export TOXENV="${TESTVER}-non_root" ;; esac fi # Configure OpenSSL -export OPENSSL_CONF=$(python `dirname $BASH_SOURCE`/openssl.py) +export OPENSSL_CONF=$(${PYTHON:=python} `dirname $BASH_SOURCE`/openssl.py) -# Dump vars (the others were already dumped in install.sh) +# 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_PARALLEL_NO_SPINNER=1 tox -- ${UT_FLAGS} || exit 1 +# export TOX_PARALLEL_NO_SPINNER=1 +tox -- ${UT_FLAGS} || exit 1 # Stop if NO_BASH_TESTS is set if [ ! -z "$SIMPLE_TESTS" ] diff --git a/.config/appveyor/InstallNpcap.ps1 b/.config/ci/windows/InstallNpcap.ps1 similarity index 92% rename from .config/appveyor/InstallNpcap.ps1 rename to .config/ci/windows/InstallNpcap.ps1 index d8477fd9e1a..347b7eb2420 100644 --- a/.config/appveyor/InstallNpcap.ps1 +++ b/.config/ci/windows/InstallNpcap.ps1 @@ -22,8 +22,8 @@ function checkTheSum($file, $hash) { function DownloadNPCAP_free { $file = $PSScriptRoot+"\npcap-0.96.exe" $hash = "83667e1306fdcf7f9967c10277b36b87e50ee8812e1ee2bb9443bdd065dc04a1" - # Download the 0.96 file from nmap servers - wget "https://npcap.com/dist/npcap-0.96.exe" -UseBasicParsing -OutFile $file + # 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 } 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 fe284a77e4a..7ed3f0caa78 100644 --- a/.config/codespell_ignore.txt +++ b/.config/codespell_ignore.txt @@ -1,31 +1,48 @@ -Ether +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 @@ -33,3 +50,4 @@ vas wan wanna webp +widgits diff --git a/.config/mypy/mypy.ini b/.config/mypy/mypy.ini index 6a04ecd04eb..7b17578099d 100644 --- a/.config/mypy/mypy.ini +++ b/.config/mypy/mypy.ini @@ -2,10 +2,13 @@ # Internal Scapy modules that we ignore -[mypy-scapy.libs.six,scapy.libs.six.moves,scapy.libs.winpcapy] +[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.*] diff --git a/.config/mypy/mypy_check.py b/.config/mypy/mypy_check.py index 58d878df8d6..371d93c3487 100644 --- a/.config/mypy/mypy_check.py +++ b/.config/mypy/mypy_check.py @@ -63,56 +63,56 @@ "--ignore-missing-imports", # config "--follow-imports=skip", # Remove eventually - "--config-file=" + os.path.abspath( - os.path.join( - localdir, - "mypy.ini" - ) - ), + "--config-file=" + os.path.abspath(os.path.join(localdir, "mypy.ini")), "--show-traceback", -] + ([ - "--platform=" + PLATFORM -] if PLATFORM else []) +] + (["--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", - ]) + ARGS.extend( + [ + "--always-true=LINUX", + "--always-false=OPENBSD", + "--always-false=FREEBSD", + "--always-false=NETBSD", + "--always-false=DARWIN", + "--always-false=WINDOWS", + "--always-false=BSD", + ] + ) 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", - ]) + 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 { + x + for x in FILES + if ( + x + not in { # Disabled on Windows - "scapy/arch/linux.py", + "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/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 -] +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_enabled.txt b/.config/mypy/mypy_enabled.txt index 42f1c6db7b8..a5001e3509f 100644 --- a/.config/mypy/mypy_enabled.txt +++ b/.config/mypy/mypy_enabled.txt @@ -10,11 +10,16 @@ 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.py -scapy/arch/unix.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 @@ -86,6 +91,15 @@ 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 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/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b76164695de..a7340ee6c6b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,11 @@ - + **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 (using `cd test && ./run_tests` or `tox`) +- [ ] 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/) diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/cifuzz.yml new file mode 100644 index 00000000000..3e173be1825 --- /dev/null +++ b/.github/workflows/cifuzz.yml @@ -0,0 +1,39 @@ +name: CIFuzz + +on: + pull_request: + 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 c941a9f0094..abcbfe9684f 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -7,6 +7,11 @@ on: # 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 @@ -16,11 +21,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Scapy - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install tox run: pip install tox - name: Run flake8 tests @@ -29,105 +34,150 @@ jobs: run: tox -e spell - name: Run twine check run: tox -e twine + - name: Run gitarchive check + run: tox -e gitarchive docs: + # 'runs-on' and 'python-version' should match the ones defined in .readthedocs.yml name: Build doc - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout Scapy - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + 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: - name: Checkout Scapy - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install tox run: pip install tox - name: Run mypy run: tox -e mypy utscapy: - name: ${{ matrix.os }} ${{ matrix.installmode }} ${{ matrix.python }} ${{ matrix.mode }} + 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: ["2.7", "3.10"] - mode: [both] + python: ["3.8", "3.9", "3.10", "3.11", "3.12"] + mode: [non_root] installmode: [''] + flags: [" -K scanner"] + allow-failure: ['false'] include: - # Linux non-root only tests - - os: ubuntu-latest + # 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.8" - mode: non_root - - os: ubuntu-latest - python: "3.9" - mode: non_root - # PyPy tests: root only - - os: ubuntu-latest - python: "pypy2.7" + python: "3.13" mode: root + flags: " -K scanner" + # PyPy tests: root only - os: ubuntu-latest python: "pypy3.9" mode: root + flags: " -K scanner" # Libpcap test - os: ubuntu-latest - python: "3.10" + python: "3.13" mode: root installmode: 'libpcap' - # MacOS tests - - os: macos-10.15 - python: "2.7" + 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: macos-14 + python: "3.13" mode: both - - os: macos-10.15 - python: "3.10" + 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@v3 + uses: actions/checkout@v4 # Codecov requires a fetch-depth > 1 with: fetch-depth: 2 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - - name: Install Tox and any other packages + - name: Install Tox and any other packages (linux/osx) run: ./.config/ci/install.sh ${{ matrix.installmode }} - - name: Run Tox + 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@v2 + uses: codecov/codecov-action@v5 + continue-on-error: true with: - file: /home/runner/work/scapy/scapy/.coverage + token: ${{ secrets.CODECOV_TOKEN }} cryptography: name: pyca/cryptography test runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.12" - name: Install tox run: pip install tox # pyca/cryptography's CI installs cryptography @@ -145,12 +195,12 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: 'python' - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.gitignore b/.gitignore index 34b606d3ceb..97e2a8a8be6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,14 +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..55c09aff689 --- /dev/null +++ b/.packit.yml @@ -0,0 +1,51 @@ +--- +# 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" + - "sed -i '/^# check$/a%check\\nOPENSSL_ENABLE_SHA1_SIGNATURES=1 OPENSSL_CONF=$(python3 ./.config/ci/openssl.py) ./test/run_tests -c test/configs/linux.utsc -K ci_only -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-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 ecf566c59b4..b4732b29e04 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,18 +4,27 @@ version: 2 +sphinx: + configuration: doc/scapy/conf.py + formats: - epub - pdf build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" + 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: install: - method: pip path: . extra_requirements: - - docs + - doc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c1a159a5a5a..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 @@ -136,26 +136,6 @@ make sure logging in Scapy remains helpful. - 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. - -### 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` - ### Code review Maintainers tend to be picky, and you might feel frustrated that your 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 ba7b62253f4..0a9b17ae4b5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -# Scapy   Scapy +# Scapy   Scapy -[![Scapy unit tests](https://github.com/secdev/scapy/workflows/Scapy%20unit%20tests/badge.svg?event=push)](https://github.com/secdev/scapy/actions?query=workflow%3A%22Scapy+unit+tests%22+branch%3Amaster+event%3Apush) -[![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/) [![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) @@ -26,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.9). 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). @@ -65,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) @@ -92,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/notebooks/Scapy in 15 minutes.ipynb b/doc/notebooks/Scapy in 15 minutes.ipynb index 7cc498d710b..57befdb3ffb 100644 --- a/doc/notebooks/Scapy in 15 minutes.ipynb +++ b/doc/notebooks/Scapy in 15 minutes.ipynb @@ -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 c6e75010328..6b9351d6544 100644 --- a/doc/notebooks/tls/notebook3_tls_compromised.ipynb +++ b/doc/notebooks/tls/notebook3_tls_compromised.ipynb @@ -12,89 +12,89 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ "from scapy.all import *\n", "load_layer('tls')" - ] + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "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": { "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": {}, - "outputs": [], "source": [ "# 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": {}, - "outputs": [], "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": {}, - "outputs": [], "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": {}, - "outputs": [], "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 }, { - "cell_type": "markdown", "metadata": {}, + "cell_type": "markdown", "source": [ "# Decrypting TLS Traffic Protected with PFS\n", "\n", @@ -104,14 +104,15 @@ "```\n", "cd doc/notebooks/tls/raw_data/\n", "\n", - "# Start a TLS 1.12 Server using the s_server\n", - "sudo openssl s_server -accept localhost:443 -cert pki/srv_cert.pem -key pki/srv_key.pem -WWW -tls1_2\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 s_client and retrieve the secrets.txt file\n", - "openssl s_client -connect localhost:443 -keylogfile tls_nss_example.keys.txt\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", @@ -120,38 +121,58 @@ ] }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "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 = rdpcap(\"raw_data/tls_nss_example.pcap\")" - ] + "packets = sniff(offline=\"raw_data/tls_nss_example.pcap\", session=TCPSession)" + ], + "outputs": [], + "execution_count": null }, { - "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], + "cell_type": "code", "source": [ - "# Display the HTTP GET query\n", - "packets[11][TLS].show()" - ] + "# Display the TLS1.2 HTTP GET query\n", + "packets[9][TLS].show()" + ], + "outputs": [], + "execution_count": null }, { + "metadata": {}, "cell_type": "code", - "execution_count": null, + "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[13][TLS].show()" - ] + "packets[28][TLS13].show()" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", @@ -166,24 +187,23 @@ }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "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", - "execution_count": null, "metadata": {}, - "outputs": [], "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", @@ -192,34 +212,37 @@ "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\"][\"Secret\"]\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[9][TLS]), tls_session=client_finished.tls_session.mirror())" - ] + "server_finished = TLS(raw(packets[8][TLS]), tls_session=client_finished.tls_session.mirror())" + ], + "outputs": [], + "execution_count": null }, { "cell_type": "code", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Display the HTTP GET query\n", - "http_query = TLS(raw(packets[11][TLS]), tls_session=server_finished.tls_session.mirror())\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", - "execution_count": null, "metadata": {}, - "outputs": [], "source": [ "# Display the answer containing the secret\n", - "http_response = TLS(raw(packets[13][TLS]), tls_session=http_query.tls_session.mirror())\n", + "http_response = TLS(raw(packets[10][TLS]), tls_session=http_query.tls_session.mirror())\n", "http_response.show()" - ] + ], + "outputs": [], + "execution_count": null } ], "metadata": { 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 index 69734b2585b..6cf32f6e662 100644 --- a/doc/notebooks/tls/raw_data/tls_nss_example.keys.txt +++ b/doc/notebooks/tls/raw_data/tls_nss_example.keys.txt @@ -1,2 +1,7 @@ # SSL/TLS secrets log file, generated by OpenSSL -CLIENT_RANDOM c43c799f04ad31e397ee4fe14c8819a19bf5951bbc545cada407c6c7589e60ab b599798159244555ddd10d80b5552a37d327fd6e661f3520194c28ef6e8bb0af6e3fb4d4f9945a61e83a41f2345fa27a +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 index f03811d0c87..9268ae4866c 100644 Binary files a/doc/notebooks/tls/raw_data/tls_nss_example.pcap and b/doc/notebooks/tls/raw_data/tls_nss_example.pcap differ diff --git a/doc/scapy.1 b/doc/scapy.1 index f926cb3015b..395871a8849 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 @@ -51,13 +52,13 @@ increase log verbosity. Can be used many times. 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 @@ -70,11 +71,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 @@ -82,19 +78,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" @@ -103,8 +102,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 @@ -117,7 +116,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 @@ -203,7 +202,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/_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/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 f42382fba2c..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,721 +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, 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 - -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 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.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. - -:: - - 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!... - - -.. 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() - -.. _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:: https://scapy.net/files/doc/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 + >>> 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/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 326083045e4..56f1c2ed175 100644 --- a/doc/scapy/backmatter.rst +++ b/doc/scapy/backmatter.rst @@ -1,11 +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. -- Nils Weiss contributed automotive specific layers and utilities. diff --git a/doc/scapy/build_dissect.rst b/doc/scapy/build_dissect.rst index b099798137a..2f53c349933 100644 --- a/doc/scapy/build_dissect.rst +++ b/doc/scapy/build_dissect.rst @@ -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 diff --git a/doc/scapy/conf.py b/doc/scapy/conf.py index 8776319cac8..be9480fb2f0 100644 --- a/doc/scapy/conf.py +++ b/doc/scapy/conf.py @@ -68,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 @@ -168,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'), ] @@ -178,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) ] @@ -189,7 +189,7 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'Scapy', 'Scapy Documentation', - 'Philippe Biondi and the Scapy community', 'Scapy', + 'The Scapy community', 'Scapy', '', 'Miscellaneous'), ] diff --git a/doc/scapy/development.rst b/doc/scapy/development.rst index 1d590d3ad65..3c2fcda23d5 100644 --- a/doc/scapy/development.rst +++ b/doc/scapy/development.rst @@ -293,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 - python3 setup.py sdist - twine check dist/scapy-2.4.3.tar.gz - twine upload dist/scapy-2.4.3.tar.gz +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 + + $ ./test/run_tests 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/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/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/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/ntlm/ntlmrelay_ldap.png b/doc/scapy/graphics/ntlm/ntlmrelay_ldap.png deleted file mode 100644 index ba410371db7..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_ldap.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_ldaps.png b/doc/scapy/graphics/ntlm/ntlmrelay_ldaps.png deleted file mode 100644 index 34f1b659547..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_ldaps.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_smb.png b/doc/scapy/graphics/ntlm/ntlmrelay_smb.png deleted file mode 100644 index a171f0ebe1e..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_smb.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_smb2.png b/doc/scapy/graphics/ntlm/ntlmrelay_smb2.png deleted file mode 100644 index f9931227fd2..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_smb2.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_smb_win1.png b/doc/scapy/graphics/ntlm/ntlmrelay_smb_win1.png deleted file mode 100644 index e35dd103bf2..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_smb_win1.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_smb_win2.png b/doc/scapy/graphics/ntlm/ntlmrelay_smb_win2.png deleted file mode 100644 index eb2e7e759d3..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_smb_win2.png and /dev/null differ diff --git a/doc/scapy/graphics/ntlm/ntlmrelay_smb_wireshark.png b/doc/scapy/graphics/ntlm/ntlmrelay_smb_wireshark.png deleted file mode 100644 index e27982314e3..00000000000 Binary files a/doc/scapy/graphics/ntlm/ntlmrelay_smb_wireshark.png 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 6999c73acd7..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:: diff --git a/doc/scapy/installation.rst b/doc/scapy/installation.rst index 165a9d36e9b..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,17 +18,19 @@ Each of these steps can be done in a different way depending on your platform an Scapy versions ============== -.. raw:: html - -
- - -
- -.. 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 ===================== @@ -52,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 @@ -73,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:: + $ git clone https://github.com/secdev/scapy.git + $ cd scapy - $ 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 +2. Install Scapy using `pip `_:: -3. Install Scapy in the standard `distutils `_ way:: + $ pip install . - $ cd scapy - $ sudo python setup.py 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 ===================== @@ -123,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 @@ -194,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. @@ -269,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. @@ -288,68 +287,28 @@ Solaris / SunOS requires ``libpcap`` (installed by default) to work. Windows ------- -.. sectionauthor:: Dirk Loss - -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. - -.. image:: graphics/scapy-win-screenshot1.png - :scale: 80 - :align: center - -You need the following software in order to install Scapy on Windows: +You need to install Npcap in order to install Scapy on Windows (should also work with Winpcap, but unsupported nowadays): - * `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``. + * 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 -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). +Once that is done, you can `continue with Scapy's installation <#latest-release>`_. -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. +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). -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>`_. +Screenshots +^^^^^^^^^^^ -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 eab616bae0c..6abee5daa66 100644 --- a/doc/scapy/layers/automotive.rst +++ b/doc/scapy/layers/automotive.rst @@ -769,13 +769,13 @@ Create CAN-frames from an ISOTP message:: Send ISOTP message over ISOTP socket:: - isoTpSocket = ISOTPSocket('vcan0', sid=0x241, did=0x641) + isoTpSocket = ISOTPSocket('vcan0', tx_id=0x241, rx_id=0x641) isoTpMessage = ISOTP('Message') isoTpSocket.send(isoTpMessage) Sniff ISOTP message:: - isoTpSocket = ISOTPSocket('vcan0', sid=0x641, did=0x241) + isoTpSocket = ISOTPSocket('vcan0', tx_id=0x641, rx_id=0x241) packets = isoTpSocket.sniff(timeout=0.5) ISOTP Sockets @@ -817,7 +817,7 @@ 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 @@ -834,7 +834,7 @@ 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) + sock = ISOTPSocket("can0", tx_id=0x641, rx_id=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. @@ -848,7 +848,7 @@ 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: + with ISOTPSocket("can0", tx_id=0x641, rx_id=0x241) as sock: sock.send(...) Usage with python-can CANSockets:: @@ -856,7 +856,7 @@ 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(bustype='socketcan', channel="can0"), sid=0x641, did=0x241) as sock: + with ISOTPSocket(CANSocket(bustype='socketcan', channel="can0"), tx_id=0x641, rx_id=0x241) as sock: sock.send(...) This second example allows the usage of any ``python_can.interface`` object. @@ -886,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:: @@ -904,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() @@ -1158,7 +1158,7 @@ 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() @@ -1183,7 +1183,7 @@ Usage example:: 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) @@ -1364,6 +1364,454 @@ Request the Vehicle Identification Number (VIN):: .. image:: ../graphics/animations/animation-scapy-obd.svg +Message Authentication (AUTOSAR SecOC) +====================================== + +AutoSAR SecOC is a security architecture protecting communication between ECUs in a vehicle from cyber-attacks. + +- **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 + +.. figure:: ../graphics/automotive/autosar1.png + :alt: Overview SecOC. Author: AUTOSAR + +Generation +---------- + +- 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 +---------- + +.. 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 + +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 + +Profiles +-------- + +AutoSAR specifies three profiles for truncated freshness value and MAC sizes. All use CMAC with AES128: + +- **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 + +- **Profile 3 (JASPAR)** + - Algorithm: CMAC/AES-128 + - Freshness value: 64 bits + - Truncated Freshness value: 4 bits + - MAC: 28 bits + +Freshness Value +--------------- + +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 + +Sync Message +------------ + +Synchronizes the 'Trip Counter' and 'Reset Counter' across all ECUs to maintain a consistent freshness value. + +- 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 + +SecOC in Scapy +============== + +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. + +General Implementation Difficulties +----------------------------------- + +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. + +Customization +------------- + +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. + +.. code-block:: python + + 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. + +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. + +.. code-block:: python + + # Suppose this is our SecOC packet + pkt: My_SecOC_CANFD + + # 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 + + # 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") + + + +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) + + 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") + + 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") + + def close(self) -> None: + if self.closed: + return + self.closed = True + self.read_pipe.close() + self.write_pipe.close() + + def reset(self) -> None: + self.off() + time.sleep(self.delay_off) + self.on() + time.sleep(self.delay_on) + +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 ==================== @@ -1400,7 +1848,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. :: diff --git a/doc/scapy/layers/bluetooth.rst b/doc/scapy/layers/bluetooth.rst index 1e8b0c69161..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`` @@ -638,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..7e08b59202e --- /dev/null +++ b/doc/scapy/layers/dcerpc.rst @@ -0,0 +1,482 @@ +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` (unfinished) + +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, + ) + + +.. note:: Currently, a DCERPC_Server will let a client bind on all interfaces that Scapy has registered (imported). Supposedly though, you know which RPCs are going to be queried. + +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..d4b3d6571bd --- /dev/null +++ b/doc/scapy/layers/gssapi.rst @@ -0,0 +1,137 @@ +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` + +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>`_ \ No newline at end of file diff --git a/doc/scapy/layers/http.rst b/doc/scapy/layers/http.rst index 9912f80aa68..b7c5d5811fa 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 = 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: @@ -120,18 +199,9 @@ Send an HTTPRequest to ``www.secdev.org`` and write the result in a file: ``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:: diff --git a/doc/scapy/layers/kerberos.rst b/doc/scapy/layers/kerberos.rst index 4041fcb515b..168e2d7ecab 100644 --- a/doc/scapy/layers/kerberos.rst +++ b/doc/scapy/layers/kerberos.rst @@ -1,21 +1,382 @@ Kerberos ======== -.. note:: Kerberos per `RFC4120 `_ + `RFC6113 `_ (FAST) +.. note:: Kerberos per `RFC4120 `_ + `RFC6113 `_ (FAST) + `[MS-KILE] `_ (Windows) -High-Level -__________ +Scapy's Kerberos implementation is accessed through two main components: -Scapy includes a (tiny) kerberos client, that has basic functionalities such as: +- :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"))) + +- **Renew a TGT or ST**: + +.. code:: + + >>> t.renew(0) # renew TGT + >>> t.renew(1) # renew ST. Works only with 'host/' SPNs + +- **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. + +- **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 | ++---------------------------------------+--------------------------------+ + +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. +.. 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", "192.168.122.17", password="Password1") + >>> res = krb_as_req("user1@DOMAIN.LOCAL", password="Password1") This is what it looks like with wireshark: @@ -45,12 +406,73 @@ The result is a named tuple with both the full AP-REP and the decrypted session >>> 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:** -Low-level -_________ +.. 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) -Decrypt kerberos packets ------------------------- + +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: @@ -108,9 +530,9 @@ You likely want to decrypt ``pkt.root.padata[0].padataValue`` which is an :class >>> from scapy.libs.rfc3961 import Key, EncryptionType >>> enc = pkt[Kerberos].root.padata[0].padataValue - >>> k = Key(EncryptionType.AES256, key=hex_bytes("7fada4e566ae4fb270e2800a23ae87127a819d42e69b5e22de0ddc63da80096d")) + >>> 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``. This is the same value than ``enc.etype.val``, which allows to know which key to use. +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: @@ -120,7 +542,7 @@ We can then proceed to perform the decryption: pausec=0x9a4db |> Compute Kerberos keys ---------------------- +~~~~~~~~~~~~~~~~~~~~~ .. note:: Encryption for Kerberos 5 is defined in `RFC3961 `_ @@ -141,7 +563,7 @@ Let's run a few examples: >>> # Get the AES256 key for User1@DOMAIN.LOCAL with "Password1" >>> from scapy.libs.rfc3961 import Key, EncryptionType - >>> Key.string_to_key(EncryptionType.AES256, b"Password1", b"DOMAIN.LOCALUser1") + >>> 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' @@ -150,26 +572,26 @@ Let's run a few examples: .. 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, b"password", b"ATHENA.MIT.EDUraeburn", struct.pack(">L", 1200)) - >>> print(bytes_hex(k.key)) - b'4c01cd46d632d01e6dbe230a01ed642a' + >>> 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 ------------- +Decrypt FAST manually +~~~~~~~~~~~~~~~~~~~~~ -.. note:: Have a look at `RFC6113 `_ for Kerberos FAST +.. 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. Courtesy of Aurélien Bordes + FAST armoring in AS-REQ. Credit to `this paper by A. Bordes `_. .. code:: pycon - >>> pkt = Ether(hex_bytes(b'52540013d0835254003ea3be08004502089636a1400080063ad3c0a87fd2c0a87fc8fecc0058eea93069573b278e50180402897400000000086a6a82086630820862a103020105a20302010aa38207a23082079e3082079aa10402020088a28207900482078ca082078830820784a082064a30820646a003020101a182063d048206396e82063530820631a003020105a10302010ea20703050000000000a38205796182057530820571a003020105a10c1b0a444f4d312e4c4f43414ca21f301da003020102a11630141b066b72627467741b0a444f4d312e4c4f43414ca382053930820535a003020112a103020102a282052704820523acc8b7671c0d50522f1a8d8452ce450aceb40fff0229e8ee546bccf1512e4877ef93dde465595260a6a5a8e85ea38600ce8dff7d510f3c744e2c43eb9d3187d638f716c29b6e7aa9eb407de28d0161f49013966eda0a161ff174dad42e7aa500cfe298541215448013ffe4883b6b1166f908f50de129487fe77fff874fd4102cdcce8db8dbeb8da02f08cc88b3790cdad5ec499959c7e79d6fef107d1e17ce80cc3df050b7e7a1c31f278e4fd4ea9523c950876f174be363234f8495b9550de1560ba17daeafbf133f78991053d929ad3fd668327d42288e6581671daaef908682ee282e17c31d8f8bb55d27fce155ee2e84a2ff8bc9600891be15e6ede3e1bbd2742a7af8b0a32c48973c9e3776a69647bab11592756c5a15b9101c392efa35d000abb3dabccd97e64426e3fd8d47e0e369c83b5391f38947d536d351c061081d654eef1a3861cdb2ea2bc48222b450d1b7d09c0670493bccc60dfcaa5cfe46fd50adf8e388204a4691dc5f0c3dbae0b4da6ac2dd781f149a444840aaa3a3c3befb5a5c04ee0405baed66afcf9b988d10ea14a955f43df79465e6fc02a12bce3870988950f1ab48e1a4f876f351671c5061e6399a63cb0479f7bd017dfd9bc5be192faf6d4f11e6ee6003933eeaf632f0056c4c1ccd183d7977cfca85419fe5b039674419d802068e792c9576ae2a88bfbeb1f59273226782c6efb288717d8f7a4bc3bf4c697fcac1adc1829f0a914f2559b278ccadd108eb87a11dacc88e4302e9af627474e57171192b94c6b358f8f98e308596215d2fb9d9c2b49c4cbedcb43fc231b86f0493d56b82962cf3383a84f8922c2b99f8fa8fdd85797b09a6e60f72007c0379988be2ff1cfc16f21300c1b4b784174005a9185f760e68ef94b9384eb24decee31b63d1b92278cd75b85d4d80c4e83306533a9d95aa6207cbfbeb0970a41c44aba59839f007923ecd8ff0de8314990a435dbea4dedbee16faf5ab2be9f96d691cfa983a6c843bd183f84c1b4998a3eaa907cae6b82b0ae8363f3edd8cb03d3c9c60ff55a84d8a292ea20555fbd6ce5ad4ad7a6b4bc5bff2e02c477a7a8a98d5a387d389caa172c400b151d95871b2aa16a040dc71a9be5f0774b06a5ca87674ccb4109a2c41db9e3160704218ad495d0751194fbef4becae4d7be24b9d968da592256a2b22cf724e989e71a60d0603b59bebd475285f793794b7a18af49a2b68670e3a6247c453274e35c863a16b5023c6c94659e25abb27c760f989ac0bbf9a5b125d0ea34fb03225cc93d5b8b6829e906883ee76cf8ee61dfacc488e8dc5cbc8ba9705a9e915a68f838232394f97fb1aac4a2a90fe17d46f9c51946a2bf9598df7f5b5e7ee692a78860eea3cef748a5be36529228e40b4aec83ebc8bb14176a4c565b06500e9517229b8340c55812101dbbc6bee693c35873082a5a1a53b35cf3509193d4dc5175c9360a00da71692ba205b3264aecc9ecc8bca31fec43efc8701423bb484f6f21699439dd30f71228f16eaab96b7de3547721d1635bbfe50678900ac378a4958b6c34964f3e0dc843880dbde57fb4a76ab85eba2b190bfdaefc7ba17e109f839493b0f2d6fc7ea17403bebe06f2809314ca514606f54668082364ed6752019f27e1df74f93fcf1c25630a29713a89d4a998c444bc91279c6fc66e0aa5dec72be316e1160cf9f90d5915c464b6bfec5216e901be4726db596a15745511c63736a69ac9ecb9e86601c631b4992653c320e6983562fa613134560cb606621e9661ac5961313ee70868ab48d6010173d8a96fffdb2baf4afe18c846d3fed6f30b9a809d72e647735fc536edec543abc232480d28660395a4819e30819ba003020112a281930481901273d5af61ad426d51d0757e897917caeb6fc1b6950554e8d750f95d27f444e3aaf7ae0bf4595b5e906d9682dbdeedcf6eb42a84ab8092997b783f57710127228165deeb2ce5e09e2ddc71555dc31970a8312d888b8ae766382098276d62b4bd76f34cbc889e24ad5405ec037ceb724fdb71fe247fe2a414a037ed33c796f4475fcfb5993eed147b6d63d740d58da5b0a1173015a003020110a10e040ca75f26db2301c6970feba452a282011930820115a003020112a282010c048201083caf34ecefd84c786703c20039de61bc01ebed9be7e51c90a582fec852696bf92fd165cd5b5ef0f9b8edb666c9cca5690d364e5c6ad69e7d5bc7e055757aaa6206428a302524144d5d97cc0b64db13335045039171ed1f0d111ca1bd4651ebca3d74db029e5c6d3c7f8600c44e55b14cd3c7f6a15c9133400e4255d71f237bf288c186137cd04a5f2cabba3166de5bf11190a2e5962e4dbbfb9801e3be73ede5a536eb27a086b644f12245198459c063b8ecba228e1f9209e05a5bcbb39a12651e103438ee7998e666d8628812fa34bc07f4c4d0a4d86fe207128de37e1ffd169a4cb879cb5b9db8f9c3e86143bfd43409ca47e90f3bc848a1838fce7209f57296e44963a2d1e3d4a481af3081aca00703050040810010a11a3018a003020101a111300f1b0d61646d2d722d786d617274696ea2061b04444f4d31a3193017a003020102a110300e1b066b72627467741b04444f4d31a511180f32303337303931333032343830355aa611180f32303337303931333032343830355aa70602043f58a7a0a81530130201120201110201170201180202ff79020103a91d301b3019a003020114a112041053525620202020202020202020202020')) + >>> pkt = Ether(bytes.fromhex(b'52540013d0835254003ea3be08004502089636a1400080063ad3c0a87fd2c0a87fc8fecc0058eea93069573b278e50180402897400000000086a6a82086630820862a103020105a20302010aa38207a23082079e3082079aa10402020088a28207900482078ca082078830820784a082064a30820646a003020101a182063d048206396e82063530820631a003020105a10302010ea20703050000000000a38205796182057530820571a003020105a10c1b0a444f4d312e4c4f43414ca21f301da003020102a11630141b066b72627467741b0a444f4d312e4c4f43414ca382053930820535a003020112a103020102a282052704820523acc8b7671c0d50522f1a8d8452ce450aceb40fff0229e8ee546bccf1512e4877ef93dde465595260a6a5a8e85ea38600ce8dff7d510f3c744e2c43eb9d3187d638f716c29b6e7aa9eb407de28d0161f49013966eda0a161ff174dad42e7aa500cfe298541215448013ffe4883b6b1166f908f50de129487fe77fff874fd4102cdcce8db8dbeb8da02f08cc88b3790cdad5ec499959c7e79d6fef107d1e17ce80cc3df050b7e7a1c31f278e4fd4ea9523c950876f174be363234f8495b9550de1560ba17daeafbf133f78991053d929ad3fd668327d42288e6581671daaef908682ee282e17c31d8f8bb55d27fce155ee2e84a2ff8bc9600891be15e6ede3e1bbd2742a7af8b0a32c48973c9e3776a69647bab11592756c5a15b9101c392efa35d000abb3dabccd97e64426e3fd8d47e0e369c83b5391f38947d536d351c061081d654eef1a3861cdb2ea2bc48222b450d1b7d09c0670493bccc60dfcaa5cfe46fd50adf8e388204a4691dc5f0c3dbae0b4da6ac2dd781f149a444840aaa3a3c3befb5a5c04ee0405baed66afcf9b988d10ea14a955f43df79465e6fc02a12bce3870988950f1ab48e1a4f876f351671c5061e6399a63cb0479f7bd017dfd9bc5be192faf6d4f11e6ee6003933eeaf632f0056c4c1ccd183d7977cfca85419fe5b039674419d802068e792c9576ae2a88bfbeb1f59273226782c6efb288717d8f7a4bc3bf4c697fcac1adc1829f0a914f2559b278ccadd108eb87a11dacc88e4302e9af627474e57171192b94c6b358f8f98e308596215d2fb9d9c2b49c4cbedcb43fc231b86f0493d56b82962cf3383a84f8922c2b99f8fa8fdd85797b09a6e60f72007c0379988be2ff1cfc16f21300c1b4b784174005a9185f760e68ef94b9384eb24decee31b63d1b92278cd75b85d4d80c4e83306533a9d95aa6207cbfbeb0970a41c44aba59839f007923ecd8ff0de8314990a435dbea4dedbee16faf5ab2be9f96d691cfa983a6c843bd183f84c1b4998a3eaa907cae6b82b0ae8363f3edd8cb03d3c9c60ff55a84d8a292ea20555fbd6ce5ad4ad7a6b4bc5bff2e02c477a7a8a98d5a387d389caa172c400b151d95871b2aa16a040dc71a9be5f0774b06a5ca87674ccb4109a2c41db9e3160704218ad495d0751194fbef4becae4d7be24b9d968da592256a2b22cf724e989e71a60d0603b59bebd475285f793794b7a18af49a2b68670e3a6247c453274e35c863a16b5023c6c94659e25abb27c760f989ac0bbf9a5b125d0ea34fb03225cc93d5b8b6829e906883ee76cf8ee61dfacc488e8dc5cbc8ba9705a9e915a68f838232394f97fb1aac4a2a90fe17d46f9c51946a2bf9598df7f5b5e7ee692a78860eea3cef748a5be36529228e40b4aec83ebc8bb14176a4c565b06500e9517229b8340c55812101dbbc6bee693c35873082a5a1a53b35cf3509193d4dc5175c9360a00da71692ba205b3264aecc9ecc8bca31fec43efc8701423bb484f6f21699439dd30f71228f16eaab96b7de3547721d1635bbfe50678900ac378a4958b6c34964f3e0dc843880dbde57fb4a76ab85eba2b190bfdaefc7ba17e109f839493b0f2d6fc7ea17403bebe06f2809314ca514606f54668082364ed6752019f27e1df74f93fcf1c25630a29713a89d4a998c444bc91279c6fc66e0aa5dec72be316e1160cf9f90d5915c464b6bfec5216e901be4726db596a15745511c63736a69ac9ecb9e86601c631b4992653c320e6983562fa613134560cb606621e9661ac5961313ee70868ab48d6010173d8a96fffdb2baf4afe18c846d3fed6f30b9a809d72e647735fc536edec543abc232480d28660395a4819e30819ba003020112a281930481901273d5af61ad426d51d0757e897917caeb6fc1b6950554e8d750f95d27f444e3aaf7ae0bf4595b5e906d9682dbdeedcf6eb42a84ab8092997b783f57710127228165deeb2ce5e09e2ddc71555dc31970a8312d888b8ae766382098276d62b4bd76f34cbc889e24ad5405ec037ceb724fdb71fe247fe2a414a037ed33c796f4475fcfb5993eed147b6d63d740d58da5b0a1173015a003020110a10e040c75f02d8d2954e0ae1a9e0653a282011930820115a003020112a282010c04820108ae9bbc4629c80f4a383a69c4583824295c75f34b000b3fdbdaab073a042935e32c29e0ee2b2b446e4a6a2592362d0d593cddd74dacc24f16353776e1b5d192ad1cf5e63f66f40a134ecb87c077c30922bc0cab00ae23d187d56090d9098f843c54fabe7c012ff87e317dfe339c40911264609d489b041a4e9b52c0eb03ee88a393d17da92786bd1716b92eb0d7a5a24a64ade0870dea8a7e138acdf209ee277cb3fadeedab173fd64cc10a1004010774658b94852639bda10a5e8aff29174e3d2c7032c32631b074afdac0e6832bae74de9be19e522f63bc8499753a209291fee1861c29096cc8ee3cfda5be235b0aa95635916edcfcdaf90b896e2eaa5a57d5e4da0b00408f4201a481af3081aca00703050040810010a11a3018a003020101a111300f1b0d61646d2d302d66617374656e62a2061b04444f4d31a3193017a003020102a110300e1b066b72627467741b04444f4d31a511180f32303337303931333032343830355aa611180f32303337303931333032343830355aa70602043f58a7a0a81530130201120201110201170201180202ff79020103a91d301b3019a003020114a112041053525620202020202020202020202020')) >>> pkt[TCP].payload.show() ###[ KerberosTCPHeader ]### len = 2154 @@ -212,7 +634,7 @@ Let's take a Kerberos AS-REQ packet with FAST armoring (RFC6113): | | | | | | | kvno = None | | | | | | | cipher = \xed\x14{mc\xd7@\xd5\x8d\xa5\xb0']> | | | | checksumtype= 'HMAC-SHA1-96-AES256' 0x10 - | | | | checksum = + | | | | checksum = | | | | \encFastReq\ | | | | |###[ EncryptedData ]### | | | | | etype = 'AES-256' 0x12 @@ -224,7 +646,7 @@ Let's take a Kerberos AS-REQ packet with FAST armoring (RFC6113): | | \cname \ | | |###[ PrincipalName ]### | | | nameType = 'NT-PRINCIPAL' 0x1 - | | | nameString= [] + | | | nameString= [] | | realm = | | \sname \ | | |###[ PrincipalName ]### @@ -254,7 +676,7 @@ We have the krbtgt for this demo: >>> from scapy.libs.rfc3961 import Key, EncryptionType >>> krbtgt_hex = "ac67a63d7155791fe31dace230ab516e818c453dfdbd44cbe691b240725c4907" - >>> krbtgt = Key(EncryptionType.AES256, key=hex_bytes(krbtgt_hex)) + >>> krbtgt = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex(krbtgt_hex)) We can therefore decrypt the first payload: @@ -285,7 +707,7 @@ We can therefore decrypt the first payload: addresses = None [...] -We can see the ticket session key in there, let's retrieve it and build a ``Key`` object: +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=)`` @@ -363,7 +785,7 @@ That we can now use to decrypt the last payload: | \cname \ | |###[ PrincipalName ]### | | nameType = 'NT-PRINCIPAL' 0x1 - | | nameString= [] + | | nameString= [] | realm = | \sname \ | |###[ PrincipalName ]### @@ -381,8 +803,8 @@ That we can now use to decrypt the last payload: | encAuthorizationData= None | additionalTickets= None -Encryption ----------- +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`. @@ -396,7 +818,7 @@ For instance, during pre-authentication, encode ``PA-ENC-TIMESTAMP``: >>> # Create the PADATA layer with its EncryptedValue >>> pkt = PADATA(padataType=0x2, padataValue=EncryptedData()) >>> # Compute the key - >>> key = Key.string_to_key(EncryptionType.AES256, b"Password1", b"DOMAIN.LOCALUser1") + >>> 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))) 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/ntlm.rst b/doc/scapy/layers/ntlm.rst deleted file mode 100644 index 6a2561e60ec..00000000000 --- a/doc/scapy/layers/ntlm.rst +++ /dev/null @@ -1,128 +0,0 @@ -NTLM -==== - -Scapy provides dissection & build methods for NTLM and other Windows mechanisms. -In particular, the ``ntlm_relay`` command allows to perform some NTLM relaying attacks. - -.. note:: - - Read `this article from hackndo `_ to understand how NTLM relay work and what we are trying to achieve here. - -Examples --------- - - -**Requirement: Answer to all netbios requests with the local IP** - -.. code:: - - netbios_announce(iface="virbr0") - -SMB <-> SMB: SMB relay with force downgrade to SMB1 -___________________________________________________ - -.. note:: - - ``server_kwargs={"REAL_HOSTNAME":"WIN1"}`` is compulsory on SMB1 if the name that you are spoofing is different from the real name. Set this to avoid getting a ``STATUS_DUPLICATE_NAME`` - -.. code:: - - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_SMB_Client, iface="virbr0", ALLOW_SMB2=False, server_kwargs={"REAL_HOSTNAME":"WIN1"}) - -.. image:: ../graphics/ntlm/ntlmrelay_smb.png - :align: center - -.. image:: ../graphics/ntlm/ntlmrelay_smb_win1.png - :align: center - -.. image:: ../graphics/ntlm/ntlmrelay_smb_wireshark.png - :align: center - -.. image:: ../graphics/ntlm/ntlmrelay_smb_win2.png - :align: center - - -SMB <-> SMB: Perform a SMB2 relay - default -___________________________________________ - -.. code:: - - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_SMB_Client, iface="virbr0") - -.. warning:: - - The legitimate client will the validity of the negotiated flags by using a signed IOCTL ``FSCTL_VALIDATE_NEGOTIATE_INFO`` which we cannot fake, therefore losing the connection. - We however still have created an authenticated illegitimate client to the server, where we won't be performing that check, that we can use. See the case right below. - -SMB <-> SMB: Perform a SMB2 relay - scripted -____________________________________________ - -Because of the note above, we now close the legitimate client & run commands on the server directly. - -.. note:: - - Setting ``ECHO`` to ``False`` on the server instantly terminates the connection once Authentication is successful. - We set ``RUN_SCRIPT`` to ``True`` to run a script (in ``DO_RUN_SCRIPT`` in the automaton) once Authentication is successful. Note that ``REAL_HOSTNAME`` is required in this case. - -.. code:: - - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_SMB_Client, iface="virbr0", server_kwargs={"ECHO": False}, client_kwargs={"REAL_HOSTNAME": "WIN1", "RUN_SCRIPT": True}) - -.. image:: ../graphics/ntlm/ntlmrelay_smb2.png - :align: center - -SMB <-> SMB: SMB relay with force downgrade to SMB1 & drop NEGOEX -_________________________________________________________________ - -This example points out that the NEGOEX messages are optional: dropping them has no effect on the SMB1 connection. - -.. code:: - - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_SMB_Client, iface="virbr0", ALLOW_SMB2=False, server_kwargs={"PASS_NEGOEX": False, "REAL_HOSTNAME":"WIN1"}) - -SMB <-> SMB: SMB relay with force downgrade to SMB1 & drop extended security -____________________________________________________________________________ - -This probably won't work. SMB1 clients abort unextended connections these days. - -.. code:: - - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_SMB_Client, iface="virbr0", ALLOW_SMB2=False, server_kwargs={"REAL_HOSTNAME":"WIN1"}, DROP_EXTENDED_SECURITY=True) - -SMB2 <-> LDAP: relay SMB's NTLM to an LDAP server -_________________________________________________ - -.. note:: - - Negotiating LDAP using SMB's credentials does work, but sets the ``SIGN`` field during the NTLM exchange. This causes LDAP to require signing. Read `the HackNDo article ` for more info. - -.. code:: - - load_layer("ldap") - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_LDAP_Client, iface="virbr0") - -.. image:: ../graphics/ntlm/ntlmrelay_ldap.png - :align: center - -Let's try using DROP-THE-MIC-v1 or DROP-THE-MIC-v2: - -.. code:: - - load_layer("ldap") - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_LDAP_Client, iface="virbr0", DROP_MIC_v1=True) - -.. code:: - - load_layer("ldap") - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_LDAP_Client, iface="virbr0", DROP_MIC_v2=True) - -SMB2 <-> LDAPS: relay SMB's NTLM to an LDAPS server -___________________________________________________ - -.. code:: - - load_layer("ldap") - ntlm_relay(NTLM_SMB_Server, "192.168.122.156", NTLM_LDAPS_Client, iface="virbr0") - -.. image:: ../graphics/ntlm/ntlmrelay_ldaps.png - :align: center 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/routing.rst b/doc/scapy/routing.rst index a426d9fd93d..2f9a9ec8010 100644 --- a/doc/scapy/routing.rst +++ b/doc/scapy/routing.rst @@ -1,36 +1,86 @@ -************* -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'] -You can also use the :py:attr:`conf.ifaces ` object to get interfaces. -In this example, the object is first displayed as as column. Then, the :py:attr:`dev_from_index() ` is used to access the interface at index 2. +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 - SRC INDEX IFACE IPv4 IPv6 MAC - sys 2 eth0 10.0.0.5 fe80::10a:2bef:dc12:afae Microsof:12:cb:ef - sys 1 lo 127.0.0.1 ::1 00:00:00:00:00:00 - >>> conf.ifaces.dev_from_index(2) - + 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 ----------- @@ -61,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 @@ -72,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`` @@ -84,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 @@ -94,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/troubleshooting.rst b/doc/scapy/troubleshooting.rst index 3025e3c097d..4131ec280f6 100644 --- a/doc/scapy/troubleshooting.rst +++ b/doc/scapy/troubleshooting.rst @@ -8,21 +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 `_: + + *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).* + +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) -- **Using Libpcap** - ``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 libpcap disabled):** - You should set the interface in monitor mode on your own. I personally like - to use iwconfig for that (replace ``monitor`` by ``managed`` to disable):: + 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:: - $ sudo ifconfig IFACE down - $ sudo iwconfig IFACE mode monitor - $ sudo ifconfig IFACE up + $ iw dev wlan0 interface add mon0 type monitor + $ ifconfig mon0 up -**If you are using Npcap:** please note that Npcap ``npcap-0.9983`` broke the 802.11 util back in 2019. It has yet to be fixed (as of Npcap 0.9994) so in the meantime, use `npcap-0.9982.exe `_ + If you want to enable monitor mode manually, have a look at https://wiki.wireshark.org/CaptureSetup/WLAN#linux -.. note:: many adapters do not support monitor mode, especially on Windows, or may incorrectly report the headers. See `the Wireshark doc about this `_ +.. 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. @@ -35,12 +47,14 @@ 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 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. -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):: +.. 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. + +**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) / ICMP()) + >>> sr1(IP() / ICMP()) > With IPv6, you can simply do:: @@ -50,11 +64,20 @@ With IPv6, you can simply do:: > # Layer 2 - >>> conf.iface = "lo" - >>> srp1(Ether() / IPv6() / ICMPv6EchoRequest()) + >>> srp1(Ether() / IPv6() / ICMPv6EchoRequest(), iface=conf.loopback_name) >> -On Windows, BSD, and macOS, you must deactivate the local firewall and set ````conf.iface``` to the loopback interface prior to using the following commands:: +.. 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()) @@ -63,11 +86,45 @@ On Windows, BSD, and macOS, you must deactivate the local firewall and set ````c > # Layer 2 - >>> srp1(Loopback() / IP() / ICMP()) + >>> srp1(Loopback() / IP() / ICMP(), iface=conf.loopback_name) >> - >>> srp1(Loopback() / IPv6() / ICMPv6EchoRequest()) + >>> 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 b0c907ca20f..1202d9cc400 100644 --- a/doc/scapy/usage.rst +++ b/doc/scapy/usage.rst @@ -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 ======================= ==================================================== @@ -268,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 ------- @@ -408,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")) @@ -550,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: @@ -784,11 +793,17 @@ Advanced Sniffing - Sniffing Sessions Scapy includes some basic Sessions, but it is possible to implement your own. Available by default: -- :py:class:`~scapy.sessions.IPSession` -> *defragment IP packets* on-the-flow, to make a stream usable by ``prn``. +- :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 / DCERPC + - 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 @@ -800,9 +815,12 @@ 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 :py:class:`~scapy.sessions.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. -.. note:: Would you need it, you can use: ``class TLS_over_TCP(TLSSession, TCPSession): pass`` to sniff TLS packets that are defragmented. How to use TCPSession to defragment TCP packets ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -829,6 +847,8 @@ The ``data`` argument is bytes and the ``metadata`` argument is a dictionary whi - ``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 ------- @@ -1101,7 +1121,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 @@ -1230,21 +1250,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:: - - >>> # Of course, conf.iface can be replaced by any interfaces accessed through conf.ifaces - ... conf.iface.setmonitor(True) + See the :doc:`TroubleShooting ` section for more information on the usage of Monitor mode among Scapy. -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", @@ -1361,21 +1369,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.', @@ -1421,6 +1429,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 ----------------- @@ -1452,28 +1472,41 @@ Visualizing the results in a list:: >>> res.nsummary(prn=lambda s,r: r.src, lfilter=lambda s,r: r.haslayer(ISAKMP) ) -DNS spoof ---------- +DNS server +---------- -See :class:`~scapy.layers.dns.DNS_am`:: +By default, ``dnsd`` uses a joker (IPv4 only): it answers to all unknown servers with the joker. See :class:`~scapy.layers.dns.DNS_am`:: - >>> dns_spoof(iface="tap0", joker="192.168.1.1") + >>> dnsd(iface="tap0", match={"google.com": "1.1.1.1"}, joker="192.168.1.1") -LLMNR spoof ------------ +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" - >>> llmnr_spoof(iface="tap0", filter_ips=Net("10.0.0.1/24")) + >>> llmnrd(iface="tap0", from_ip=Net("10.0.0.1/24")) -Netbios spoof -------------- +Note that ``llmnrd`` extends the ``dnsd`` API. + +Netbios server +-------------- See :class:`~scapy.layers.netbios.NBNS_am`:: - >>> nbns_spoof(iface="eth0") # With local IP - >>> nbns_spoof(iface="eth0", ip="192.168.122.17") # With some other IP + >>> 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) --------------------------------------------- @@ -1482,6 +1515,29 @@ Node status request (get NetbiosName from IP) >>> 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 ------------------- @@ -1625,7 +1681,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: @@ -1786,7 +1842,7 @@ Scapy dissects slowly and/or misses packets under heavy loads. .. note:: - Please bare 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. + 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 ^^^^^^^^ @@ -1803,6 +1859,39 @@ There are quite a few ways of speeding up scapy's dissection. You can use all of # 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/vagrant_ci/Vagrantfile b/doc/vagrant_ci/Vagrantfile index c5b6af96ff7..0ba833fd421 100644 --- a/doc/vagrant_ci/Vagrantfile +++ b/doc/vagrant_ci/Vagrantfile @@ -8,13 +8,18 @@ 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-13.0-RELEASE" + bsd.vm.box = "freebsd/FreeBSD-14.0-RELEASE" bsd.vm.provision "shell", path: "provision_freebsd.sh" end diff --git a/doc/vagrant_ci/provision_freebsd.sh b/doc/vagrant_ci/provision_freebsd.sh index f3044049951..56a9b92203d 100644 --- a/doc/vagrant_ci/provision_freebsd.sh +++ b/doc/vagrant_ci/provision_freebsd.sh @@ -5,13 +5,15 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi +PACKAGES="git python39 python311 py39-virtualenv py39-pip py39-sqlite3 py311-sqlite3 bash rust sudo" + pkg update -pkg install --yes git python2 python3 py37-virtualenv py27-sqlite3 py37-sqlite3 bash rust +pkg install --yes $PACKAGES bash git clone https://github.com/secdev/scapy cd scapy export PATH=/usr/local/bin/:$PATH -virtualenv-3.7 -p python3.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 e887d606437..a4d59bd27e4 100644 --- a/doc/vagrant_ci/provision_netbsd.sh +++ b/doc/vagrant_ci/provision_netbsd.sh @@ -5,17 +5,18 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -RELEASE="9.0_2020Q4" +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/${RELEASE}/All/" pkg_delete curl -pkg_add git python27 python38 py38-virtualenv py27-sqlite3 py38-sqlite3 py38-expat rust mozilla-rootcerts-openssl +pkg_add -u $PACKAGES git clone https://github.com/secdev/scapy cd scapy -virtualenv-3.8 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 397c4b8653f..1759249f391 100644 --- a/doc/vagrant_ci/provision_openbsd.sh +++ b/doc/vagrant_ci/provision_openbsd.sh @@ -5,13 +5,15 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -sudo pkg_add git python-2.7.18p0 python3 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..109963e5cad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,99 @@ +[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 = { text="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", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "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 ce21208a155..87b6f50e91d 100755 --- a/run_scapy +++ b/run_scapy @@ -2,16 +2,6 @@ DIR=$(dirname "$0") if [ -z "$PYTHON" ] then - ARGS="" - for arg in "$@" - do - case $arg - in - -2) PYTHON=python2;; - -3) PYTHON=python3;; - *) ARGS="$ARGS $arg";; - esac - done PYTHON=${PYTHON:-python3} fi $PYTHON --version > /dev/null 2>&1 @@ -20,4 +10,4 @@ then echo "WARNING: '$PYTHON' not found, using 'python' instead." PYTHON=python fi -PYTHONPATH=$DIR exec "$PYTHON" -m scapy $ARGS +PYTHONPATH=$DIR exec "$PYTHON" -m scapy $@ diff --git a/run_scapy.bat b/run_scapy.bat index 11c73621a8d..d801bbdc0e3 100644 --- a/run_scapy.bat +++ b/run_scapy.bat @@ -1,17 +1,23 @@ @echo off +setlocal set PYTHONPATH=%~dp0 REM shift will not work with %* set "_args=%*" -IF "%1" == "-2" ( - set PYTHON=python - set "_args=%_args:~3%" -) ELSE IF "%1" == "-3" ( - set PYTHON=python3 +IF "%PYTHON%" == "" set PYTHON=py +WHERE %PYTHON% >nul 2>&1 +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 ) -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/scapy/__init__.py b/scapy/__init__.py index 10739ec6200..3eb172ec490 100644 --- a/scapy/__init__.py +++ b/scapy/__init__.py @@ -10,10 +10,16 @@ https://scapy.net """ +import datetime import os import re import subprocess +__all__ = [ + "VERSION", + "__version__", +] + _SCAPY_PKG_DIR = os.path.dirname(__file__) @@ -31,8 +37,46 @@ def _parse_tag(tag): # 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) + 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(): @@ -77,7 +121,7 @@ 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") @@ -91,45 +135,52 @@ def _version(): :return: the Scapy version """ - # Rely on git archive "export-subst" git attribute. - # See 'man gitattributes' for more details. - # Note: describe is only supported with git >= 2.32.0 - # but we use it to workaround GH#3121 - git_archive_id = '$Format:%h %(describe)$'.strip().split() - sha1 = git_archive_id[0] - tag = git_archive_id[1] - if "Format" not in sha1: - # We are in a git archive - if "describe" in tag: - # git is too old! - tag = "" - if tag: - return _parse_tag(tag) - elif sha1: - return "git-archive." + sha1 - return 'unknown.version' - # Fallback to calling git + # 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: - 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/ansmachine.py b/scapy/ansmachine.py index 7232b31dcd6..3c5cb865d33 100644 --- a/scapy/ansmachine.py +++ b/scapy/ansmachine.py @@ -11,19 +11,19 @@ # Answering machines # ######################## +import abc import functools +import threading import socket import warnings from scapy.arch import get_if_addr from scapy.config import conf -from scapy.sendrecv import send, sniff, AsyncSniffer +from scapy.sendrecv import sendp, sniff, AsyncSniffer from scapy.packet import Packet from scapy.plist import PacketList -import scapy.libs.six as six - -from scapy.compat import ( +from typing import ( Any, Callable, Dict, @@ -32,14 +32,13 @@ Tuple, Type, TypeVar, - _Generic_metaclass, cast, ) _T = TypeVar("_T", Packet, PacketList) -class ReferenceAM(_Generic_metaclass): +class ReferenceAM(type): def __new__(cls, name, # type: str bases, # type: Tuple[type, ...] @@ -68,8 +67,7 @@ def __new__(cls, return obj -@six.add_metaclass(ReferenceAM) -class AnsweringMachine(Generic[_T]): +class AnsweringMachine(Generic[_T], metaclass=ReferenceAM): function_name = "" filter = None # type: Optional[str] sniff_options = {"store": 0} # type: Dict[str, Any] @@ -77,7 +75,7 @@ class AnsweringMachine(Generic[_T]): "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(send) + send_function = staticmethod(sendp) def __init__(self, **kargs): # type: (Any) -> None @@ -145,9 +143,10 @@ def is_request(self, req): # type: (Packet) -> int return 1 + @abc.abstractmethod def make_reply(self, req): # type: (Packet) -> _T - return req + pass def send_reply(self, reply, send_function=None): # type: (_T, Optional[Callable[..., None]]) -> None @@ -191,58 +190,50 @@ def run(self, *args, **kargs): ) 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) - -class AnsweringMachineUtils: - @staticmethod - def reverse_packet(req, mirror_src=False): - # type: (Packet, bool) -> Optional[Packet] - from scapy.layers.inet import IP, TCP, UDP - from scapy.layers.inet6 import IPv6 - if IP in req: - resp = IP( - dst=req[IP].src, - src=mirror_src and req[IP].dst or None, - ) - elif IPv6 in req: - resp = IPv6( - dst=req[IPv6].src, - src=mirror_src and req[IPv6].dst or None, - ) - else: - return None - for layer in [UDP, TCP]: - if req.haslayer(layer): - resp /= layer(dport=req.sport, sport=req.dport) - break - return cast(Packet, resp) + 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 clients + 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 = conf.raw_layer + self.cls = cls def close(self): # type: () -> None @@ -251,7 +242,11 @@ def close(self): def sniff(self): # type: () -> None from scapy.supersocket import StreamSocket - ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + 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() @@ -279,6 +274,19 @@ def sniff(self): 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 super(AnsweringMachineTCP, self).make_reply(req) + 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 297cfea0cef..316d398f570 100644 --- a/scapy/arch/__init__.py +++ b/scapy/arch/__init__.py @@ -14,22 +14,31 @@ from scapy.config import conf, _set_conf_sockets from scapy.consts import LINUX, SOLARIS, WINDOWS, BSD from scapy.data import ( - ARPHDR_ETHER, - ARPHDR_LOOPBACK, - ARPHDR_PPP, - ARPHDR_TUN, - IPV6_ADDR_GLOBAL + IPV6_ADDR_GLOBAL, + IPV6_ADDR_LOOPBACK, +) +from scapy.error import log_loading +from scapy.interfaces import ( + _GlobInterfaceType, + network_name, + resolve_iface, ) -from scapy.error import log_loading, Scapy_Exception -from scapy.interfaces import NetworkInterface, network_name from scapy.pton_ntop import inet_pton, inet_ntop +from scapy.libs.extcap import load_extcap + # Typing imports -from scapy.compat import ( +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. @@ -41,11 +50,12 @@ "get_if_list", "get_if_raw_addr", "get_if_raw_addr6", - "get_if_raw_hwaddr", "get_working_if", "in6_getifaddr", + "read_nameservers", "read_routes", "read_routes6", + "load_extcap", "SIOCGIFHWADDR", ] @@ -66,7 +76,7 @@ def str2mac(s): def get_if_addr(iff): - # type: (str) -> str + # type: (_GlobInterfaceType) -> str """ Returns the IPv4 of an interface or "0.0.0.0" if not available """ @@ -74,32 +84,30 @@ def get_if_addr(iff): def get_if_hwaddr(iff): - # type: (Union[NetworkInterface, str]) -> str + # type: (_GlobInterfaceType) -> str """ Returns the MAC (hardware) address of an interface """ - from scapy.arch import get_if_raw_hwaddr - addrfamily, mac = get_if_raw_hwaddr(iff) # noqa: F405 - if addrfamily in [ARPHDR_ETHER, ARPHDR_LOOPBACK, ARPHDR_PPP, ARPHDR_TUN]: - return str2mac(mac) - else: - raise Scapy_Exception("Unsupported address family (%i) for interface [%s]" % (addrfamily, iff)) # noqa: E501 + return resolve_iface(iff).mac or "00:00:00:00:00:00" def get_if_addr6(niff): - # type: (NetworkInterface) -> Optional[str] + # 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] == IPV6_ADDR_GLOBAL), None) + if x[2] == iff and x[1] == scope), None) def get_if_raw_addr6(iff): - # type: (NetworkInterface) -> Optional[bytes] + # type: (_GlobInterfaceType) -> Optional[bytes] """ Returns the main global unicast address associated with provided interface, in network format. If no global address is found, None @@ -114,11 +122,9 @@ def get_if_raw_addr6(iff): # Next step is to import following architecture specific functions: # def attach_filter(s, filter, iface) -# def get_if(iff,cmd) -# def get_if_index(iff) # def get_if_raw_addr(iff) -# def get_if_raw_hwaddr(iff) # def in6_getifaddr() +# def read_nameservers() # def read_routes() # def read_routes6() # def set_promisc(s,iff,val=1) @@ -126,7 +132,6 @@ def get_if_raw_addr6(iff): if LINUX: from scapy.arch.linux import * # noqa F403 elif BSD: - from scapy.arch.unix import read_routes, read_routes6, in6_getifaddr # noqa: E501 from scapy.arch.bpf.core import * # noqa F403 if not conf.use_pcap: # Native @@ -145,6 +150,22 @@ def get_if_raw_addr6(iff): ) SIOCGIFHWADDR = 0 # mypy compat + # DUMMYS + def get_if_raw_addr(iff: Union['NetworkInterface', str]) -> bytes: + return b"\0\0\0\0" + + def in6_getifaddr() -> List[Tuple[str, int, str]]: + return [] + + def read_nameservers() -> List[str]: + return [] + + def read_routes() -> List[str]: + return [] + + def read_routes6() -> List[str]: + return [] + if LINUX or BSD: conf.load_layers.append("tuntap") diff --git a/scapy/arch/bpf/consts.py b/scapy/arch/bpf/consts.py index 3ea84277496..df207a3397f 100644 --- a/scapy/arch/bpf/consts.py +++ b/scapy/arch/bpf/consts.py @@ -12,6 +12,12 @@ 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 @@ -23,19 +29,20 @@ IOC_IN = 0x80000000 IOC_INOUT = IOC_IN | IOC_OUT -_th = lambda x: x if isinstance(x, int) else ctypes.sizeof(x) +_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) -_IOR = lambda g, n, t: _IOC(IOC_OUT, g, n, t) -_IOW = lambda g, n, t: _IOC(IOC_IN, g, n, t) -_IOWR = lambda g, n, t: _IOC(IOC_INOUT, g, n, t) +_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 diff --git a/scapy/arch/bpf/core.py b/scapy/arch/bpf/core.py index ff0810f7eef..db03a03c3a9 100644 --- a/scapy/arch/bpf/core.py +++ b/scapy/arch/bpf/core.py @@ -7,138 +7,46 @@ Scapy *BSD native support - core """ -from __future__ import absolute_import -from ctypes import cdll, cast, pointer -from ctypes import c_int, c_ulong, c_uint, c_char_p, Structure, POINTER -from ctypes.util import find_library import fcntl import os -import re import socket import struct -import subprocess -import scapy -from scapy.arch.bpf.consts import BIOCSETF, SIOCGIFFLAGS, BIOCSETIF -from scapy.arch.common import compile_filter, _iff_flags -from scapy.arch.unix import get_if, in6_getifaddr -from scapy.compat import plain_str +from scapy.arch.bpf.consts import BIOCSETF, BIOCSETIF +from scapy.arch.common import compile_filter from scapy.config import conf from scapy.consts import LINUX -from scapy.data import ARPHDR_LOOPBACK, ARPHDR_ETHER -from scapy.error import Scapy_Exception, warning -from scapy.interfaces import InterfaceProvider, IFACES, NetworkInterface, \ - network_name -from scapy.pton_ntop import inet_ntop +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") - -# ctypes definitions - -LIBC = cdll.LoadLibrary(find_library("c")) - -LIBC.ioctl.argtypes = [c_int, c_ulong, ] -LIBC.ioctl.restype = c_int - -# The following is implemented as of Python >= 3.3 -# under socket.*. Remember to use them when dropping Py2.7 - -# See https://docs.python.org/3/library/socket.html#socket.if_nameindex - - -class if_nameindex(Structure): - _fields_ = [("if_index", c_uint), - ("if_name", c_char_p)] - - -_ptr_ifnameindex_table = POINTER(if_nameindex * 255) - -LIBC.if_nameindex.argtypes = [] -LIBC.if_nameindex.restype = _ptr_ifnameindex_table -LIBC.if_freenameindex.argtypes = [_ptr_ifnameindex_table] -LIBC.if_freenameindex.restype = None - -# Addresses manipulation functions - - -def get_if_raw_addr(ifname): - """Returns the IPv4 address configured on 'ifname', packed with inet_pton.""" # noqa: E501 - - ifname = network_name(ifname) - - # 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).strip()) - return b"\0\0\0\0" - - # Get IPv4 addresses - addresses = [ - line.strip() 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 - - ifname = network_name(ifname) - # Handle the loopback interface separately - if ifname == conf.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).strip()) - - # Get MAC addresses - addresses = [ - line.strip() 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(':')] - - # Check that the address length is correct - if len(mac) != 6: - raise Scapy_Exception("No MAC address found on %s !" % ifname) - - return (ARPHDR_ETHER, ''.join(mac)) - - # BPF specific functions + def get_dev_bpf(): + # type: () -> Tuple[int, int] """Returns an opened BPF file object""" # Get the first available BPF handle @@ -148,62 +56,55 @@ def get_dev_bpf(): return (fd, bpf) 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) + 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 !") -# Interface manipulation functions - -def _get_ifindex_list(): +def in6_getifaddr(): + # type: () -> List[Tuple[str, int, str]] """ - Returns a list containing (iface, index) - """ - ptr = LIBC.if_nameindex() - ifaces = [] - for i in range(255): - iface = ptr.contents[i] - if not iface.if_name: - break - ifaces.append((plain_str(iface.if_name), iface.if_index)) - LIBC.if_freenameindex(ptr) - return ifaces - - -_IFNUM = re.compile(r"([0-9]*)([ab]?)$") + 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. + """ + 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 + ] -def _get_if_flags(ifname): - """Internal function to get interface flags""" - # Get interface flags - try: - result = get_if(ifname, SIOCGIFFLAGS) - except IOError: - warning("ioctl(SIOCGIFFLAGS) failed on %s !", ifname) - return None - # Convert flags - ifflags = struct.unpack("16xH14x", result)[0] - return ifflags +# Interface provider class BPFInterfaceProvider(InterfaceProvider): name = "BPF" def _is_valid(self, dev): + # type: (NetworkInterface) -> bool if not dev.flags & 0x1: # not IFF_UP return False # Get a BPF handle @@ -215,8 +116,7 @@ def _is_valid(self, dev): 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())) + fcntl.ioctl(fd, BIOCSETIF, struct.pack("16s16x", dev.network_name.encode())) except IOError: return False else: @@ -226,29 +126,19 @@ def _is_valid(self, dev): os.close(fd) def load(self): - from scapy.fields import FlagValue + # type: () -> Dict[str, NetworkInterface] data = {} - ips = in6_getifaddr() - for ifname, index in _get_ifindex_list(): - try: - ifflags = _get_if_flags(ifname) - mac = scapy.utils.str2mac(get_if_raw_hwaddr(ifname)[1]) - ip = inet_ntop(socket.AF_INET, get_if_raw_addr(ifname)) - except Scapy_Exception: - continue - ifflags = FlagValue(ifflags, _iff_flags) - if_data = { - "name": ifname, - "network_name": ifname, - "description": ifname, - "flags": ifflags, - "index": index, - "ip": ip, - "ips": [x[0] for x in ips if x[2] == ifname] + [ip], - "mac": mac - } - data[ifname] = NetworkInterface(self, if_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 -IFACES.register_provider(BPFInterfaceProvider) +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 3610cba2204..0b76644444b 100644 --- a/scapy/arch/bpf/supersocket.py +++ b/scapy/arch/bpf/supersocket.py @@ -8,6 +8,8 @@ """ from select import select + +import abc import ctypes import errno import fcntl @@ -36,10 +38,22 @@ 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 +from scapy.interfaces import network_name, _GlobInterfaceType from scapy.supersocket import SuperSocket from scapy.compat import raw +# 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: @@ -61,7 +75,7 @@ class bpf_timeval(ctypes.Structure): _fields_ = [("tv_sec", ctypes.c_ulong), ("tv_usec", ctypes.c_ulong)] else: - class bpf_timeval(ctypes.Structure): + class bpf_timeval(ctypes.Structure): # type: ignore _fields_ = [("tv_sec", ctypes.c_uint32), ("tv_usec", ctypes.c_uint32)] @@ -81,20 +95,28 @@ class bpf_hdr(ctypes.Structure): 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): + 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 - self.assigned_interface = None + self.fd_flags = None # type: Optional[int] + self.type = type + self.bpf_fd = -1 # SuperSocket mandatory variables if promisc is None: @@ -104,15 +126,13 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, self.iface = network_name(iface or conf.iface) # Get the BPF handle - self.ins = None - (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.ins, BIOCSTSTAMP, + self.bpf_fd, BIOCSTSTAMP, struct.pack('I', BPF_T_NANOTIME) ) except IOError: @@ -121,7 +141,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Set the BPF buffer length try: fcntl.ioctl( - self.ins, BIOCSBLEN, + self.bpf_fd, BIOCSBLEN, struct.pack('I', BPF_BUFFER_LENGTH) ) except IOError: @@ -131,16 +151,15 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # Assign the network interface to the BPF handle try: fcntl.ioctl( - self.ins, BIOCSETIF, + 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() @@ -160,7 +179,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, if macos_version < 101500: dlt_radiotap = struct.pack('I', DLT_IEEE802_11_RADIO) try: - fcntl.ioctl(self.ins, BIOCSDLT, dlt_radiotap) + fcntl.ioctl(self.bpf_fd, BIOCSDLT, dlt_radiotap) except IOError: raise Scapy_Exception("Can't set %s into monitor mode!" % self.iface) @@ -170,7 +189,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # 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) @@ -178,7 +197,7 @@ 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) @@ -193,10 +212,10 @@ 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) + attach_filter(self.bpf_fd, filter, self.iface) filter_attached = True - except ImportError as ex: - warning("Cannot set filter: %s" % ex) + 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 @@ -204,7 +223,7 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, # more than ensuring the length frame is not null. filter = "greater 0" try: - attach_filter(self.ins, filter, self.iface) + attach_filter(self.bpf_fd, filter, self.iface) except ImportError as ex: warning("Cannot set filter: %s" % ex) @@ -212,15 +231,17 @@ def __init__(self, iface=None, type=ETH_P_ALL, promisc=None, filter=None, 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 @@ -228,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 !", @@ -242,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 @@ -265,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__ @@ -316,6 +348,7 @@ 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. """ @@ -327,14 +360,17 @@ 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) @@ -343,12 +379,14 @@ 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): + # type: (bytes) -> None """ Extract all frames from the buffer and stored them in the received list """ @@ -381,6 +419,7 @@ def extract_frames(self, bpf_buffer): 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) @@ -391,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) @@ -406,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(): @@ -425,15 +466,35 @@ 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 @@ -443,12 +504,14 @@ def send(self, pkt): 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 # @@ -476,38 +539,51 @@ def send(self, pkt): # 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 = raw(pkt) + 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 = raw(self.guessed_cls() / pkt) + frame = fd.guessed_cls() / pkt pkt.sent_time = time.time() # Send the frame - L2bpfSocket.send(self, frame) + return L2bpfSocket.send(fd, frame) + @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) -# Sockets manipulation functions - -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 ce354096fea..8077c3dd286 100644 --- a/scapy/arch/common.py +++ b/scapy/arch/common.py @@ -8,15 +8,21 @@ """ import ctypes +import re +import socket + from scapy.config import conf -from scapy.data import MTU, ARPHDR_ETHER, ARPHRD_TO_DLT -from scapy.error import Scapy_Exception -from scapy.interfaces import network_name +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 scapy.compat import ( +from typing import ( + List, Optional, Union, ) @@ -32,7 +38,6 @@ "RUNNING", "NOARP", "PROMISC", - "NOTRAILERS", "ALLMULTI", "MASTER", "SLAVE", @@ -46,6 +51,15 @@ ] +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) + + # BPF HANDLERS @@ -85,16 +99,16 @@ def compile_filter(filter_exp, # type: str ) iface = conf.iface # Try to guess linktype to avoid requiring root - from scapy.arch import get_if_raw_hwaddr 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 not linktype and conf.use_bpf: - linktype = ARPHDR_ETHER 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, 1, -1 ) @@ -104,7 +118,7 @@ def compile_filter(filter_exp, # type: str pcap = pcap_open_live( 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( @@ -116,3 +130,18 @@ def compile_filter(filter_exp, # type: str "Failed to compile filter expression %s (%s)" % (filter_exp, ret) ) return bpf + + +####### +# 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 index ba8dcdcb52e..80816a9083e 100644 --- a/scapy/arch/libpcap.py +++ b/scapy/arch/libpcap.py @@ -16,8 +16,13 @@ from scapy.automaton import select_objects 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.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, @@ -33,18 +38,19 @@ from scapy.packet import Packet from scapy.pton_ntop import inet_ntop from scapy.supersocket import SuperSocket -from scapy.utils import str2mac +from scapy.utils import str2mac, decode_locale_str import scapy.consts -from scapy.compat import ( - cast, +from typing import ( + Any, Dict, List, NoReturn, Optional, Tuple, Type, + cast, ) if not scapy.consts.WINDOWS: @@ -77,20 +83,27 @@ class _L2libpcapSocket(SuperSocket): - __slots__ = ["pcap_fd"] + __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.cls = conf.l2types[ll] + self.LL = conf.l2types[ll] + if ll in [ + DLT_RAW, + DLT_RAW_ALT, + ]: + self.lvl = 3 + else: + self.lvl = 2 else: - self.cls = conf.default_l2 + self.LL = conf.default_l2 warning( "Unable to guess datalink type " "(interface=%s linktype=%i). Using %s", - self.iface, ll, self.cls.name + self.iface, ll, self.LL.name ) def recv_raw(self, x=MTU): @@ -102,7 +115,7 @@ def recv_raw(self, x=MTU): ts, pkt = self.pcap_fd.next() if pkt is None: return None, None, None - return self.cls, pkt, ts + return self.LL, pkt, ts def nonblock_recv(self, x=MTU): # type: (int) -> Optional[Packet] @@ -126,13 +139,17 @@ def close(self): if self.closed: return self.closed = True - self.pcap_fd.close() + 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: @@ -148,12 +165,16 @@ def close(self): 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, @@ -196,6 +217,7 @@ def load_winpcapy(): flags = p.contents.flags # FLAGS ips = [] mac = "" + itype = -1 a = p.contents.addresses while a: # IPv4 address @@ -223,7 +245,7 @@ def load_winpcapy(): ips.append(addr) a = a.contents.next flags = FlagValue(flags, _pcap_if_flags) - if_list[name] = (description, ips, flags, mac) + if_list[name] = (description, ips, flags, mac, itype) p = p.contents.next conf.cache_pcapiflist = if_list except Exception: @@ -259,8 +281,6 @@ def load_winpcapy(): conf.use_npcap = True conf.loopback_name = conf.loopback_name = "Npcap Loopback Adapter" # noqa: E501 -if WINDOWS: - NPCAP_PATH = "" if conf.use_pcap: class _PcapWrapper_libpcap: # noqa: F811 """Wrapper for the libpcap calls""" @@ -278,30 +298,86 @@ def __init__(self, network_name(device).encode("utf8") ) self.dtl = -1 - 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 + if not WINDOWS or conf.use_npcap: + from scapy.libs.winpcapy import pcap_create 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: - log_runtime.error("Could not set monitor mode") - if pcap_activate(self.pcap) != 0: - raise OSError("Could not activate the pcap handler") + 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 = bytes(bytearray(self.errbuf)).strip(b"\x00") + 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) @@ -322,13 +398,16 @@ def next(self): 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] # type: ignore + self.pkt_data[:self.header.contents.len] )) return ts, pkt __next__ = next @@ -343,22 +422,25 @@ def datalink(self): 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) -> bool + # type: (str) -> None filter_exp = create_string_buffer(f.encode("utf8")) - if pcap_compile(self.pcap, byref(self.bpf_program), filter_exp, 1, -1) == -1: # noqa: E501 - log_runtime.error("Could not compile filter expression %s", f) - return False - else: - if pcap_setfilter(self.pcap, byref(self.bpf_program)) == -1: - log_runtime.error("Could not set filter %s", f) - return False - return True + 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 @@ -389,21 +471,23 @@ def load(self): data = {} i = 0 for ifname, dat in conf.cache_pcapiflist.items(): - description, ips, flags, mac = dat + description, ips, flags, mac, itype = dat i += 1 - if not mac: - from scapy.arch import get_if_hwaddr + if LINUX or BSD or SOLARIS and not mac: + from scapy.arch.unix import get_if_raw_hwaddr try: - mac = get_if_hwaddr(ifname) + itype, _mac = get_if_raw_hwaddr(ifname) + mac = str2mac(_mac) except Exception: # There are at least 3 different possible exceptions - continue + mac = "00:00:00:00:00:00" if_data = { 'name': ifname, 'description': description or ifname, 'network_name': ifname, 'index': i, - 'mac': mac or '00:00:00:00:00:00', + 'mac': mac, + 'type': itype, 'ips': ips, 'flags': flags } @@ -442,9 +526,13 @@ def __init__(self, self.promisc = promisc else: self.promisc = conf.sniff_promisc + self.monitor = monitor fd = open_pcap( - iface, MTU, self.promisc, 100, - monitor=monitor + device=iface, + snaplen=MTU, + promisc=self.promisc, + to_ms=100, + monitor=self.monitor, ) super(L2pcapListenSocket, self).__init__(fd) try: @@ -466,7 +554,7 @@ def __init__(self, self.pcap_fd.setfilter(filter) def send(self, x): - # type: (int) -> NoReturn + # type: (Packet) -> NoReturn raise Scapy_Exception( "Can't send anything with L2pcapListenSocket" ) @@ -486,12 +574,19 @@ def __init__(self, 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 - fd = open_pcap(iface, MTU, self.promisc, 100, - monitor=monitor) + 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: @@ -520,6 +615,7 @@ def __init__(self, filter = "(ether proto %i) and (%s)" % (type, filter) else: filter = "ether proto %i" % type + self.filter = filter if filter: self.pcap_fd.setfilter(filter) @@ -535,21 +631,68 @@ def send(self, x): class L3pcapSocket(L2pcapSocket): desc = "read/write packets at layer 3 using only libpcap" - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - r = L2pcapSocket.recv(self, x) - if r: + 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 - # Makes send detects when it should add - # Loopback(), Dot11... instead of Ether() - sx = raw(self.cls() / x) + # 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 self.pcap_fd.send(sx) + 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 9087156c09f..00000000000 --- a/scapy/arch/linux.py +++ /dev/null @@ -1,700 +0,0 @@ -# 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 __future__ import absolute_import - - -from fcntl import ioctl -from select import select - -import array -import ctypes -import os -import socket -import struct -import subprocess -import sys -import time - -import scapy.utils -import scapy.utils6 -from scapy.compat import raw, plain_str -from scapy.consts import LINUX -from scapy.arch.common import ( - _iff_flags, - compile_filter, -) -from scapy.arch.unix import get_if, get_if_raw_hwaddr -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_loading, - log_runtime, - warning, -) -from scapy.interfaces import IFACES, InterfaceProvider, NetworkInterface, \ - network_name -from scapy.libs.structures import sock_fprog -from scapy.packet import Packet, Padding -from scapy.pton_ntop import inet_ntop -from scapy.supersocket import SuperSocket - -import scapy.libs.six as six - -# Typing imports -from scapy.compat import ( - Any, - Callable, - 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 get_if_raw_addr(iff): - # type: (Union[NetworkInterface, str]) -> bytes - r""" - Return the raw IPv4 address of an interface. - If unavailable, returns b"\0\0\0\0" - """ - try: - return get_if(iff, SIOCGIFADDR)[20:24] - except IOError: - return b"\0\0\0\0" - - -def _get_if_list(): - # type: () -> List[str] - """ - Function to read the interfaces from /proc/net/dev - """ - try: - f = open("/proc/net/dev", "rb") - except IOError: - try: - f.close() - except Exception: - pass - log_loading.critical("Can't open /proc/net/dev !") - return [] - lst = [] - f.readline() - f.readline() - for line in f: - lst.append(plain_str(line).split(":")[0].strip()) - f.close() - return lst - - -def attach_filter(sock, bpf_filter, iface): - # type: (socket.socket, str, Union[NetworkInterface, str]) -> 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 - bp = struct.pack( - 'HL', - bp.bf_len, ctypes.addressof(bp.bf_insns.contents) - ) - else: - bp = sock_fprog(bp.bf_len, bp.bf_insns) - sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_FILTER, bp) - - -def set_promisc(s, iff, val=1): - # type: (socket.socket, Union[NetworkInterface, str], int) -> None - 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, # type: str - ip_mask, # type: int - gw_str, # type: str - metric # type: int - ): - # type: (...) -> Optional[Tuple[int, int, str, str, str, int]] - """ - 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_ar = array.array('B', b'\0' * 4096) - ifreq = ioctl(sck.fileno(), SIOCGIFCONF, - struct.pack("iL", len(names_ar), names_ar.buffer_info()[0])) - - # Extract interfaces names - out = struct.unpack("iL", ifreq)[0] - names_b = names_ar.tobytes() if six.PY3 else names_ar.tostring() # type: ignore # noqa: E501 - names = [names_b[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_b in names: - ifname = plain_str(ifname_b) - # Only look for a matching interface name - if not ifname.startswith(iface_name): - continue - - # Retrieve and convert addresses - ifreq = ioctl(sck, SIOCGIFADDR, struct.pack("16s16x", ifname_b)) - ifaddr = struct.unpack(">I", ifreq[20:24])[0] # type: int - ifreq = ioctl(sck, SIOCGIFNETMASK, struct.pack("16s16x", ifname_b)) - msk = struct.unpack(">I", ifreq[20:24])[0] # type: int - - # Get the full interface name - 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 None - - -def read_routes(): - # type: () -> List[Tuple[int, int, str, str, str, int]] - try: - f = open("/proc/net/route", "rb") - except IOError: - log_loading.critical("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", conf.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", conf.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", conf.loopback_name, ifaddr, 1)) # noqa: E501 - else: - warning("Interface %s: unknown address family (%i)" % (conf.loopback_name, addrfamily)) # noqa: E501 - except IOError as err: - if err.errno == 99: - warning("Interface %s: no address assigned" % conf.loopback_name) # noqa: E501 - else: - warning("Interface %s: failed to get address config (%s)" % (conf.loopback_name, str(err))) # noqa: E501 - - for line_b in f.readlines()[1:]: - line = plain_str(line_b) - iff, dst_b, gw, flags_b, _, _, metric_b, msk_b, _, _, _ = line.split() - flags = int(flags_b, 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_b, 16)) & 0xffffffff - msk_int = socket.htonl(int(msk_b, 16)) & 0xffffffff - gw_str = scapy.utils.inet_ntoa(struct.pack("I", int(gw, 16))) - metric = int(metric_b) - - route = (dst_int, msk_int, gw_str, iff, ifaddr, metric) - if ifaddr_int & msk_int != dst_int: - tmp_route = get_alias_address(iff, dst_int, gw_str, metric) - if tmp_route: - route = tmp_route - routes.append(route) - - f.close() - s.close() - return routes - -############ -# IPv6 # -############ - - -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. - """ - ret = [] # type: List[Tuple[str, int, str]] - 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(): - # type: () -> List[Tuple[str, int, str, str, List[str], int]] - 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): - # type: (bytes) -> str - ret = struct.unpack('4s4s4s4s4s4s4s4s', p) - addr = b':'.join(ret).decode() - return scapy.utils6.in6_ptop(addr) - - lifaddr = in6_getifaddr() - for line in f.readlines(): - d_b, dp_b, _, _, nh_b, metric_b, rc, us, fl_b, dev_b = line.split() - metric = int(metric_b, 16) - fl = int(fl_b, 16) - dev = plain_str(dev_b) - - if fl & RTF_UP == 0: - continue - if fl & RTF_REJECT: - continue - - d = proc2r(d_b) - dp = int(dp_b, 16) - nh = proc2r(nh_b) - - cset = [] # candidate set (possible source addresses) - if dev == conf.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): - # type: (Union[NetworkInterface, str]) -> int - return int(struct.unpack("I", get_if(iff, SIOCGIFINDEX)[16:20])[0]) - - -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] - from scapy.fields import FlagValue - data = {} - ips = in6_getifaddr() - for i in _get_if_list(): - try: - ifflags = struct.unpack("16xH14x", get_if(i, SIOCGIFFLAGS))[0] - index = get_if_index(i) - mac = scapy.utils.str2mac( - get_if_raw_hwaddr(i, siocgifhwaddr=SIOCGIFHWADDR)[1] - ) - ip = None # type: Optional[str] - ip = inet_ntop(socket.AF_INET, get_if_raw_addr(i)) - except IOError: - warning("Interface %s does not exist!", i) - continue - if ip == "0.0.0.0": - ip = None - ifflags = FlagValue(ifflags, _iff_flags) - if_data = { - "name": i, - "network_name": i, - "description": i, - "flags": ifflags, - "index": index, - "ip": ip, - "ips": [x[0] for x in ips if x[2] == i] + [ip] if ip else [], - "mac": mac - } - data[i] = NetworkInterface(self, if_data) - return data - - -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: - log_runtime.error("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 - ) - 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 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): - 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 recv(self, x=MTU): - # type: (int) -> Optional[Packet] - 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): - # type: (Packet) -> int - iff = x.route()[0] - if iff is None: - iff = network_name(conf.iface) - sdto = (iff, self.type) - self.outs.bind(sdto) - sn = self.outs.getsockname() - ll = lambda x: x # type: Callable[[Packet], Packet] - type_x = type(x) - if type_x in conf.l3types: - sdto = (iff, conf.l3types.layer2num[type_x]) - if sn[3] in conf.l2types: - ll = lambda x: conf.l2types.num2layer[sn[3]]() / x - if self.lvl == 3 and type_x != self.LL: - warning("Incompatible L3 types detected using %s instead of %s !", - type_x, self.LL) - self.LL = type_x - sx = raw(ll(x)) - x.sent_time = time.time() - try: - return self.outs.sendto(sx, sdto) - except socket.error as msg: - if msg.errno == 22 and len(sx) < conf.min_pkt_size: - return self.outs.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 += self.outs.sendto(raw(ll(p)), sdto) - return i - else: - raise - - -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/__init__.py b/scapy/arch/linux/__init__.py new file mode 100644 index 00000000000..f4e380487ba --- /dev/null +++ b/scapy/arch/linux/__init__.py @@ -0,0 +1,479 @@ +# 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 +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 + bp = struct.pack( # type: ignore + 'HL', + bp.bf_len, ctypes.addressof(bp.bf_insns.contents) + ) + else: + bp = sock_fprog(bp.bf_len, bp.bf_insns) # type: ignore + sock.setsockopt(socket.SOL_SOCKET, SO_ATTACH_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..d5d267df10a --- /dev/null +++ b/scapy/arch/linux/rtnetlink.py @@ -0,0 +1,983 @@ +# 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 + for attr in msg.data: + if attr.rta_type == 0x01: # IFA_ADDRESS + address = attr.rta_data + break + if address is not None: + data = { + "af_family": family, + "index": ifindex, + "address": address, + } + if family == 10: # ipv6 + data["scope"] = scapy.utils6.in6_getscope(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/solaris.py b/scapy/arch/solaris.py index dd6b22cfcd5..cb30bf04e89 100644 --- a/scapy/arch/solaris.py +++ b/scapy/arch/solaris.py @@ -18,6 +18,7 @@ # From sys/sockio.h and net/if.h SIOCGIFHWADDR = 0xc02069b9 # Get hardware address +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 diff --git a/scapy/arch/unix.py b/scapy/arch/unix.py index acd52c8f562..672a98a6da7 100644 --- a/scapy/arch/unix.py +++ b/scapy/arch/unix.py @@ -17,13 +17,12 @@ from scapy.config import conf from scapy.consts import FREEBSD, NETBSD, OPENBSD, SOLARIS from scapy.error import log_runtime, warning -from scapy.interfaces import network_name, NetworkInterface 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 scapy.compat import ( +from typing import ( List, Optional, Tuple, @@ -33,10 +32,9 @@ def get_if(iff, cmd): - # type: (Union[NetworkInterface, str], int) -> bytes + # type: (str, int) -> bytes """Ease SIOCGIF* ioctl calls""" - iff = network_name(iff) sck = socket.socket() try: return ioctl(sck, cmd, struct.pack("16s16x", iff.encode("utf8"))) @@ -44,7 +42,7 @@ def get_if(iff, cmd): sck.close() -def get_if_raw_hwaddr(iff, # type: Union[NetworkInterface, str] +def get_if_raw_hwaddr(iff, # type: str siocgifhwaddr=None, # type: Optional[int] ): # type: (...) -> Tuple[int, bytes] diff --git a/scapy/arch/windows/__init__.py b/scapy/arch/windows/__init__.py index df8af008d31..81b7bed57fd 100755 --- a/scapy/arch/windows/__init__.py +++ b/scapy/arch/windows/__init__.py @@ -13,12 +13,17 @@ import socket import struct import subprocess as sp - import warnings -from scapy.arch.windows.structures import _windows_title, \ - GetAdaptersAddresses, GetIpForwardTable, GetIpForwardTable2, \ - get_service_status +import winreg + +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, ProgPath from scapy.error import ( @@ -30,27 +35,29 @@ ) from scapy.interfaces import NetworkInterface, InterfaceProvider, \ dev_from_index, resolve_iface, network_name -from scapy.pton_ntop import inet_ntop, inet_pton -from scapy.utils import atol, itom, mac2str, str2mac +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.libs.six as six -from scapy.libs.six.moves import input, winreg 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 scapy.compat import ( - cast, - overload, +from typing import ( Any, Dict, + Iterator, List, - Literal, Optional, Tuple, + Type, Union, + cast, + overload, ) +from scapy.compat import Literal conf.use_pcap = True @@ -63,6 +70,7 @@ # 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: @@ -76,7 +84,7 @@ # 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 # type: ignore if not hasattr(socket, 'IPV6_HDRINCL'): @@ -87,7 +95,7 @@ if not hasattr(socket, 'SOL_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'): @@ -186,34 +194,17 @@ 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): @@ -264,33 +255,33 @@ def _get_mac(x): 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): - # 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 - ips = [] ips.extend(_resolve_ips(unicast)) if extended: @@ -298,20 +289,18 @@ 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"]), + "name": plain_str(x["friendly_name"]), "index": x["interface_index"], - "description": _str_decode(x["description"]), - "guid": _str_decode(x["adapter_name"]), + "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() ] @@ -334,6 +323,7 @@ def __init__(self, provider, data=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) @@ -349,10 +339,11 @@ def update(self, data): self.guid = data['guid'] self.ipv4_metric = data['ipv4_metric'] self.ipv6_metric = data['ipv6_metric'] + self.nameservers = data['nameservers'] try: # Npcap loopback interface - if conf.use_npcap and self.network_name == NPCAP_LOOPBACK_NAME: + 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" @@ -481,15 +472,15 @@ def setchannel(self, channel): self._check_npcap_requirement() return self._npcap_set("channel", str(channel)) - def frequence(self): + def frequency(self): # type: () -> int - """Get the frequence of the interface. + """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.""" @@ -612,21 +603,44 @@ def load(self, NetworkInterface_Win=NetworkInterface_Win): # Try a restart WindowsInterfacesProvider._pcap_check() + legacy_npcap_guid = None windows_interfaces = dict() for i in get_windows_if_list(): - # Detect Loopback interface - if "Loopback" in i['name']: - i['name'] = conf.loopback_name + # Only consider interfaces with a GUID if i['guid']: - if conf.use_npcap and i['name'] == conf.loopback_name: - i['guid'] = NPCAP_LOOPBACK_NAME + 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 + index = 0 - for netw, if_data in six.iteritems(conf.cache_pcapiflist): - name, ips, flags, _ = if_data - guid = _pcapname_to_guid(netw) - data = windows_interfaces.get(guid, None) + for netw, name, ips, flags, guid, data in iterinterfaces(): if data: # Exists in Windows registry data['network_name'] = netw @@ -645,10 +659,11 @@ def load(self, NetworkInterface_Win=NetworkInterface_Win): 'ipv4_metric': 0, 'ipv6_metric': 0, 'ips': ips, - 'flags': flags + 'flags': flags, + 'nameservers': [], } # No KeyError will happen here, as we get it from cache - results[guid] = NetworkInterface_Win(self, data) + results[netw] = NetworkInterface_Win(self, data) return results def reload(self): @@ -661,6 +676,14 @@ def reload(self): load_winpcapy() return self.load() + 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 conf.L3socket + # Register provider conf.ifaces.register_provider(WindowsInterfacesProvider) @@ -674,7 +697,7 @@ def get_ips(v6=False): :param v6: IPv6 addresses """ res = {} - for iface in six.itervalues(conf.ifaces): + for iface in conf.ifaces.values(): if v6: res[iface] = iface.ips[6] else: @@ -682,15 +705,6 @@ def get_ips(v6=False): return res -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 get_ip_from_name(ifname, v6=False): # type: (str, bool) -> str """Backward compatibility: indirectly calls get_ips @@ -743,7 +757,7 @@ def pcap_service_stop(askadmin=True): if conf.use_pcap: _orig_open_pcap = libpcap.open_pcap - def open_pcap(iface, # type: Union[str, NetworkInterface] + def open_pcap(device, # type: Union[str, NetworkInterface] *args, # type: Any **kargs # type: Any ): @@ -751,7 +765,7 @@ def open_pcap(iface, # type: Union[str, NetworkInterface] """open_pcap: Windows routine for creating a pcap from an interface. This function is also responsible for detecting monitor mode. """ - iface = cast(NetworkInterface_Win, resolve_iface(iface)) + iface = cast(NetworkInterface_Win, resolve_iface(device)) iface_network_name = iface.network_name if not iface: raise Scapy_Exception( @@ -771,12 +785,6 @@ def open_pcap(iface, # type: Union[str, NetworkInterface] libpcap.open_pcap = open_pcap # type: ignore -def get_if_raw_hwaddr(iface): - # type: (Union[NetworkInterface, str]) -> Tuple[int, bytes] - _iface = resolve_iface(iface) - return ARPHDR_ETHER, _iface.mac and mac2str(_iface.mac) or b"\x00" * 6 - - def _read_routes_c_v1(): # type: () -> List[Tuple[int, int, str, str, str, int]] """Retrieve Windows routes through a GetIpForwardTable call. @@ -970,11 +978,11 @@ def _route_add_loopback(routes=None, # type: Optional[List[Any]] if iface == conf.loopback_name: conf.route.routes.remove(route) # Remove conf.loopback_name interface - for devname, iface in list(conf.ifaces.items()): - if iface == conf.loopback_name: + for devname, ifname in list(conf.ifaces.items()): + if ifname == conf.loopback_name: conf.ifaces.pop(devname) # Inject interface - conf.ifaces["{0XX00000-X000-0X0X-X00X-00XXXX000XXX}"] = 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.network_name == conf.loopback_name: @@ -1018,6 +1026,21 @@ 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 567208986eb..b2b457da615 100644 --- a/scapy/arch/windows/native.py +++ b/scapy/arch/windows/native.py @@ -6,59 +6,33 @@ """ 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 select_objects -from scapy.arch.windows.structures import GetIcmpStatistics 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 scapy.compat import ( +from typing import ( Any, List, Optional, @@ -70,14 +44,31 @@ 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 __selectable_force_select__ = True # see automaton.py - __slots__ = ["promisc", "cls", "ipv6", "proto"] + __slots__ = ["promisc", "cls", "ipv6"] def __init__(self, iface=None, # type: Optional[_GlobInterfaceType] - proto=socket.IPPROTO_IP, # type: int ttl=128, # type: int ipv6=False, # type: bool promisc=True, # type: bool @@ -87,58 +78,45 @@ def __init__(self, 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 - host = self.iface.ip if self.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) @@ -147,6 +125,8 @@ def __init__(self, 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) @@ -160,6 +140,16 @@ def __init__(self, ) 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) @@ -170,6 +160,12 @@ def send(self, 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) @@ -197,24 +193,36 @@ def recv_raw(self, x=MTU): data, address = self.ins.recvfrom(x) except io.BlockingIOError: return None, None, None # type: ignore - from scapy.layers.inet import IP - from scapy.layers.inet6 import IPv6 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 close(self): # type: () -> None - if not self.closed and self.promisc: + if not self.closed and self.promisc and hasattr(self, 'ins'): self.ins.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) super(L3WinSocket, self).close() @@ -229,23 +237,7 @@ class L3WinSocket6(L3WinSocket): def __init__(self, **kwargs): # type: (**Any) -> None - super(L3WinSocket6, self).__init__(ipv6=True, **kwargs) - - -def open_icmp_firewall(host): - # type: (str) -> int - """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(): - # type: () -> int - """See help(scapy.arch.windows.native) for more information. - Returns the current ICMP seq number.""" - return GetIcmpStatistics()['stats']['icmpOutStats']['dwEchos'] + super(L3WinSocket6, self).__init__( + ipv6=True, + **kwargs, + ) diff --git a/scapy/arch/windows/structures.py b/scapy/arch/windows/structures.py index c0d2e33e493..e84b407cf23 100644 --- a/scapy/arch/windows/structures.py +++ b/scapy/arch/windows/structures.py @@ -19,16 +19,20 @@ 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 scapy.compat import ( - AddressFamily, +from typing import ( Any, Dict, + IO, List, Optional, + Tuple, ) ANY_SIZE = 65500 # FIXME quite inefficient :/ @@ -41,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 @@ -200,52 +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(): - # type: () -> Dict[str, Dict[str, Dict[str, int]]] - """Return all Windows ICMP stats from iphlpapi""" - statistics = MIB_ICMP() - _GetIcmpStatistics(byref(statistics)) - results = _struct_to_dict(statistics) - del statistics - return results ############################## ##### Adapters Addresses ##### @@ -606,3 +565,61 @@ def GetIpForwardTable2(AF=AddressFamily.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 4f0ebb0b744..a9f9bb537f9 100644 --- a/scapy/as_resolvers.py +++ b/scapy/as_resolvers.py @@ -8,12 +8,11 @@ """ -from __future__ import absolute_import import socket from scapy.config import conf from scapy.compat import plain_str -from scapy.compat import ( +from typing import ( Any, Optional, Tuple, @@ -64,7 +63,10 @@ def _resolve_one(self, ip): 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 diff --git a/scapy/asn1/asn1.py b/scapy/asn1/asn1.py index 8054228f917..bd58715e26d 100644 --- a/scapy/asn1/asn1.py +++ b/scapy/asn1/asn1.py @@ -8,8 +8,6 @@ ASN.1 (Abstract Syntax Notation One) """ -from __future__ import absolute_import -from __future__ import print_function import random from datetime import datetime, timedelta, tzinfo @@ -18,9 +16,8 @@ from scapy.volatile import RandField, RandIP, GeneralizedTime from scapy.utils import Enum_metaclass, EnumElement, binrepr from scapy.compat import plain_str, bytes_encode, chb, orb -import scapy.libs.six as six -from scapy.compat import ( +from typing import ( Any, AnyStr, Dict, @@ -29,12 +26,13 @@ Optional, Tuple, Type, - TypeVar, Union, - _Generic_metaclass, cast, TYPE_CHECKING, ) +from typing import ( + TypeVar, +) if TYPE_CHECKING: from scapy.asn1.ber import BERcodec_Object @@ -79,9 +77,7 @@ def __init__(self, objlist=None): else: self.objlist = [ x._asn1_obj - for x in six.itervalues( - ASN1_Class_UNIVERSAL.__rdict__ # type: ignore - ) + for x in ASN1_Class_UNIVERSAL.__rdict__.values() # type: ignore if hasattr(x, "_asn1_obj") ] self.chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" # noqa: E501 @@ -92,14 +88,12 @@ def _fix(self, n=0): 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): z1 = int(random.expovariate(0.05) + 1) - return o("".join(random.choice(self.chars) for _ in range(z1))) + return o("".join(random.choice(self.chars) for _ in range(z1)).encode()) elif issubclass(o, ASN1_SEQUENCE) and (n < 10): z2 = int(random.expovariate(0.08) + 1) return o([self.__class__(objlist=self.objlist)._fix(n + 1) @@ -149,8 +143,7 @@ class ASN1_Codecs_metaclass(Enum_metaclass): element_class = ASN1Codec -@six.add_metaclass(ASN1_Codecs_metaclass) -class ASN1_Codecs: +class ASN1_Codecs(metaclass=ASN1_Codecs_metaclass): BER = cast(ASN1Codec, 1) DER = cast(ASN1Codec, 2) PER = cast(ASN1Codec, 3) @@ -215,12 +208,12 @@ def __new__(cls, ): # 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 @@ -231,15 +224,14 @@ def __new__(cls, ncls = cast('Type[ASN1_Class]', type.__new__(cls, name, bases, dct)) - for v in six.itervalues(ncls.__dict__): + for v in ncls.__dict__.values(): if isinstance(v, ASN1Tag): # overwrite ASN1Tag contexts, even cloned ones v.context = ncls return ncls -@six.add_metaclass(ASN1_Class_metaclass) -class ASN1_Class: +class ASN1_Class(metaclass=ASN1_Class_metaclass): pass @@ -281,11 +273,12 @@ class ASN1_Class_UNIVERSAL(ASN1_Class): 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(_Generic_metaclass): +class ASN1_Object_metaclass(type): def __new__(cls, name, # type: str bases, # type: Tuple[type, ...] @@ -306,8 +299,7 @@ def __new__(cls, _K = TypeVar('_K') -@six.add_metaclass(ASN1_Object_metaclass) -class ASN1_Object(Generic[_K]): +class ASN1_Object(Generic[_K], metaclass=ASN1_Object_metaclass): tag = ASN1_Class_UNIVERSAL.ANY def __init__(self, val): @@ -362,9 +354,16 @@ def __ne__(self, other): # type: (Any) -> bool return bool(self.val != other) - def command(self): - # type: () -> str - return "%s(%s)" % (self.__class__.__name__, repr(self.val)) + 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)) ####################### @@ -493,6 +492,17 @@ 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 @@ -510,7 +520,7 @@ def __repr__(self): ) -class ASN1_STRING(ASN1_Object[str]): +class ASN1_STRING(ASN1_Object[bytes]): tag = ASN1_Class_UNIVERSAL.STRING @@ -545,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 @@ -569,7 +579,7 @@ class ASN1_GENERAL_STRING(ASN1_STRING): tag = ASN1_Class_UNIVERSAL.GENERAL_STRING -class ASN1_GENERALIZED_TIME(ASN1_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: @@ -700,6 +710,22 @@ 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[List[Any]]): tag = ASN1_Class_UNIVERSAL.SEQUENCE @@ -716,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 @@ -724,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 5e2a8148ad1..23899274e4a 100644 --- a/scapy/asn1/ber.py +++ b/scapy/asn1/ber.py @@ -11,11 +11,11 @@ # Good read: https://luca.ntop.org/Teaching/Appunti/asn1.html -from __future__ import absolute_import 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 ( + ASN1Tag, ASN1_BADTAG, ASN1_BadTag_Decoding_Error, ASN1_Class, @@ -28,9 +28,8 @@ ASN1_Object, _ASN1_ERROR, ) -from scapy.libs import six -from scapy.compat import ( +from typing import ( Any, AnyStr, Dict, @@ -41,7 +40,6 @@ Type, TypeVar, Union, - _Generic_metaclass, cast, ) @@ -107,7 +105,10 @@ class BER_BadTag_Decoding_Error(BER_Decoding_Error, def BER_len_enc(ll, size=0): - # type: (int, int) -> bytes + # 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"" @@ -216,7 +217,7 @@ def BER_id_enc(n): def BER_tagging_dec(s, # type: bytes - hidden_tag=None, # type: Optional[Any] + hidden_tag=None, # type: Optional[int | ASN1Tag] implicit_tag=None, # type: Optional[int] explicit_tag=None, # type: Optional[int] safe=False, # type: Optional[bool] @@ -224,6 +225,7 @@ def BER_tagging_dec(s, # type: bytes ): # 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 = ( @@ -233,13 +235,13 @@ def BER_tagging_dec(s, # type: bytes if implicit_tag is not None: ber_id, s = BER_id_dec(s) if ber_id != implicit_tag: - if not safe and ber_id & 0x1f != implicit_tag & 0x1f: + 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: @@ -253,11 +255,11 @@ def BER_tagging_dec(s, # type: bytes return real_tag, s -def BER_tagging_enc(s, hidden_tag=None, implicit_tag=None, explicit_tag=None): - # type: (bytes, Optional[Any], Optional[int], Optional[int]) -> bytes +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((hash(hidden_tag) & ~(0x1f)) | implicit_tag) + s[1:] + s = BER_id_enc(implicit_tag) + s[1:] elif explicit_tag is not None: s = BER_id_enc(explicit_tag) + BER_len_enc(len(s)) + s return s @@ -265,7 +267,7 @@ def BER_tagging_enc(s, hidden_tag=None, implicit_tag=None, explicit_tag=None): # [ BER classes ] # -class BERcodec_metaclass(_Generic_metaclass): +class BERcodec_metaclass(type): def __new__(cls, name, # type: str bases, # type: Tuple[type, ...] @@ -284,8 +286,7 @@ def __new__(cls, _K = TypeVar('_K') -@six.add_metaclass(BERcodec_metaclass) -class BERcodec_Object(Generic[_K]): +class BERcodec_Object(Generic[_K], metaclass=BERcodec_metaclass): codec = ASN1_Codecs.BER tag = ASN1_Class_UNIVERSAL.ANY @@ -346,13 +347,13 @@ def do_dec(cls, _context = cls.tag.context cls.check_string(s) p, remainder = BER_id_dec(s) - if p not in _context: # type: ignore + 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) - tag = _context[p] # type: ignore + tag = _context[p] codec = cast('Type[BERcodec_Object[_K]]', tag.get_codec(ASN1_Codecs.BER)) if codec == BERcodec_Object: @@ -391,13 +392,13 @@ def safedec(cls, return cls.dec(s, context, safe=True) @classmethod - def enc(cls, s): - # type: (_K) -> bytes - 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: try: - return BERcodec_INTEGER.enc(int(s)) # type: ignore + return BERcodec_INTEGER.enc(int(s), size_len=size_len) # type: ignore except TypeError: raise TypeError("Trying to encode an invalid value !") @@ -413,8 +414,8 @@ class BERcodec_INTEGER(BERcodec_Object[int]): tag = ASN1_Class_UNIVERSAL.INTEGER @classmethod - def enc(cls, i): - # type: (int) -> bytes + def enc(cls, i, size_len=0): + # type: (int, Optional[int]) -> bytes ls = [] while True: ls.append(i & 0xff) @@ -425,9 +426,9 @@ def enc(cls, i): i >>= 8 if not i: break - s = [chb(hash(c)) for c in ls] - 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) @@ -484,8 +485,8 @@ def do_dec(cls, ) @classmethod - def enc(cls, _s): - # type: (AnyStr) -> bytes + 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) if len(s) % 8 == 0: @@ -496,18 +497,18 @@ 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[str]): tag = ASN1_Class_UNIVERSAL.STRING @classmethod - def enc(cls, _s): - # type: (str) -> bytes + 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, @@ -524,20 +525,20 @@ class BERcodec_NULL(BERcodec_INTEGER): tag = ASN1_Class_UNIVERSAL.NULL @classmethod - def enc(cls, i): - # type: (int) -> bytes + 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[bytes]): tag = ASN1_Class_UNIVERSAL.OID @classmethod - def enc(cls, _oid): - # type: (AnyStr) -> bytes + 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".")] @@ -547,7 +548,7 @@ def enc(cls, _oid): lst[1] += 40 * 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, @@ -626,13 +627,13 @@ class BERcodec_SEQUENCE(BERcodec_Object[Union[bytes, List[BERcodec_Object[Any]]] tag = ASN1_Class_UNIVERSAL.SEQUENCE @classmethod - def enc(cls, _ll): - # type: (Union[bytes, List[BERcodec_Object[Any]]]) -> bytes + 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(hash(cls.tag)) + BER_len_enc(len(ll)) + ll + return chb(int(cls.tag)) + BER_len_enc(len(ll), size=size_len) + ll @classmethod def do_dec(cls, @@ -673,13 +674,13 @@ class BERcodec_IPADDRESS(BERcodec_STRING): tag = ASN1_Class_UNIVERSAL.IPADDRESS @classmethod - def enc(cls, ipaddr_ascii): - # type: (str) -> bytes + 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): @@ -697,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 7a61411b9e1..029f8281225 100644 --- a/scapy/asn1/mib.py +++ b/scapy/asn1/mib.py @@ -8,16 +8,14 @@ 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.libs.six as six from scapy.compat import plain_str -from scapy.compat import ( +from typing import ( Any, Dict, List, @@ -48,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) @@ -69,9 +67,9 @@ def _oid(self, x): 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.d): + if p != 0 or xl[p] not in self.d.values(): return x - xl[p] = next(k for k, v in six.iteritems(self.d) 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): @@ -164,7 +162,7 @@ def load_mib(filenames): unresolved = {} # type: Dict[str, List[str]] alias = {} # type: Dict[str, str] # Export the current MIB to a working dictionary - for k in six.iterkeys(conf.mib): + for k in conf.mib: _mib_register(conf.mib[k], k.split("."), the_mib, unresolved, alias) # Read the files @@ -193,14 +191,14 @@ def load_mib(filenames): # Create the new MIB newmib = MIBDict(_name="MIB") # Add resolved values - for oid, key in six.iteritems(the_mib): + for oid, key in the_mib.items(): newmib[".".join(key)] = oid # Add unresolved values - for oid, key in six.iteritems(unresolved): + for oid, key in unresolved.items(): newmib[".".join(key)] = oid # Add aliases - for key, oid in six.iteritems(alias): - newmib[key] = oid + for key_s, oid in alias.items(): + newmib[key_s] = oid conf.mib = newmib @@ -212,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", @@ -231,12 +230,70 @@ 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", +} + +# pkcs7 # + +pkcs7_oids = { + "1.2.840.113549.1.7.2": "id-signedData", } # pkcs9 # pkcs9_oids = { + "1.2.840.113549.1.9": "pkcs9", "1.2.840.113549.1.9.0": "modules", "1.2.840.113549.1.9.1": "emailAddress", "1.2.840.113549.1.9.2": "unstructuredName", @@ -363,20 +420,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", @@ -388,8 +447,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", @@ -397,7 +456,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", @@ -432,7 +491,14 @@ 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] + "1.3.6.1.4.1.311.2.1.14": "CERT_EXTENSIONS", + "1.3.6.1.4.1.311.10.3.4": "szOID_EFS_CRYPTO", + "1.3.6.1.4.1.311.20.2": "ENROLL_CERTTYPE", + "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 = { @@ -497,6 +563,10 @@ def load_mib(filenames): "1.3.6.1.5.5.7.48.1.1": "basic-response" } +certTransp_oids = { + '1.3.6.1.4.1.11129.2.4.2': "SignedCertificateTimestampList", +} + # ansi-x962 # x962KeyType_oids = { @@ -514,6 +584,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 = { @@ -614,35 +690,59 @@ def load_mib(filenames): '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, + pkcs7_oids, pkcs9_oids, attributeType_oids, certificateExtension_oids, certExt_oids, + certPkixAd_oids, + certPkixKp_oids, certPkixPe_oids, certPkixQt_oids, - certPkixKp_oids, - certPkixAd_oids, certPolicy_oids, + certTransp_oids, evPolicy_oids, x962KeyType_oids, x962Signature_oids, + x942KeyType_oids, ansiX962Curve_oids, certicomCurve_oids, gssapi_oids, + kerberos_oids, ] x509_oids = {} diff --git a/scapy/asn1fields.py b/scapy/asn1fields.py index 2efcaf551f9..f2d8613af37 100644 --- a/scapy/asn1fields.py +++ b/scapy/asn1fields.py @@ -8,6 +8,8 @@ Classes that implement ASN.1 data structures. """ +import copy + from functools import reduce from scapy.asn1.asn1 import ( @@ -41,9 +43,8 @@ ) from scapy import packet -import scapy.libs.six as six -from scapy.compat import ( +from typing import ( Any, AnyStr, Callable, @@ -92,6 +93,7 @@ def __init__(self, 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: @@ -103,14 +105,20 @@ def __init__(self, self.default = default # type: ignore else: 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 = 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 @@ -164,8 +172,8 @@ 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, hidden_tag=self.ASN1_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) @@ -182,7 +190,7 @@ def extract_packet(self, try: c = cls(s, _underlayer=_underlayer) except ASN1F_badsequence: - c = packet.Raw(s, _underlayer=_underlayer) + c = packet.Raw(s, _underlayer=_underlayer) # type: ignore cpad = c.getlayer(packet.Raw) s = b"" if cpad is not None: @@ -230,8 +238,12 @@ def __str__(self): return repr(self) def randval(self): - # type: () -> RandField[Any] - return RandInt() + # type: () -> RandField[_I] + return cast(RandField[_I], RandInt()) + + def copy(self): + # type: () -> ASN1F_field[_I, _A] + return copy.copy(self) ############################ @@ -275,7 +287,7 @@ def __init__(self, keys = range(len(enum)) else: keys = list(enum) - if any(isinstance(x, six.string_types) for x in keys): + if any(isinstance(x, str) for x in keys): i2s, s2i = s2i, i2s # type: ignore for k in keys: i2s[k] = enum[k] @@ -431,14 +443,8 @@ 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)) super(ASN1F_SEQUENCE, self).__init__( - name, default, context=self.context, - implicit_tag=self.implicit_tag, - explicit_tag=self.explicit_tag, - flexible_tag=self.flexible_tag + name, default, **kwargs ) self.seq = seq self.islist = len(seq) > 1 @@ -453,7 +459,7 @@ def is_empty(self, pkt): def get_fields_list(self): # type: () -> List[ASN1F_field[Any, Any]] - return reduce(lambda x, y: x + y.get_fields_list(), # type: ignore + return reduce(lambda x, y: x + y.get_fields_list(), self.seq, []) def m2i(self, pkt, s): @@ -498,7 +504,7 @@ def dissect(self, pkt, s): def build(self, pkt): # type: (ASN1_Packet) -> bytes - s = reduce(lambda x, y: x + y.build(pkt), # type: ignore + s = reduce(lambda x, y: x + y.build(pkt), self.seq, b"") return super(ASN1F_SEQUENCE, self).i2m(pkt, s) @@ -507,7 +513,12 @@ class ASN1F_SET(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_UNIVERSAL.SET -_SEQ_T = Union['ASN1_Packet', Type[ASN1F_field], 'ASN1F_PACKET'] +_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], @@ -527,10 +538,13 @@ def __init__(self, explicit_tag=None, # type: Optional[Any] ): # type: (...) -> None - if isinstance(cls, type) and issubclass(cls, ASN1F_field): - self.fld = cast(Type[ASN1F_field[Any, Any]], cls) - self._extract_packet = lambda s, pkt: self.fld( - self.name, b"").m2i(pkt, s) + 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) @@ -588,12 +602,23 @@ def build(self, pkt): s = b"".join(raw(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): # type: () -> Any if self.holds_packets: return packet.fuzz(self.cls()) else: - return self.fld(self.name, b"").randval() + return self.fld.randval() def __repr__(self): # type: () -> str @@ -657,7 +682,7 @@ def i2repr(self, pkt, x): return self._field.i2repr(pkt, x) -_CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field], 'ASN1F_PACKET'] +_CHOICE_T = Union['ASN1_Packet', Type[ASN1F_field[Any, Any]], 'ASN1F_PACKET'] class ASN1F_CHOICE(ASN1F_field[_CHOICE_T, ASN1_Object[Any]]): @@ -691,21 +716,18 @@ def __init__(self, name, default, *args, **kwargs): # should be ASN1_Packet if hasattr(p.ASN1_root, "choices"): root = cast(ASN1F_CHOICE, p.ASN1_root) - for k, v in six.iteritems(root.choices): + 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"): - p = cast(Union[ASN1F_PACKET, Type[ASN1F_field[Any, Any]]], p) 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 - if p.implicit_tag is not None: - self.choices[p.implicit_tag & 0x1f] = 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") @@ -724,9 +746,7 @@ def m2i(self, pkt, s): if tag in self.choices: choice = self.choices[tag] else: - if tag & 0x1f in self.choices: # Try resolve only the tag number - choice = self.choices[tag & 0x1f] - elif self.flexible_tag: + if self.flexible_tag: choice = ASN1F_field else: raise ASN1_Error( @@ -736,9 +756,8 @@ def m2i(self, pkt, s): ) ) if hasattr(choice, "ASN1_root"): - choice = cast('ASN1_Packet', choice) # we don't want to import ASN1_Packet in this module... - return self.extract_packet(choice, s, _underlayer=pkt) + return self.extract_packet(choice, s, _underlayer=pkt) # type: ignore elif isinstance(choice, type): return choice(self.name, b"").m2i(pkt, s) else: @@ -753,7 +772,7 @@ def i2m(self, pkt, x): s = raw(x) if hash(type(x)) in self.pktchoices: imp, exp = self.pktchoices[hash(type(x))] - s = BER_tagging_enc(s, hidden_tag=self.ASN1_tag, + s = BER_tagging_enc(s, implicit_tag=imp, explicit_tag=exp) return BER_tagging_enc(s, explicit_tag=self.explicit_tag) @@ -761,10 +780,10 @@ def i2m(self, pkt, x): def randval(self): # type: () -> RandChoice randchoices = [] - for p in six.itervalues(self.choices): + for p in self.choices.values(): if hasattr(p, "ASN1_root"): # should be ASN1_Packet class - randchoices.append(packet.fuzz(p())) + randchoices.append(packet.fuzz(p())) # type: ignore elif hasattr(p, "ASN1_tag"): if isinstance(p, type): # should be (basic) ASN1F_field class @@ -794,9 +813,9 @@ def __init__(self, name, None, context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag ) - if implicit_tag is None and explicit_tag is None: + 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 + self.network_tag = 16 | 0x20 # 16 + CONSTRUCTED self.default = default def m2i(self, pkt, s): @@ -838,11 +857,23 @@ def i2m(self, s = b"" else: s = raw(x) - return BER_tagging_enc(s, hidden_tag=self.ASN1_tag, + 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()) @@ -864,8 +895,10 @@ def __init__(self, ): # type: (...) -> None self.cls = cls - super(ASN1F_BIT_STRING_ENCAPS, self).__init__( - name, default and raw(default), context=context, + super(ASN1F_BIT_STRING_ENCAPS, self).__init__( # type: ignore + name, + default and raw(default), + context=context, implicit_tag=implicit_tag, explicit_tag=explicit_tag ) @@ -885,12 +918,13 @@ def m2i(self, pkt, s): # type: ignore return p, remain def i2m(self, pkt, x): # type: ignore - # type: (ASN1_Packet, Optional[ASN1_Packet]) -> bytes - s = b"" if x is None else raw(x) - return super(ASN1F_BIT_STRING_ENCAPS, self).i2m( - pkt, - ASN1_BIT_STRING(s, readable=True) - ) + # 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): @@ -912,6 +946,18 @@ def __init__(self, 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 @@ -924,3 +970,51 @@ def i2repr(self, pkt, x): 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 36943fb5f89..058aecc0edb 100644 --- a/scapy/asn1packet.py +++ b/scapy/asn1packet.py @@ -9,12 +9,10 @@ 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.libs.six as six -from scapy.compat import ( +from typing import ( Any, Dict, Tuple, @@ -36,11 +34,13 @@ def __new__(cls, # 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), + ) -@six.add_metaclass(ASN1Packet_metaclass) -class ASN1_Packet(Packet): +class ASN1_Packet(Packet, metaclass=ASN1Packet_metaclass): ASN1_root = cast('ASN1F_field[Any, Any]', None) ASN1_codec = None diff --git a/scapy/automaton.py b/scapy/automaton.py index 4dd647cec75..2aae9168725 100644 --- a/scapy/automaton.py +++ b/scapy/automaton.py @@ -2,7 +2,7 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Copyright (C) Gabriel Potter +# Copyright (C) Gabriel Potter """ Automata with states, transitions and actions. @@ -16,6 +16,7 @@ import logging import os import random +import socket import sys import threading import time @@ -26,19 +27,19 @@ from collections import deque from scapy.config import conf -from scapy.utils import do_graph -from scapy.error import log_runtime, warning -from scapy.plist import PacketList +from scapy.consts import WINDOWS from scapy.data import MTU -from scapy.supersocket import SuperSocket +from scapy.error import log_runtime, warning +from scapy.interfaces import _GlobInterfaceType from scapy.packet import Packet -from scapy.consts import WINDOWS -import scapy.libs.six as six +from scapy.plist import PacketList +from scapy.supersocket import SuperSocket, StreamSocket +from scapy.utils import do_graph -from scapy.compat import ( +# Typing imports +from typing import ( Any, Callable, - DecoratorCallable, Deque, Dict, Generic, @@ -51,9 +52,13 @@ Type, TypeVar, Union, - _Generic_metaclass, cast, ) +from scapy.compat import DecoratorCallable + + +# winsock.h +FD_READ = 0x00000001 def select_objects(inputs, remain): @@ -78,28 +83,45 @@ def select_objects(inputs, remain): [b] :param inputs: objects to process - :param remain: timeout. If 0, return []. + :param remain: timeout. If 0, poll. If None, block. """ if not WINDOWS: return select.select(inputs, [], [], remain)[0] - natives = [] + inputs = list(inputs) events = [] + created = [] results = set() - for i in list(inputs): + for i in inputs: if getattr(i, "__selectable_force_select__", False): - natives.append(i) + # 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) - if natives: - results = results.union(set(select.select(natives, [], [], remain)[0])) + 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].fileno()), + ctypes.c_void_p(events[0]), remainms ) else: @@ -109,29 +131,31 @@ def select_objects(inputs, remain): res = ctypes.windll.kernel32.WaitForMultipleObjects( len(events), (ctypes.c_void_p * len(events))( - *[x.fileno() for x in events] + *events ), False, remainms ) if res != 0xFFFFFFFF and res != 0x00000102: # Failed or Timeout - results.add(events[res]) + results.add(inputs[res]) if len(events) > 1: # Now poll the others, if any - for evt in events: + for i, evt in enumerate(events): res = ctypes.windll.kernel32.WaitForSingleObject( - ctypes.c_void_p(evt.fileno()), + ctypes.c_void_p(evt), 0 # poll: don't wait ) if res == 0: - results.add(evt) + 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") -@six.add_metaclass(_Generic_metaclass) class ObjectPipe(Generic[_T]): def __init__(self, name=None): # type: (Optional[str]) -> None @@ -140,7 +164,6 @@ def __init__(self, name=None): self.__rd, self.__wr = os.pipe() self.__queue = deque() # type: Deque[_T] if WINDOWS: - self._fd = None # type: Optional[int] self._wincreate() if WINDOWS: @@ -153,31 +176,27 @@ def _wincreate(self): def _winset(self): # type: () -> None - if ctypes.windll.kernel32.SetEvent( - ctypes.c_void_p(self._fd)) == 0: + 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: + if ctypes.windll.kernel32.ResetEvent(ctypes.c_void_p(self._fd)) == 0: warning(ctypes.FormatError(ctypes.GetLastError())) def _winclose(self): # type: () -> None - if self._fd and ctypes.windll.kernel32.CloseHandle( - ctypes.c_void_p(self._fd)) == 0: + if ctypes.windll.kernel32.CloseHandle(ctypes.c_void_p(self._fd)) == 0: warning(ctypes.FormatError(ctypes.GetLastError())) - self._fd = None def fileno(self): # type: () -> int if WINDOWS: - return self._fd if self._fd is not None else -1 + return self._fd return self.__rd def send(self, obj): - # type: (Union[_T]) -> int + # type: (_T) -> int self.__queue.append(obj) if WINDOWS: self._winset() @@ -196,9 +215,13 @@ def flush(self): # type: () -> None pass - def recv(self, n=0): - # type: (Optional[int]) -> Optional[_T] + def recv(self, n=0, options=socket.MsgFlag(0)): + # type: (Optional[int], socket.MsgFlag) -> Optional[_T] if self.closed: + raise EOFError + if options & socket.MSG_PEEK: + if self.__queue: + return self.__queue[0] return None os.read(self.__rd, 1) elt = self.__queue.popleft() @@ -222,9 +245,12 @@ def close(self): self.closed = True os.close(self.__rd) os.close(self.__wr) - self.__queue.clear() if WINDOWS: - self._winclose() + try: + self._winclose() + except ImportError: + # Python is shutting down + pass def __repr__(self): # type: () -> str @@ -240,7 +266,7 @@ def select(sockets, remain=conf.recv_poll_rate): # 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 @@ -260,9 +286,11 @@ def __init__(self, **args): def __repr__(self): # type: () -> str - return "" % " ".join("%s=%r" % (k, v) - for (k, v) in six.iteritems(self.__dict__) # noqa: E501 - if not k.startswith("_")) + return "" % " ".join( + "%s=%r" % (k, v) + for k, v in self.__dict__.items() + if not k.startswith("_") + ) class Timer(): @@ -364,11 +392,11 @@ def expired(self): return lst def until_next(self): - # type: () -> float + # type: () -> Optional[float] try: return min([t._remaining() for t in self.timers if t._running()]) except ValueError: - return 0 + return None # None means blocking def count(self): # type: () -> int @@ -444,6 +472,7 @@ class ATMT: CONDITION = "Condition" RECV = "Receive condition" TIMEOUT = "Timeout condition" + EOF = "EOF condition" IOEVENT = "I/O event" class NewStateRequested(Exception): @@ -577,7 +606,7 @@ def deco(f, state=state, timeout=Timer(timeout)): @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)): # 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 @@ -587,6 +616,17 @@ def deco(f, state=state, timeout=Timer(timeout, prio=prio, autoreload=True)): # 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 + class _ATMT_Command: RUN = "RUN" @@ -618,34 +658,36 @@ def __init__(self, self.ioevent = ioevent self.proto = proto # write, read - self.spa, self.spb = ObjectPipe[bytes]("spa"), \ - ObjectPipe[bytes]("spb") + 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 send(self, s): - # type: (bytes) -> int - if not isinstance(s, bytes): - s = bytes(s) + # type: (Any) -> int return self.spa.send(s) def fileno(self): # type: () -> int return self.spb.fileno() - def recv(self, n=MTU): - # type: (Optional[int]) -> Any + # 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 and r is not None: - r = self.proto(r) + 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 @@ -682,9 +724,10 @@ def __new__(cls, name, bases, dct): 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_states = [] # type: List[_StateWrapper] + cls.stop_state = None # type: Optional[_StateWrapper] cls.ionames = [] cls.iosupersockets = [] @@ -693,11 +736,11 @@ 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) + decorated = [v for v in members.values() if hasattr(v, "atmt_type")] for m in decorated: @@ -711,8 +754,10 @@ def __new__(cls, name, bases, dct): if m.atmt_initial: cls.initial_states.append(m) if m.atmt_stop: - cls.stop_states.append(m) - elif m.atmt_type in [ATMT.CONDITION, ATMT.RECV, ATMT.TIMEOUT, ATMT.IOEVENT]: # noqa: E501 + 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: @@ -720,6 +765,8 @@ 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) @@ -731,11 +778,13 @@ def __new__(cls, name, bases, dct): for co in m.atmt_cond: cls.actions[co].append(m) - for v in itertools.chain(six.itervalues(cls.conditions), - six.itervalues(cls.recv_conditions), - six.itervalues(cls.ioevents)): + 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: @@ -759,7 +808,7 @@ def build_graph(self): 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: @@ -770,22 +819,48 @@ def build_graph(self): 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, timers in six.iteritems(self.timeout): + 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): @@ -804,27 +879,40 @@ def graph(self, **kargs): return do_graph(s, **kargs) -@six.add_metaclass(Automaton_metaclass) -class Automaton: +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_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", {}) - self.send_sock_class = kargs.pop("ll", conf.L3socket) - self.recv_sock_class = kargs.pop("recvsock", conf.L2listen) + 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 @@ -841,6 +929,7 @@ def __init__(self, *args, **kargs): 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): @@ -868,25 +957,144 @@ def __init__(self, *args, **kargs): self.start() - def parse_args(self, debug=0, store=1, **kargs): - # type: (int, int, Any) -> None + 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): - # type: (Packet) -> None - 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 six.iteritems(self.timeout): + for _, timers in self.timeout.items(): for timer in timers: # type: Timer if timer._func.atmt_condname == name: return timer @@ -899,35 +1107,33 @@ def __init__(self, wr # type: Union[int, ObjectPipe[bytes], None] ): # type: (...) -> None - if rd is not None and not isinstance(rd, (int, ObjectPipe)): - rd = rd.fileno() # type: ignore - if wr is not None and not isinstance(wr, (int, ObjectPipe)): - wr = wr.fileno() # type: ignore self.rd = rd self.wr = wr + if isinstance(self.rd, socket.socket): + self.__selectable_force_select__ = True def fileno(self): # type: () -> int - if isinstance(self.rd, ObjectPipe): - return self.rd.fileno() - elif isinstance(self.rd, int): + if isinstance(self.rd, int): return self.rd + elif self.rd: + return self.rd.fileno() return 0 def read(self, n=65535): # type: (int) -> Optional[bytes] - if isinstance(self.rd, ObjectPipe): - return self.rd.recv(n) - elif isinstance(self.rd, int): + if isinstance(self.rd, int): return os.read(self.rd, n) + elif self.rd: + return self.rd.recv(n) return None def write(self, msg): # type: (bytes) -> int - if isinstance(self.wr, ObjectPipe): - return self.wr.send(msg) - elif isinstance(self.wr, 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): @@ -996,8 +1202,8 @@ class Singlestep(AutomatonStopped): class InterceptionPoint(AutomatonStopped): def __init__(self, msg, state=None, result=None, packet=None): - # type: (str, Optional[Message], Optional[str], Optional[str]) -> None # noqa: E501 - 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): @@ -1009,8 +1215,12 @@ def debug(self, lvl, msg): if self.debug_level >= lvl: log_runtime.debug(msg) - def send(self, pkt): - # type: (Packet) -> None + def isrunning(self): + # type: () -> bool + return self.started.locked() + + 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 @@ -1033,7 +1243,7 @@ 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: @@ -1045,7 +1255,7 @@ def __iter__(self): def __del__(self): # type: () -> None - self.stop() + self.destroy() def _run_condition(self, cond, *args, **kargs): # type: (_StateWrapper, Any, Any) -> None @@ -1095,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() @@ -1117,9 +1329,9 @@ def _do_control(self, ready, *args, **kargs): elif c.type == _ATMT_Command.FREEZE: continue elif c.type == _ATMT_Command.STOP: - if self.stop_states: + if self.stop_state: # There is a stop state - self.state = self.stop_states[0](self) + self.state = self.stop_state() iterator = self._do_iter() else: # Act as FORCESTOP @@ -1149,6 +1361,10 @@ def _do_control(self, ready, *args, **kargs): self.cmdout.send(m) self.debug(3, "Stopping control thread (tid=%i)" % self.threadid) self.threadid = None + 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 @@ -1195,9 +1411,11 @@ def _do_iter(self): 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: @@ -1209,14 +1427,35 @@ def _do_iter(self): 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) if fd == self.cmdin: yield self.CommandMessage("Received command message") # noqa: E501 elif fd == self.listen_sock: - pkt = self.listen_sock.recv(MTU) + try: + 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 @@ -1239,7 +1478,7 @@ def __repr__(self): # type: () -> str return "" % ( self.__class__.__name__, - ["HALTED", "RUNNING"][self.started.locked()] + ["HALTED", "RUNNING"][self.isrunning()] ) # Public API @@ -1273,8 +1512,10 @@ def remove_breakpoints(self, *bps): def start(self, *args, **kargs): # type: (Any, Any) -> None - if not self.started.locked(): - self._do_start(*args, **kargs) + if self.isrunning(): + raise ValueError("Already started") + # Start the control thread + self._do_start(*args, **kargs) def run(self, resume=None, # type: Optional[Message] @@ -1301,40 +1542,66 @@ def run(self, 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 _flush_inout(self): # type: () -> None - 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() + # 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 - self.cmdin.send(Message(type=_ATMT_Command.STOP)) + try: + self.cmdin.send(Message(type=_ATMT_Command.STOP)) + except OSError: + pass if wait: - self._flush_inout() + with self.started: + self._flush_inout() def forcestop(self, wait=True): # type: (bool) -> None - self.cmdin.send(Message(type=_ATMT_Command.FORCESTOP)) + try: + self.cmdin.send(Message(type=_ATMT_Command.FORCESTOP)) + except OSError: + pass if wait: - self._flush_inout() + with self.started: + self._flush_inout() def restart(self, *args, **kargs): # type: (Any, Any) -> None diff --git a/scapy/autorun.py b/scapy/autorun.py index fd9df73ce09..1e5d4b10d26 100644 --- a/scapy/autorun.py +++ b/scapy/autorun.py @@ -7,9 +7,11 @@ 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 threading import traceback @@ -19,7 +21,7 @@ from scapy.error import log_scapy, Scapy_Exception from scapy.utils import tex_escape -from scapy.compat import ( +from typing import ( Any, Optional, TextIO, @@ -27,9 +29,6 @@ Tuple, ) -from scapy.libs.six.moves import queue -import scapy.libs.six as six - ######################### # Autorun stuff # @@ -63,7 +62,7 @@ def autorun_commands(_cmds, my_globals=None, verb=None): my_globals = _scapy_builtins() interp = ScapyAutorunInterpreter(locals=my_globals) try: - del six.moves.builtins.__dict__["scapy_session"]["_"] + del builtins.__dict__["scapy_session"]["_"] except KeyError: pass if verb is not None: @@ -99,9 +98,9 @@ def autorun_commands(_cmds, my_globals=None, verb=None): finally: conf.verb = sv try: - return six.moves.builtins.__dict__["scapy_session"]["_"] + return builtins.__dict__["scapy_session"]["_"] except KeyError: - return six.moves.builtins.__dict__.get("_", None) + return builtins.__dict__.get("_", None) def autorun_commands_timeout(cmds, timeout=None, **kwargs): @@ -113,7 +112,7 @@ def autorun_commands_timeout(cmds, timeout=None, **kwargs): if timeout is None: return autorun_commands(cmds, **kwargs) - q = queue.Queue() + q = Queue() # type: Queue[Any] def _runner(): # type: () -> None @@ -127,14 +126,14 @@ def _runner(): return q.get() -class StringWriter(six.StringIO): +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 - six.StringIO.__init__(self) + super().__init__() def write(self, x): # type: (str) -> int @@ -168,7 +167,7 @@ def autorun_get_interactive_session(cmds, **kargs): try: try: sys.stdout = sys.stderr = sw - sys.excepthook = sys.__excepthook__ # type: ignore + sys.excepthook = sys.__excepthook__ res = autorun_commands_timeout(cmds, **kargs) except StopAutorun as e: e.code_run = sw.s diff --git a/scapy/base_classes.py b/scapy/base_classes.py index 24872f09aad..6940223dc3e 100644 --- a/scapy/base_classes.py +++ b/scapy/base_classes.py @@ -11,9 +11,9 @@ # Generators # ################ -from __future__ import absolute_import from functools import reduce +import abc import operator import os import random @@ -28,9 +28,7 @@ from scapy.error import Scapy_Exception from scapy.consts import WINDOWS -from scapy.libs.six.moves import range - -from scapy.compat import ( +from typing import ( Any, Dict, Generic, @@ -41,14 +39,16 @@ Type, TypeVar, Union, - _Generic_metaclass, cast, + TYPE_CHECKING, ) -try: - import pyx -except ImportError: - pass +if TYPE_CHECKING: + try: + import pyx + except ImportError: + pass + from scapy.packet import Packet _T = TypeVar("_T") @@ -109,8 +109,75 @@ def __repr__(self): return "" % self.values +class _ScopedIP(str): + """ + A str that also holds extra attributes. + """ + __slots__ = ["scope"] + + def __init__(self, _: str) -> None: + self.scope = None + + 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""" + """ + 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 @@ -143,11 +210,16 @@ def int2ip(val): # type: (int) -> str return socket.inet_ntoa(struct.pack('!I', val)) - def __init__(self, net, stop=None): - # type: (str, Union[None, str]) -> None + 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) @@ -174,7 +246,10 @@ def __iter__(self): # type: () -> Iterator[str] # Python 2 won't handle huge (> sys.maxint) values in range() for i in range(self.count): - yield self.int2ip(self.start + i) + yield ScopedIP( + self.int2ip(self.start + i), + scope=self.scope, + ) def __len__(self): # type: () -> int @@ -187,20 +262,28 @@ def __iterlen__(self): def choice(self): # type: () -> str - return self.int2ip(random.randint(self.start, self.stop)) + return ScopedIP( + self.int2ip(random.randint(self.start, self.stop)), + scope=self.scope, + ) def __repr__(self): # 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")' % ( + return '%s("%s/%d"%s)' % ( self.__class__.__name__, self.net, self.mask, + scope_id_repr, ) - return '%s("%s", "%s")' % ( + return '%s("%s", "%s"%s)' % ( self.__class__.__name__, self.int2ip(self.start), self.int2ip(self.stop), + scope_id_repr, ) def __eq__(self, other): @@ -220,7 +303,7 @@ def __ne__(self, other): def __hash__(self): # type: () -> int - return hash(("scapy.Net", self.family, self.start, self.stop)) + return hash(("scapy.Net", self.family, self.start, self.stop, self.scope)) def __contains__(self, other): # type: (Any) -> bool @@ -278,20 +361,20 @@ def __iterlen__(self): # Packet abstract and base classes # ###################################### -class Packet_metaclass(_Generic_metaclass): - def __new__(cls, +class Packet_metaclass(type): + def __new__(cls: Type[_T], name, # type: str bases, # type: Tuple[type, ...] dct # type: Dict[str, Any] ): - # type: (...) -> Type['scapy.packet.Packet'] + # type: (...) -> Type['Packet'] if "fields_desc" in dct: # perform resolution of references to other packets # noqa: E501 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: # type: ignore + for pkt_fld in fld_or_pkt.fields_desc: resolved_fld.append(pkt_fld) else: resolved_fld.append(fld_or_pkt) @@ -299,7 +382,7 @@ def __new__(cls, resolved_fld = [] for b in bases: if hasattr(b, "fields_desc"): - resolved_fld = b.fields_desc # type: ignore + resolved_fld = b.fields_desc break if resolved_fld: # perform default value replacements @@ -346,17 +429,16 @@ def __new__(cls, ]) except (ImportError, AttributeError, KeyError): pass - newcls = cast('Type[scapy.packet.Packet]', - type.__new__(cls, name, bases, dct)) + 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( + newcls.__all_slots__ = set( # type: ignore attr for cls in newcls.__mro__ if hasattr(cls, "__slots__") for attr in cls.__slots__ ) - newcls.aliastypes = ( + newcls.aliastypes = ( # type: ignore [newcls] + getattr(newcls, "aliastypes", []) ) @@ -371,30 +453,30 @@ def __new__(cls, return newcls def __getattr__(self, attr): - # type: (str) -> scapy.fields.Field[Any, Any] - for k in self.fields_desc: # type: ignore + # type: (str) -> Any + for k in self.fields_desc: if k.name == attr: - return k # type: ignore + return k raise AttributeError(attr) def __call__(cls, *args, # type: Any **kargs # type: Any ): - # type: (...) -> 'scapy.packet.Packet' + # type: (...) -> 'Packet' if "dispatch_hook" in cls.__dict__: try: - cls = cls.dispatch_hook(*args, **kargs) # type: ignore + cls = cls.dispatch_hook(*args, **kargs) except Exception: from scapy import config if config.conf.debug_dissector: raise - cls = config.conf.raw_layer # type: ignore + cls = config.conf.raw_layer i = cls.__new__( cls, # type: ignore cls.__name__, cls.__bases__, - cls.__dict__ + cls.__dict__ # type: ignore ) i.__init__(*args, **kargs) return i # type: ignore @@ -402,22 +484,22 @@ def __call__(cls, # Note: see compat.py for an explanation -class Field_metaclass(_Generic_metaclass): - def __new__(cls, +class Field_metaclass(type): + def __new__(cls: Type[_T], name, # type: str bases, # type: Tuple[type, ...] dct # type: Dict[str, Any] ): - # type: (...) -> Type[scapy.fields.Field[Any, Any]] + # type: (...) -> Type[_T] dct.setdefault("__slots__", []) - newcls = super(Field_metaclass, cls).__new__(cls, name, bases, dct) + newcls = type.__new__(cls, name, bases, dct) return newcls # type: ignore PacketList_metaclass = Field_metaclass -class BasePacket(Gen['scapy.packet.Packet']): +class BasePacket(Gen['Packet']): __slots__ = [] # type: List[str] @@ -430,8 +512,9 @@ class BasePacketList(Gen[_T]): class _CanvasDumpExtended(object): - def canvas_dump(self, **kwargs): - # type: (**Any) -> 'pyx.canvas.canvas' + @abc.abstractmethod + def canvas_dump(self, layer_shift=0, rebuild=1): + # type: (int, int) -> pyx.canvas.canvas pass def psdump(self, filename=None, **kargs): diff --git a/scapy/compat.py b/scapy/compat.py index b076f446ec0..e3e2c2875a1 100644 --- a/scapy/compat.py +++ b/scapy/compat.py @@ -6,62 +6,36 @@ Python 2 and 3 link classes. """ -from __future__ import absolute_import import base64 import binascii -import collections -import gzip -import socket import struct import sys -import scapy.libs.six as six +from typing import ( + Any, + AnyStr, + Callable, + Optional, + TypeVar, + TYPE_CHECKING, + Union, +) # Very important: will issue typing errors otherwise __all__ = [ # typing - 'Any', - 'AnyStr', - 'Callable', - 'DefaultDict', - 'Deque', - 'Dict', - 'Generic', - 'IO', - 'Iterable', - 'Iterable', - 'Iterator', - 'List', + 'DecoratorCallable', 'Literal', - 'NamedTuple', - 'NewType', - 'NoReturn', - 'Optional', - 'Pattern', - 'Sequence', - 'Set', - 'Sized', - 'TextIO', - 'Tuple', - 'Type', - 'TypeVar', - 'Union', - 'ValuesView', - 'cast', - 'overload', - 'FAKE_TYPING', - 'TYPE_CHECKING', + 'Protocol', + 'Self', + 'UserDict', # compat - 'AddressFamily', 'base64_bytes', 'bytes_base64', 'bytes_encode', 'bytes_hex', 'chb', - 'gzip_compress', - 'gzip_decompress', 'hex_bytes', - 'lambda_tuple_converter', 'orb', 'plain_str', 'raw', @@ -71,38 +45,12 @@ # Note: # supporting typing on multiple python versions is a nightmare. -# Since Python 3.7, Generic is a type instead of a metaclass, -# therefore we can't support both at the same time. Our strategy -# is to only use the typing module if the Python version is >= 3.7 -# and use totally fake replacements otherwise. -# HOWEVER, when using the fake ones, to emulate stub Generic -# fields (e.g. _PacketField[str]) we need to add a fake -# __getitem__ to Field_metaclass - -try: - import typing # noqa: F401 - from typing import TYPE_CHECKING - if sys.version_info[0:2] <= (3, 6): - # Generic is messed up before Python 3.7 - # https://github.com/python/typing/issues/449 - raise ImportError - FAKE_TYPING = False -except ImportError: - FAKE_TYPING = True - TYPE_CHECKING = False - -# Import or create fake types +# we provide a FakeType class to be able to use types added on +# later Python versions (since we run mypy on 3.12), on older +# ones. -# If your class uses a metaclass AND Generic, you'll need to -# extend this class in the metaclass to avoid conflicts... -# Of course we wouldn't need this on Python 3 :/ -class _Generic_metaclass(type): - if FAKE_TYPING: - def __getitem__(self, typ): - # type: (Any) -> Any - return self - +# Import or create fake types def _FakeType(name, cls=object): # type: (str, Optional[type]) -> Any @@ -127,113 +75,30 @@ def __repr__(self): return _FT(name) -if not FAKE_TYPING: - # Only required if using mypy-lang for static typing - from typing import ( - Any, - AnyStr, - Callable, - DefaultDict, - Deque, - Dict, - Generic, - IO, - Iterable, - Iterator, - List, - NewType, - NoReturn, - Optional, - Pattern, - Sequence, - Set, - Sized, - TextIO, - Tuple, - Type, - TypeVar, - Union, - ValuesView, - cast, - overload, - ) +# Python 3.8 Only +if sys.version_info >= (3, 8): + from typing import Literal + from typing import Protocol else: - # Let's be creative and make some fake ones. - def cast(_type, obj): # type: ignore - return obj - - Any = _FakeType("Any") - AnyStr = _FakeType("AnyStr") # type: ignore - Callable = _FakeType("Callable") - DefaultDict = _FakeType("DefaultDict", # type: ignore - collections.defaultdict) - Deque = _FakeType("Deque") # type: ignore - Dict = _FakeType("Dict", dict) # type: ignore - IO = _FakeType("IO") # type: ignore - Iterable = _FakeType("Iterable") # type: ignore - Iterator = _FakeType("Iterator") # type: ignore - List = _FakeType("List", list) # type: ignore - NewType = _FakeType("NewType") - NoReturn = _FakeType("NoReturn") - Optional = _FakeType("Optional") - Pattern = _FakeType("Pattern") # type: ignore - Sequence = _FakeType("Sequence", list) # type: ignore - Set = _FakeType("Set", set) # type: ignore - TextIO = _FakeType("TextIO") # type: ignore - Tuple = _FakeType("Tuple") - Type = _FakeType("Type", type) - TypeVar = _FakeType("TypeVar") # type: ignore - Union = _FakeType("Union") - ValuesView = _FakeType("List", list) # type: ignore - - class Sized(object): # type: ignore - pass + Literal = _FakeType("Literal") - @six.add_metaclass(_Generic_metaclass) - class Generic(object): # type: ignore + class Protocol: pass - overload = lambda x: x - -# Broken < Python 3.7 -if sys.version_info >= (3, 7): - from typing import NamedTuple +# Python 3.9 Only +if sys.version_info >= (3, 9): + from collections import UserDict else: - # Hack for Python < 3.7 - Implement NamedTuple pickling - def _unpickleNamedTuple(name, len_params, *args): - return collections.namedtuple( - name, - args[:len_params] - )(*args[len_params:]) - - def NamedTuple(name, params): - tup_params = tuple(x[0] for x in params) - cls = collections.namedtuple(name, tup_params) - - class _NT(cls): - def __reduce__(self): - """Used by pickling methods""" - return (_unpickleNamedTuple, - (name, len(tup_params)) + tup_params + tuple(self)) - _NT.__name__ = cls.__name__ - return _NT + from collections import UserDict as _UserDict + UserDict = _FakeType("_UserDict", _UserDict) -# Python 3.8 Only -if sys.version_info >= (3, 8): - from typing import Literal -else: - Literal = _FakeType("Literal") -# Python 3.4 -if sys.version_info >= (3, 4): - from socket import AddressFamily +# Python 3.11 Only +if sys.version_info >= (3, 11): + from typing import Self else: - class AddressFamily: - AF_INET = socket.AF_INET - AF_INET6 = socket.AF_INET6 - AF_UNSPEC = socket.AF_UNSPEC - + Self = _FakeType("Self") ########### # Python3 # @@ -243,92 +108,52 @@ class AddressFamily: DecoratorCallable = TypeVar("DecoratorCallable", bound=Callable[..., Any]) -def lambda_tuple_converter(func): - # type: (DecoratorCallable) -> DecoratorCallable - """ - Converts a Python 2 function as - lambda (x,y): x + y - In the Python 3 format: - lambda x,y : x + y - """ - if func is not None and func.__code__.co_argcount == 1: - return lambda *args: func( # type: ignore - args[0] if len(args) == 1 else args - ) - else: - return func - - # 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 -if six.PY2: - bytes_encode = plain_str = str # type: Callable[[Any], bytes] - orb = ord # type: Callable[[bytes], int] - - def chb(x): - # type: (int) -> bytes - if isinstance(x, str): - return x - return chr(x) - - def raw(x): - # type: (Union[Packet]) -> bytes - """ - Builds a packet and returns its bytes representation. - This function is and will always be cross-version compatible - """ - if hasattr(x, "__bytes__"): - return x.__bytes__() - return bytes(x) -else: - def raw(x): - # type: (Union[Packet]) -> bytes - """ - Builds a packet and returns its bytes representation. - This function is and will always be cross-version compatible - """ - 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) - - if sys.version_info[0:2] <= (3, 4): - def plain_str(x): - # type: (AnyStr) -> str - """Convert basic byte objects to str""" - if isinstance(x, bytes): - return x.decode(errors="ignore") - return str(x) - else: - # Python 3.5+ - 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 raw(x): + # type: (Packet) -> bytes + """ + Builds a packet and returns its bytes representation. + This function is and will always be cross-version compatible + """ + 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): @@ -343,69 +168,25 @@ def hex_bytes(x): return binascii.a2b_hex(bytes_encode(x)) -if six.PY2: - def int_bytes(x, size): - # type: (int, int) -> bytes - """Convert an int to an arbitrary sized bytes string""" - _hx = hex(x)[2:].strip("L") - return binascii.unhexlify("0" * (size * 2 - len(_hx)) + _hx) +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(x.encode('hex'), 16) -else: - 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 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) # type: ignore 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', '') # type: ignore return base64.encodebytes(bytes_encode(x)).replace(b'\n', b'') - - -if six.PY2: - import cgi - html_escape = cgi.escape -else: - import html - html_escape = html.escape - - -if six.PY2: - from StringIO import StringIO - - def gzip_decompress(x): - # type: (AnyStr) -> bytes - """Decompress using gzip""" - with gzip.GzipFile(fileobj=StringIO(x), mode='rb') as fdesc: - return fdesc.read() - - def gzip_compress(x): - # type: (AnyStr) -> bytes - """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 diff --git a/scapy/config.py b/scapy/config.py index ff1ab877f08..be3cb25b278 100755 --- a/scapy/config.py +++ b/scapy/config.py @@ -7,32 +7,41 @@ 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 socket import sys import time import warnings +from dataclasses import dataclass +from enum import Enum + +import importlib +import importlib.abc +import importlib.util + 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.libs 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 -from scapy.compat import ( +# Typing imports +from typing import ( cast, Any, Callable, - DecoratorCallable, Dict, Iterator, List, @@ -46,10 +55,12 @@ 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 @@ -136,19 +147,23 @@ def _readonly(name): class ProgPath(ConfClass): - _default = "" - 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: @@ -248,14 +263,14 @@ def get(self, 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))) @@ -279,6 +294,12 @@ def __repr__(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) @@ -302,7 +323,7 @@ def filter(self, items): """Disable dissection of unused layers to speed up dissection""" if self.filtered: raise ValueError("Already filtered. Please disable it first") - for lay in six.itervalues(self.ldict): + for lay in self.ldict.values(): for cls in lay: if cls not in self._backup_dict: self._backup_dict[cls] = cls.payload_guess[:] @@ -316,7 +337,7 @@ def unfilter(self): """Re-enable dissection for all layers""" if not self.filtered: raise ValueError("Not filtered. Please filter first") - for lay in six.itervalues(self.ldict): + for lay in self.ldict.values(): for cls in lay: cls.payload_guess = self._backup_dict[cls] self._backup_dict.clear() @@ -345,8 +366,8 @@ def lsc(): print(repr(conf.commands)) -class CacheInstance(Dict[str, Any], 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 @@ -356,22 +377,25 @@ def __init__(self, name="noname", timeout=None): def flush(self): # type: () -> None - CacheInstance.__init__( - self, - name=self.name, - timeout=self.timeout - ) + self._timetable.clear() + self.clear() def __getitem__(self, item): # type: (str) -> Any if item in self.__slots__: return object.__getattribute__(self, item) - val = super(CacheInstance, self).__getitem__(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 @@ -389,12 +413,12 @@ def __setitem__(self, item, v): self._timetable[item] = time.time() super(CacheInstance, self).__setitem__(item, v) - def update(self, # type: ignore + def update(self, other, # type: Any **kwargs # type: Any ): # type: (...) -> None - for key, value in six.iteritems(other): + 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]: @@ -404,16 +428,24 @@ def update(self, # type: ignore def iteritems(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return six.iteritems(self.__dict__) # type: ignore + 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__) # type: ignore + 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): # type: () -> Iterator[str] @@ -422,30 +454,25 @@ def __iter__(self): def itervalues(self): # type: () -> Iterator[Tuple[str, Any]] if self.timeout is None: - return six.itervalues(self.__dict__) # type: ignore + 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): # type: () -> Any - if self.timeout is None: - 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 list(self.iteritems()) def keys(self): # type: () -> Any - if self.timeout is None: - 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 list(self.iterkeys()) def values(self): # type: () -> Any - 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 + return list(self.itervalues()) def __len__(self): # type: () -> int @@ -461,9 +488,9 @@ 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) @@ -510,6 +537,187 @@ def __repr__(self): 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: + 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. @@ -609,27 +817,23 @@ def _set_conf_sockets(): 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( # type: ignore + conf.L3socket6 = functools.partial( L3pcapSocket, filter="ip6") conf.L2socket = L2pcapSocket conf.L2listen = L2pcapListenSocket - conf.ifaces.reload() - 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( # type: ignore + conf.L3socket6 = functools.partial( L3bpfSocket, filter="ip6") conf.L2socket = L2bpfSocket conf.L2listen = L2bpfListenSocket - conf.ifaces.reload() - return - if LINUX: + elif LINUX: from scapy.arch.linux import L3PacketSocket, L2Socket, L2ListenSocket conf.L3socket = L3PacketSocket conf.L3socket6 = cast( @@ -641,23 +845,19 @@ def _set_conf_sockets(): ) conf.L2socket = L2Socket conf.L2listen = L2ListenSocket - conf.ifaces.reload() - 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 - conf.ifaces.reload() - # No need to update globals on Windows - return else: - from scapy.supersocket import L3RawSocket - from scapy.layers.inet6 import L3RawSocket6 + from scapy.supersocket import L3RawSocket, L3RawSocket6 conf.L3socket = L3RawSocket conf.L3socket6 = L3RawSocket6 + # Reload the interfaces + conf.ifaces.reload() def _socket_changer(attr, val, old): @@ -703,30 +903,42 @@ def _iface_changer(attr, val, old): "See conf.ifaces output" ) return iface - return val # type: ignore + 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. """ - version = ReadOnlyAttribute("version", VERSION) - session = "" #: filename where the session will be saved + version: str = ReadOnlyAttribute("version", VERSION) + session: str = "" #: filename where the session will be saved interactive = False - #: can be "ipython", "python" or "auto". Default: Auto - 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" #: selects the default output interface for srp() and sendp(). - iface = Interceptor("iface", None, _iface_changer) # type: 'scapy.interfaces.NetworkInterface' # type: ignore # noqa: E501 - layers = LayersList() + 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 @@ -744,32 +956,33 @@ class Conf(ConfClass): #: ones in ICMP citation check_TCPerror_seqack = False verb = 2 #: level of verbosity, from 0 (almost mute) to 3 (verbose) - prompt = Interceptor("prompt", ">>> ", _prompt_changer) - #: default mode for listening socket (to get answers if you + prompt: str = Interceptor("prompt", ">>> ", _prompt_changer) + #: default mode for the promiscuous mode of a socket (to get answers if you #: spoof on a lan) - promisc = True - #: default mode for sniff() 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() - l3types = Num2Layer() + 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] - USBsocket = 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 #: history file - histfile = os.getenv('SCAPY_HISTFILE', - os.path.join(os.path.expanduser("~"), - ".scapy_history")) + 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 @@ -779,15 +992,24 @@ class Conf(ConfClass): 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. + #: When 1, print some TLS session secrets when they are computed, and + #: warn about the session recognition. debug_tls = False wepkey = "" #: 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]] - neighbor = None # type: 'scapy.layers.l2.Neighbor' + 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' @@ -796,43 +1018,49 @@ class Conf(ConfClass): #: 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() + prog: ProgPath = ProgPath() #: holds list of fields for which resolution should be done - resolve = Resolve() + resolve: Resolve = Resolve() #: holds list of enum fields for which conversion to string #: should NOT be done - noenum = Resolve() - emph = Emphasize() + noenum: Resolve = Resolve() + emph: Emphasize = Emphasize() #: read only attribute to show if PyPy is in use - use_pypy = ReadOnlyAttribute("use_pypy", isPyPy()) + 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 = Interceptor( + use_pcap: bool = Interceptor( "use_pcap", 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 - #: path or list of paths where extensions are to be looked for - extensions_paths = "." + 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 = NetCache() + #: netcache holds time-based caches for net operations + netcache: NetCache = NetCache() geoip_city = None + #: 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 = [ + load_layers: List[str] = [ 'bluetooth', 'bluetooth4LE', 'dcerpc', @@ -843,6 +1071,7 @@ class Conf(ConfClass): 'dot15d4', 'eap', 'gprs', + 'gssapi', 'hsrp', 'inet', 'inet6', @@ -856,8 +1085,9 @@ class Conf(ConfClass): 'llmnr', 'lltd', 'mgcp', + 'msrpce.rpcclient', + 'msrpce.rpcserver', 'mobileip', - 'mspac', 'netbios', 'netflow', 'ntlm', @@ -876,6 +1106,7 @@ class Conf(ConfClass): 'smbclient', 'smbserver', 'snmp', + 'spnego', 'tftp', 'vrrp', 'vxlan', @@ -885,9 +1116,13 @@ class Conf(ConfClass): #: 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. @@ -895,7 +1130,35 @@ class Conf(ConfClass): #: When True, raise exception if no dst MAC found otherwise broadcast. #: Default is False. raise_no_dst_mac = False - loopback_name = "lo" if LINUX else "lo0" + 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 @@ -929,7 +1192,7 @@ def __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) @@ -948,7 +1211,7 @@ def func_in(*args, **kwargs): raise ImportError("Cannot execute crypto-related method! " "Please install python-cryptography v1.7 or later.") # noqa: E501 return func(*args, **kwargs) - return func_in # type: ignore + return func_in def scapy_delete_temp_files(): diff --git a/scapy/consts.py b/scapy/consts.py index ecff481ed29..ed8ce90c723 100644 --- a/scapy/consts.py +++ b/scapy/consts.py @@ -10,6 +10,20 @@ 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 diff --git a/scapy/contrib/__init__.py b/scapy/contrib/__init__.py index 82f83176f1d..8c54a8489ae 100644 --- a/scapy/contrib/__init__.py +++ b/scapy/contrib/__init__.py @@ -6,3 +6,6 @@ """ 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 eacbd8532e9..b263872b0bb 100644 --- a/scapy/contrib/altbeacon.py +++ b/scapy/contrib/altbeacon.py @@ -11,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 @@ -54,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/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..d93f62aa3c5 --- /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 v1.7+. " + "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/definitions.py b/scapy/contrib/automotive/bmw/definitions.py index aa11be6209d..fe75c27cf5c 100644 --- a/scapy/contrib/automotive/bmw/definitions.py +++ b/scapy/contrib/automotive/bmw/definitions.py @@ -9,13 +9,12 @@ from scapy.packet import Packet, bind_layers from scapy.fields import ByteField, ShortField, ByteEnumField, X3BytesField, \ - StrField, StrFixedLenField, LEIntField, LEThreeBytesField, \ + 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", @@ -252,10 +251,60 @@ def i2repr(self, pkt, x): 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"}), + ByteEnumField("processClass", 0, process_classes), XStrFixedLenField("svk_id", b"", length=4), ByteField("mainVersion", 0), ByteField("subVersion", 0), @@ -272,14 +321,16 @@ 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", 0), - ByteField("pad1", 0), - LEIntField("prog_milage", 0), - StrFixedLenField("pad2", b'\x00\x00\x00\x00\x00', length=5), + StrFixedLenField("pad", b'\x00', length_from=get_length), PacketListField("entries", [], SVK_Entry, count_from=lambda x: x.entries_count)] @@ -372,7 +423,6 @@ class WEBSERVER(Packet): 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) @@ -438,7 +488,6 @@ class WEBSERVER(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" @@ -1505,7 +1554,7 @@ class WEBSERVER(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" @@ -1831,13 +1880,13 @@ class WEBSERVER(Packet): 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[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" @@ -4864,7 +4913,7 @@ class WEBSERVER(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" diff --git a/scapy/contrib/automotive/bmw/enumerator.py b/scapy/contrib/automotive/bmw/enumerator.py index 571a9676729..e19aad16c8e 100644 --- a/scapy/contrib/automotive/bmw/enumerator.py +++ b/scapy/contrib/automotive/bmw/enumerator.py @@ -8,12 +8,16 @@ from scapy.packet import Packet -from scapy.compat import Any, Iterable 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 " \ diff --git a/scapy/contrib/automotive/bmw/hsfz.py b/scapy/contrib/automotive/bmw/hsfz.py index dc54804cda4..3316d7d136f 100644 --- a/scapy/contrib/automotive/bmw/hsfz.py +++ b/scapy/contrib/automotive/bmw/hsfz.py @@ -6,19 +6,27 @@ # scapy.contrib.description = HSFZ - BMW High-Speed-Fahrzeug-Zugang # scapy.contrib.status = loads import logging -import struct import socket +import struct import time +from typing import ( + Any, + Optional, + Tuple, + Type, + Iterable, + List, + Union, +) -from scapy.compat import Optional, Tuple, Type, Iterable, List, Union from scapy.contrib.automotive import log_automotive -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, 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). @@ -26,22 +34,67 @@ 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('type', 1, {0x01: "message", - 0x02: "echo"}), - XByteField('src', 0), - XByteField('dst', 0), + 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.src ^ self.dst) + hdr_hash = struct.pack("B", self.source ^ self.target) pay_hash = self.payload.hashret() return hdr_hash + pay_hash @@ -62,6 +115,11 @@ def post_build(self, 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) @@ -78,14 +136,35 @@ def __init__(self, ip='127.0.0.1', port=6801): 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, src, dst, ip='127.0.0.1', port=6801, basecls=UDS): + 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.src = src - self.dst = dst + self.source = source + self.target = target self.basecls = HSFZ self.outputcls = basecls @@ -98,7 +177,7 @@ def send(self, x): try: return super(UDS_HSFZSocket, self).send( - HSFZ(src=self.src, dst=self.dst) / x) + HSFZ(source=self.source, target=self.target) / x) except Exception as e: # Workaround: # This catch block is currently necessary to detect errors @@ -112,18 +191,18 @@ def send(self, x): self.close() return 0 - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (Optional[int], **Any) -> Optional[Packet] pkt = super(UDS_HSFZSocket, self).recv(x) - if pkt: - return self.outputcls(bytes(pkt.payload)) + 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] - src=0xf4, # type: int + source=0xf4, # type: int timeout=0.1, # type: Union[int, float] verbose=True # type: bool ): @@ -136,7 +215,7 @@ def hsfz_scan(ip, # type: str :param ip: IPv4 address of target to scan :param scan_range: Range for HSFZ destination address - :param src: HSFZ source address, used during the scan + :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 @@ -145,7 +224,7 @@ def hsfz_scan(ip, # type: str log_automotive.setLevel(logging.DEBUG) results = list() for i in scan_range: - with UDS_HSFZSocket(src, i, ip) as sock: + with UDS_HSFZSocket(source, i, ip) as sock: try: resp = sock.sr1(UDS() / UDS_TP(), timeout=timeout, @@ -154,8 +233,8 @@ def hsfz_scan(ip, # type: str results.append((i, resp)) if resp: log_automotive.debug( - "Found endpoint %s, src=0x%x, dst=0x%x" % (ip, src, i)) + "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, dst, ip) for dst, _ in results] + return [UDS_HSFZSocket(0xf4, target, ip) for target, _ in results] diff --git a/scapy/contrib/automotive/doip.py b/scapy/contrib/automotive/doip.py index 36571cccedd..acd58a811ba 100644 --- a/scapy/contrib/automotive/doip.py +++ b/scapy/contrib/automotive/doip.py @@ -8,20 +8,41 @@ # scapy.contrib.description = Diagnostic over IP (DoIP) / ISO 13400 # scapy.contrib.status = loads -import struct 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.fields import ByteEnumField, ConditionalField, \ - XByteField, XShortField, XIntField, XShortEnumField, XByteEnumField, \ - IntField, StrFixedLenField, XStrField -from scapy.packet import Packet, bind_layers, bind_bottom_up -from scapy.supersocket import StreamSocket -from scapy.layers.inet import TCP, UDP from scapy.contrib.automotive.uds import UDS from scapy.data import MTU -from scapy.compat import Union, Tuple, Optional +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): @@ -99,8 +120,12 @@ class DoIP(Packet): 0x8003: "Diagnostic message NACK"} name = 'DoIP' fields_desc = [ - XByteField("protocol_version", 0x02), - XByteField("inverse_version", 0xFD), + 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, { @@ -116,7 +141,7 @@ class DoIP(Packet): lambda p: p.payload_type in [2, 4]), ConditionalField(StrFixedLenField("gid", b"", 6), lambda p: p.payload_type in [4]), - ConditionalField(XByteEnumField("further_action", 0, { + 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", @@ -127,7 +152,9 @@ class DoIP(Packet): 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]), + })), 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", @@ -216,10 +243,26 @@ def answers(self, other): def hashret(self): # type: () -> bytes - if self.payload_type in [0x8001, 0x8002, 0x8003]: - return bytes(self)[:2] + struct.pack( - "H", self.target_address ^ self.source_address) - return bytes(self)[:2] + 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 @@ -227,25 +270,74 @@ def post_build(self, pkt, pay): 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:] + 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], None + return s[:self.payload_length - 4], s[self.payload_length - 4:] else: - return b"", None + 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 DoIPSocket(StreamSocket): - """ Custom StreamSocket for DoIP communication. This sockets automatically - sends a routing activation request as soon as a TCP connection is +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 @@ -255,47 +347,126 @@ class DoIPSocket(StreamSocket): 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', port=13400, activate_routing=True, - source_address=0xe80, target_address=0, - activation_type=0, reserved_oem=b""): - # type: (str, int, bool, int, int, int, bytes) -> None + + 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._init_socket() + 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 - if activate_routing: - self._activate_routing( - source_address, target_address, activation_type, reserved_oem) + def _init_socket(self): + # type: () -> None + connected = False + addrinfo = socket.getaddrinfo(self.ip, self.port, proto=socket.IPPROTO_TCP) + sock_family = addrinfo[0][0] - def _init_socket(self, sock_family=socket.AF_INET): - # type: (int) -> None 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) - s.connect((self.ip, self.port)) - StreamSocket.__init__(self, s, DoIP) - - def _activate_routing(self, - source_address, # type: int - target_address, # type: int - activation_type, # type: int - reserved_oem=b"" # type: bytes - ): # type: (...) -> None + + 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=activation_type, - source_address=source_address, reserved_oem=reserved_oem), + 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 = target_address or \ - resp.logical_address_doip_entity + 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) @@ -303,41 +474,16 @@ def _activate_routing(self, 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 -class DoIPSocket6(DoIPSocket): - """ Custom StreamSocket for DoIP communication over IPv6. - This sockets automatically sends a routing activation request as soon as - a TCP connection is established. - - :param ip: IPv6 address of destination - :param port: destination port, usually 13400 - :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 - - Example: - >>> socket = DoIPSocket6("2001:16b8:3f0e:2f00:21a:37ff:febf:edb9") - >>> 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='::1', port=13400, activate_routing=True, - source_address=0xe80, target_address=0, - activation_type=0, reserved_oem=b""): - # type: (str, int, bool, int, int, int, bytes) -> None - self.ip = ip - self.port = port - self.source_address = source_address - super(DoIPSocket6, self)._init_socket(socket.AF_INET6) - - if activate_routing: - super(DoIPSocket6, self)._activate_routing( - source_address, target_address, activation_type, reserved_oem) + 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): @@ -350,11 +496,14 @@ class UDS_DoIPSocket(DoIPSocket): >>> 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 + pkt = DoIP(payload_type=0x8001, + source_address=self.source_address, + target_address=self.target_address + ) / x else: pkt = x @@ -363,35 +512,14 @@ def send(self, x): except AttributeError: pass - return super(UDS_DoIPSocket, self).send(pkt) + return super().send(pkt) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] - pkt = super(UDS_DoIPSocket, self).recv(x) + 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 - -class UDS_DoIPSocket6(DoIPSocket6, UDS_DoIPSocket): - """ - Application-Layer socket for DoIP endpoints. This socket takes care about - the encapsulation of UDS packets into DoIP packets. - - Example: - >>> socket = UDS_DoIPSocket6("2001:16b8:3f0e:2f00:21a:37ff:febf:edb9") - >>> pkt = UDS() / UDS_RDBI(identifiers=[0x1000]) - >>> resp = socket.sr1(pkt, timeout=1) - """ pass - - -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) diff --git a/scapy/contrib/automotive/ecu.py b/scapy/contrib/automotive/ecu.py index b7917a89eb4..7458468c95b 100644 --- a/scapy/contrib/automotive/ecu.py +++ b/scapy/contrib/automotive/ecu.py @@ -15,9 +15,7 @@ from types import GeneratorType from threading import Lock -import scapy.libs.six as six -from scapy.compat import Any, Union, Iterable, Callable, List, Optional, \ - Tuple, Type, cast, Dict, orb, ValuesView +from scapy.compat import orb from scapy.packet import Raw, Packet from scapy.plist import PacketList from scapy.sessions import DefaultSession @@ -25,6 +23,20 @@ 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"] @@ -40,7 +52,7 @@ class EcuState(object): def __init__(self, **kwargs): # type: (Any) -> None - self.__cache__ = None # type: Optional[Tuple[List[EcuState], ValuesView[Any]]] # noqa: E501 + 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) @@ -48,24 +60,24 @@ def __init__(self, **kwargs): def _expand(self): # type: () -> List[EcuState] - if self.__cache__ is None or \ - self.__cache__[1] != self.__dict__.values(): + 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 self.__dict__.values()]): + for x in itertools.product(*[self._flatten(v) for v in values]): kwargs = {} - for i, k in enumerate(self.__dict__.keys()): + for i, k in enumerate(keys): if x[i] is None: continue kwargs[k] = x[i] expanded.append(EcuState(**kwargs)) - self.__cache__ = (expanded, self.__dict__.values()) + self.__cache__ = (expanded, values) return self.__cache__[0] @staticmethod def _flatten(x): # type: (Any) -> List[Any] - if isinstance(x, (six.string_types, bytes)): + if isinstance(x, (str, bytes)): return [x] elif hasattr(x, "__iter__") and hasattr(x, "__len__") and len(x) == 1: return list(*x) @@ -457,17 +469,16 @@ class EcuSession(DefaultSession): """ def __init__(self, *args, **kwargs): # type: (Any, Any) -> None - DefaultSession.__init__(self, *args, **kwargs) 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): - # type: (Optional[Packet]) -> None + def process(self, pkt: Packet) -> Optional[Packet]: if not pkt: - return + return None self.ecu.update(pkt) - DefaultSession.on_packet_received(self, pkt) + return pkt class EcuResponse: @@ -505,7 +516,6 @@ def __init__(self, state=None, responses=Raw(b"\x7f\x10"), answers=None): state = cast(List[EcuState], state) self.__states = state else: - state = cast(EcuState, state) self.__states = [state] if isinstance(responses, PacketList): diff --git a/scapy/contrib/automotive/gm/gmlan.py b/scapy/contrib/automotive/gm/gmlan.py index cc185d73ceb..76c9cf110f1 100644 --- a/scapy/contrib/automotive/gm/gmlan.py +++ b/scapy/contrib/automotive/gm/gmlan.py @@ -10,10 +10,25 @@ import struct from scapy.contrib.automotive import log_automotive -from scapy.fields import ObservableDict, XByteEnumField, ByteEnumField, \ - ConditionalField, XByteField, StrField, XShortEnumField, XShortField, \ - X3BytesField, XIntField, ShortField, PacketField, PacketListField, \ - FieldListField, MultipleTypeField, StrFixedLenField +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.contrib.isotp import ISOTP @@ -26,11 +41,11 @@ if conf.contribs['GMLAN']['treat-response-pending-as-answer']: pass except KeyError: - 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.") + # 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} conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = None @@ -112,7 +127,7 @@ 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) @@ -725,7 +740,8 @@ class GMLAN_NR(Packet): name = 'NegativeResponse' fields_desc = [ 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) ] diff --git a/scapy/contrib/automotive/gm/gmlan_ecu_states.py b/scapy/contrib/automotive/gm/gmlan_ecu_states.py index 682f8a93b85..4e118512c4d 100644 --- a/scapy/contrib/automotive/gm/gmlan_ecu_states.py +++ b/scapy/contrib/automotive/gm/gmlan_ecu_states.py @@ -5,7 +5,6 @@ # 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 @@ -36,3 +35,7 @@ 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 index d992c6fd987..33fc79c1073 100644 --- a/scapy/contrib/automotive/gm/gmlan_logging.py +++ b/scapy/contrib/automotive/gm/gmlan_logging.py @@ -13,9 +13,14 @@ 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.compat import Tuple, Any 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): diff --git a/scapy/contrib/automotive/gm/gmlan_scanner.py b/scapy/contrib/automotive/gm/gmlan_scanner.py index 3308f0a8608..46db962a41b 100644 --- a/scapy/contrib/automotive/gm/gmlan_scanner.py +++ b/scapy/contrib/automotive/gm/gmlan_scanner.py @@ -13,11 +13,9 @@ from collections import defaultdict -from scapy.compat import Optional, List, Type, Any, Tuple, Iterable, Dict, \ - cast, Callable, orb +from scapy.compat import orb from scapy.contrib.automotive import log_automotive from scapy.packet import Packet -import scapy.libs.six as six from scapy.config import conf from scapy.supersocket import SuperSocket from scapy.error import Scapy_Exception @@ -43,6 +41,18 @@ # 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", @@ -52,8 +62,7 @@ "GMLAN_DCEnumerator"] -@six.add_metaclass(abc.ABCMeta) -class GMLAN_Enumerator(ServiceEnumerator): +class GMLAN_Enumerator(ServiceEnumerator, metaclass=abc.ABCMeta): """ Abstract base class for GMLAN service enumerators. This class implements GMLAN specific functions. diff --git a/scapy/contrib/automotive/gm/gmlanutils.py b/scapy/contrib/automotive/gm/gmlanutils.py index 27901297bbc..370e644ba68 100644 --- a/scapy/contrib/automotive/gm/gmlanutils.py +++ b/scapy/contrib/automotive/gm/gmlanutils.py @@ -9,7 +9,6 @@ import time -from scapy.compat import Optional, cast, Callable from scapy.contrib.automotive import log_automotive from scapy.contrib.automotive.gm.gmlan import GMLAN, GMLAN_SA, GMLAN_RD, \ @@ -20,6 +19,12 @@ from scapy.contrib.isotp import ISOTPSocket from scapy.utils import PeriodicSenderThread +from typing import ( + Optional, + cast, + Callable, +) + __all__ = ["GMLAN_TesterPresentSender", "GMLAN_InitDiagnostics", "GMLAN_GetSecurityAccess", "GMLAN_RequestDownload", "GMLAN_TransferData", "GMLAN_TransferPayload", @@ -62,7 +67,7 @@ def run(self): 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) - time.sleep(self._interval) + self._stopped.wait(timeout=self._interval) if self._stopped.is_set() or self._socket.closed: break diff --git a/scapy/contrib/automotive/kwp.py b/scapy/contrib/automotive/kwp.py index 10679c9fdb5..5617c1d7a23 100644 --- a/scapy/contrib/automotive/kwp.py +++ b/scapy/contrib/automotive/kwp.py @@ -7,18 +7,31 @@ # scapy.contrib.status = loads import struct -import time -from scapy.fields import ByteEnumField, StrField, ConditionalField, \ - BitField, XByteField, X3BytesField, ByteField, \ - ObservableDict, XShortEnumField, XByteEnumField +from scapy.fields import ( + BitField, + ByteEnumField, + ByteField, + ConditionalField, + MayEnd, + ObservableDict, + StrField, + X3BytesField, + XByteEnumField, + XByteField, + XShortEnumField, +) from scapy.packet import Packet, bind_layers, NoPayload from scapy.config import conf from scapy.error import log_loading from scapy.utils import PeriodicSenderThread from scapy.plist import _PacketIterable from scapy.contrib.isotp import ISOTP -from scapy.compat import Dict, Any + +from typing import ( + Dict, + Any, +) try: @@ -112,7 +125,7 @@ def answers(self, other): def hashret(self): # type: () -> bytes if self.service == 0x7f: - return struct.pack('B', self.requestServiceId) + return struct.pack('B', self.requestServiceId & ~0x40) else: return struct.pack('B', self.service & ~0x40) @@ -388,7 +401,8 @@ class KWP_ROE(Packet): fields_desc = [ ByteEnumField('responseRequired', 1, responseTypes), ByteEnumField('eventWindowTime', 0, eventWindowTimes), - ByteEnumField('eventType', 0, eventTypes), + MayEnd(ByteEnumField('eventType', 0, eventTypes)), + # XXX Is this MayEnd correct? ByteField('eventParameter', 0), ByteEnumField('serviceToRespond', 0, KWP.services), ByteField('serviceParameter', 0) @@ -402,7 +416,8 @@ class KWP_ROEPR(Packet): name = 'ResponseOnEventPositiveResponse' fields_desc = [ ByteField("numberOfActivatedEvents", 0), - ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes), + MayEnd(ByteEnumField('eventWindowTime', 0, KWP_ROE.eventWindowTimes)), + # XXX Is this MayEnd correct? ByteEnumField('eventType', 0, KWP_ROE.eventTypes), ] @@ -955,7 +970,8 @@ class KWP_NR(Packet): } name = 'NegativeResponse' fields_desc = [ - XByteEnumField('requestServiceId', 0, KWP.services), + MayEnd(XByteEnumField('requestServiceId', 0, KWP.services)), + # XXX Is this MayEnd correct? ByteEnumField('negativeResponseCode', 0, negativeResponseCodes) ] @@ -974,7 +990,8 @@ def answers(self, other): # ################################################################## class KWP_TesterPresentSender(PeriodicSenderThread): - def __init__(self, sock, pkt=KWP() / KWP_TP(), interval=2): + def __init__(self, sock, pkt=KWP() / KWP_TP(responseRequired=0x02), + interval=2): # type: (Any, _PacketIterable, float) -> None """ Thread that sends TesterPresent packets periodically @@ -989,4 +1006,6 @@ def run(self): while not self._stopped.is_set(): for p in self._pkts: self._socket.sr1(p, timeout=0.3, verbose=False) - time.sleep(self._interval) + self._stopped.wait(timeout=self._interval) + if self._stopped.is_set() or self._socket.closed: + break diff --git a/scapy/contrib/automotive/obd/obd.py b/scapy/contrib/automotive/obd/obd.py index 2256ed711d2..a165935d2c3 100644 --- a/scapy/contrib/automotive/obd/obd.py +++ b/scapy/contrib/automotive/obd/obd.py @@ -9,7 +9,6 @@ import struct -from scapy.contrib.automotive import log_automotive from scapy.contrib.automotive.obd.iid.iids import * from scapy.contrib.automotive.obd.mid.mids import * from scapy.contrib.automotive.obd.pid.pids import * @@ -24,11 +23,11 @@ if conf.contribs['OBD']['treat-response-pending-as-answer']: pass except KeyError: - 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.") + # 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} diff --git a/scapy/contrib/automotive/obd/scanner.py b/scapy/contrib/automotive/obd/scanner.py index 2efac6c0912..00884e5813d 100644 --- a/scapy/contrib/automotive/obd/scanner.py +++ b/scapy/contrib/automotive/obd/scanner.py @@ -10,7 +10,6 @@ import copy -from scapy.compat import List, Type, Any, Iterable from scapy.contrib.automotive.obd.obd import OBD, OBD_S03, OBD_S07, OBD_S0A, \ OBD_S01, OBD_S06, OBD_S08, OBD_S09, OBD_NR, OBD_S02, OBD_S02_Record from scapy.config import conf @@ -25,6 +24,14 @@ 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) diff --git a/scapy/contrib/automotive/scanner/configuration.py b/scapy/contrib/automotive/scanner/configuration.py index 41871b79228..f7342bc11b8 100644 --- a/scapy/contrib/automotive/scanner/configuration.py +++ b/scapy/contrib/automotive/scanner/configuration.py @@ -7,13 +7,23 @@ # scapy.contrib.status = library import inspect +from threading import Event -from scapy.compat import Any, Union, List, Type, Set, cast 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): """ @@ -105,15 +115,55 @@ def __init__(self, test_cases, **kwargs): 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 index 37e97f194c8..c778b56f53b 100644 --- a/scapy/contrib/automotive/scanner/enumerator.py +++ b/scapy/contrib/automotive/scanner/enumerator.py @@ -13,13 +13,12 @@ import copy from collections import defaultdict, OrderedDict from itertools import chain +from typing import NamedTuple -from scapy.compat import Any, Union, List, Optional, Iterable, \ - Dict, Tuple, Set, Callable, cast, NamedTuple, orb +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 -import scapy.libs.six as six +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, \ @@ -28,6 +27,20 @@ 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", @@ -46,8 +59,7 @@ ("resp_ts", Union[EDecimal, float])]) -@six.add_metaclass(abc.ABCMeta) -class ServiceEnumerator(AutomotiveTestCase): +class ServiceEnumerator(AutomotiveTestCase, metaclass=abc.ABCMeta): """ Base class for ServiceEnumerators of automotive diagnostic protocols """ @@ -64,10 +76,12 @@ class ServiceEnumerator(AutomotiveTestCase): '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 if six.PY2 else threading.Event, None), # type: ignore # noqa: E501 + 'stop_event': (threading.Event, None), 'debug': (bool, None), 'scan_range': ((list, tuple, range), None), - 'unittest': (bool, None) + 'unittest': (bool, None), + 'disable_tps_while_sending': (bool, None), + 'inter': ((int, float), lambda x: x >= 0), }) _supported_kwargs_doc = AutomotiveTestCase._supported_kwargs_doc + """ @@ -107,7 +121,12 @@ class ServiceEnumerator(AutomotiveTestCase): :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""" + :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 @@ -118,6 +137,7 @@ def __init__(self): 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 @@ -171,8 +191,14 @@ def _get_initial_requests(self, **kwargs): 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 six.iteritems(d["_request_iterators"]): + for k, v in d["_request_iterators"].items(): d["_request_iterators"][k] = list(v) except KeyError: pass @@ -218,10 +244,14 @@ def _get_retry_iterator(self, 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 + + return retry_entry def _get_initial_request_iterator(self, state, **kwargs): # type: (EcuState, Any) -> Iterable[Packet] @@ -256,6 +286,17 @@ def runtime_estimation(self): 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) @@ -263,6 +304,8 @@ def execute(self, socket, state, **kwargs): 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) @@ -285,13 +328,24 @@ def execute(self, socket, state, **kwargs): # log_automotive.debug("[i] Using iterator %s in state %s", it, state) - start_time = time.time() + start_time = time.monotonic() log_automotive.debug( - "Start execution of enumerator: %s", time.ctime(start_time)) + "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): @@ -306,7 +360,7 @@ def execute(self, socket, state, **kwargs): "Finished execution count of enumerator") return - if (start_time + execution_time) < time.time(): + if (start_time + execution_time) < time.monotonic(): log_automotive.debug( "[i] Finished execution time of enumerator: %s", time.ctime()) @@ -523,7 +577,7 @@ def _show_statistics(self, **kwargs): len(self.results_without_response)) + "\n" s += "Statistics per state\n" - s += make_lined_table(stats, lambda x: x, dump=True, sortx=str, + s += make_lined_table(stats, lambda *x: x, dump=True, sortx=str, sorty=str) or "" return s + "\n" @@ -646,8 +700,9 @@ def _show_negative_response_information(self, **kwargs): def _show_results_information(self, **kwargs): # type: (Any) -> str def _get_table_entry( - tup # type: _AutomotiveTestCaseScanResult + *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) @@ -689,8 +744,8 @@ def _get_label(self, response, positive_case="PR: PositiveResponse"): elif orb(bytes(response)[0]) == 0x7f: return self._get_negative_response_label(response) else: - if isinstance(positive_case, six.string_types): - return cast(str, positive_case) + if isinstance(positive_case, str): + return positive_case elif callable(positive_case): return positive_case(response) else: @@ -710,8 +765,11 @@ def supported_responses(self): return supported_resps -@six.add_metaclass(abc.ABCMeta) -class StateGeneratingServiceEnumerator(ServiceEnumerator, StateGenerator): +class StateGeneratingServiceEnumerator( + ServiceEnumerator, + StateGenerator, + metaclass=abc.ABCMeta +): def __init__(self): # type: () -> None super(StateGeneratingServiceEnumerator, self).__init__() diff --git a/scapy/contrib/automotive/scanner/executor.py b/scapy/contrib/automotive/scanner/executor.py index 89319f0cdcf..3d0ff3a5991 100644 --- a/scapy/contrib/automotive/scanner/executor.py +++ b/scapy/contrib/automotive/scanner/executor.py @@ -10,16 +10,12 @@ import time from itertools import product -from threading import Event -from scapy.compat import Any, Union, List, Optional, \ - Dict, Callable, Type, cast 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 -import scapy.libs.six as six from scapy.contrib.automotive.ecu import EcuState, EcuResponse, Ecu from scapy.contrib.automotive.scanner.configuration import \ AutomotiveTestCaseExecutorConfiguration @@ -27,9 +23,23 @@ _SocketUnion, _CleanupCallable, StateGenerator, TestCaseGenerator, \ AutomotiveTestCase +# Typing imports +from typing import ( + Any, + Union, + List, + Optional, + Dict, + Callable, + Type, + cast, + TypeVar, +) -@six.add_metaclass(abc.ABCMeta) -class AutomotiveTestCaseExecutor: +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 @@ -53,32 +63,32 @@ def _initial_ecu_state(self): def __init__( self, - socket, # type: _SocketUnion + 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 + 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 not isinstance(socket, SingleConversationSocket): - self.socket = SingleConversationSocket(socket) + 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() - self._stop_scan_event = Event() def __reduce__(self): # type: ignore f, t, d = super(AutomotiveTestCaseExecutor, self).__reduce__() # type: ignore # noqa: E501 @@ -94,10 +104,6 @@ def __reduce__(self): # type: ignore del d["reconnect_handler"] except KeyError: pass - try: - del d["_stop_scan_event"] - except KeyError: - pass return f, t, d @property @@ -147,25 +153,31 @@ def reset_target(self): 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 self.reconnect_handler: - try: + 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) + except Exception as e: + log_automotive.exception( + "Exception '%s' during socket.close", e) - log_automotive.info("Target reconnect") - socket = self.reconnect_handler() - if not isinstance(socket, SingleConversationSocket): - self.socket = SingleConversationSocket(socket) - else: - self.socket = socket + log_automotive.info("Target reconnect") + socket = self.reconnect_handler() + self.socket = socket if isinstance(socket, SingleConversationSocket) \ + else SingleConversationSocket(socket) - if self.socket.closed: + if self.socket and self.socket.closed: raise Scapy_Exception( "Socket closed even after reconnect. Stop scan!") @@ -174,7 +186,7 @@ def execute_test_case(self, test_case, kill_time=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 + Finally, the testcase is asked if a new edge or a new testcase was generated. :param test_case: A test case to be executed @@ -183,6 +195,10 @@ def execute_test_case(self, test_case, kill_time=None): :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) @@ -192,7 +208,7 @@ def execute_test_case(self, test_case, kill_time=None): test_case_kwargs = dict() if kill_time: - max_execution_time = max(int(kill_time - time.time()), 5) + 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) @@ -200,9 +216,7 @@ def execute_test_case(self, test_case, kill_time=None): 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, - stop_event=self._stop_scan_event, - **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) @@ -210,7 +224,7 @@ def execute_test_case(self, test_case, kill_time=None): self.check_new_testcases(test_case) if hasattr(test_case, "runtime_estimation"): - estimation = test_case.runtime_estimation() # type: ignore + estimation = test_case.runtime_estimation() if estimation is not None: log_automotive.debug( "[i] Test_case %s: TODO %d, " @@ -228,6 +242,10 @@ def check_new_testcases(self, 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: @@ -244,7 +262,8 @@ def validate_test_case_kwargs(self): def stop_scan(self): # type: () -> None - self._stop_scan_event.set() + self.configuration.stop_event.set() + log_automotive.debug("Internal stop event set!") def progress(self): # type: () -> float @@ -252,7 +271,7 @@ def progress(self): for tc in self.configuration.test_cases: if not hasattr(tc, "runtime_estimation"): continue - est = tc.runtime_estimation() # type: ignore + est = tc.runtime_estimation() if est is None: continue progress.append(est[2]) @@ -266,18 +285,25 @@ def scan(self, timeout=None): :param timeout: Time for execution. :return: None """ - self._stop_scan_event.clear() - kill_time = time.time() + (timeout or 0xffffffff) - log_automotive.debug("Set kill_time to %s" % time.ctime(kill_time)) - while kill_time > time.time(): + 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 <= time.time() - if terminate or self._stop_scan_event.is_set(): + 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 @@ -304,9 +330,11 @@ def scan(self, timeout=None): if isinstance(e, OSError): log_automotive.exception( "OSError occurred, closing socket") - self.socket.close() - if cast(SuperSocket, self.socket).closed and \ - self.reconnect_handler is None: + 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 @@ -340,7 +368,13 @@ def enter_state_path(self, path): 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() @@ -357,6 +391,10 @@ def enter_state(self, prev_state, next_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) @@ -367,6 +405,20 @@ def enter_state(self, prev_state, next_state): 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 @@ -387,7 +439,7 @@ def cleanup_state(self): if not callable(f): continue try: - if not f(self.socket, self.configuration): + 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: @@ -406,7 +458,11 @@ def show_testcases_status(self): 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])) + 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): diff --git a/scapy/contrib/automotive/scanner/graph.py b/scapy/contrib/automotive/scanner/graph.py index 16b40d00bed..3a3aa46cbd2 100644 --- a/scapy/contrib/automotive/scanner/graph.py +++ b/scapy/contrib/automotive/scanner/graph.py @@ -8,10 +8,20 @@ from collections import defaultdict -from scapy.compat import Union, List, Optional, Dict, Tuple, Set, TYPE_CHECKING 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: @@ -44,6 +54,9 @@ def add_edge(self, edge, transition_function=None): :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 diff --git a/scapy/contrib/automotive/scanner/staged_test_case.py b/scapy/contrib/automotive/scanner/staged_test_case.py index dc7a6361707..2544793a488 100644 --- a/scapy/contrib/automotive/scanner/staged_test_case.py +++ b/scapy/contrib/automotive/scanner/staged_test_case.py @@ -7,14 +7,23 @@ # scapy.contrib.status = library -from scapy.compat import Any, List, Optional, Dict, Callable, cast, \ - TYPE_CHECKING, Tuple 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 \ @@ -201,7 +210,7 @@ def pre_execute(self, def execute(self, socket, state, **kwargs): # type: (_SocketUnion, EcuState, Any) -> None - kwargs = self.__current_kwargs or dict() + kwargs.update(self.__current_kwargs or dict()) self.current_test_case.execute(socket, state, **kwargs) def post_execute(self, @@ -254,7 +263,7 @@ 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() # type: ignore + cur_est = self.current_test_case.runtime_estimation() if cur_est: return len(self.test_cases), \ self.__stage_index, \ diff --git a/scapy/contrib/automotive/scanner/test_case.py b/scapy/contrib/automotive/scanner/test_case.py index a4d30bd582e..854ee1ca794 100644 --- a/scapy/contrib/automotive/scanner/test_case.py +++ b/scapy/contrib/automotive/scanner/test_case.py @@ -10,16 +10,25 @@ import abc from collections import defaultdict -from scapy.compat import Any, Union, List, Optional, \ - Dict, Tuple, Set, Callable, TYPE_CHECKING from scapy.utils import make_lined_table, SingleConversationSocket -import scapy.libs.six as six 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 @@ -31,8 +40,7 @@ _TransitionTuple = Tuple[_TransitionCallable, Dict[str, Any], Optional[_CleanupCallable]] # noqa: E501 -@six.add_metaclass(abc.ABCMeta) -class AutomotiveTestCaseABC: +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 @@ -211,7 +219,7 @@ def _show_state_information(self, **kwargs): completed = [(state, self._state_completed[state]) for state in self.scanned_states] return make_lined_table( - completed, lambda tup: ("Scan state completed", tup[0], tup[1]), + completed, lambda x, y: ("Scan state completed", x, y), dump=True) or "" def show(self, dump=False, filtered=True, verbose=False): @@ -229,16 +237,14 @@ def show(self, dump=False, filtered=True, verbose=False): return None -@six.add_metaclass(abc.ABCMeta) -class TestCaseGenerator: +class TestCaseGenerator(metaclass=abc.ABCMeta): @abc.abstractmethod def get_generated_test_case(self): # type: () -> Optional[AutomotiveTestCaseABC] raise NotImplementedError() -@six.add_metaclass(abc.ABCMeta) -class StateGenerator: +class StateGenerator(metaclass=abc.ABCMeta): @abc.abstractmethod def get_new_edge(self, socket, config): diff --git a/scapy/contrib/automotive/someip.py b/scapy/contrib/automotive/someip.py index 1bb491644bc..40d43a09fd4 100644 --- a/scapy/contrib/automotive/someip.py +++ b/scapy/contrib/automotive/someip.py @@ -7,19 +7,20 @@ # 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.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): @@ -62,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), @@ -102,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: @@ -135,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""" @@ -153,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 @@ -184,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 @@ -205,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), @@ -288,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) ] @@ -304,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): @@ -420,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 @@ -429,15 +480,11 @@ 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"), @@ -449,21 +496,6 @@ class SD(_SDPacketBase): 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 @@ -482,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, @@ -491,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 e42e79db2e2..978bf6a0483 100644 --- a/scapy/contrib/automotive/uds.py +++ b/scapy/contrib/automotive/uds.py @@ -6,37 +6,43 @@ # scapy.contrib.description = Unified Diagnostic Service (UDS) # scapy.contrib.status = loads +""" +UDS +""" + import struct -import time +from collections import defaultdict -from scapy.contrib.automotive import log_automotive from scapy.fields import ByteEnumField, StrField, ConditionalField, \ BitEnumField, BitField, XByteField, FieldListField, \ XShortField, X3BytesField, XIntField, ByteField, \ ShortField, ObservableDict, XShortEnumField, XByteEnumField, StrLenField, \ - FieldLenField, XStrFixedLenField, XStrLenField -from scapy.packet import Packet, bind_layers, NoPayload + FieldLenField, XStrFixedLenField, XStrLenField, FlagsField, PacketListField, \ + PacketField +from scapy.packet import Packet, bind_layers, NoPayload, Raw from scapy.config import conf -from scapy.error import log_loading, Scapy_Exception from scapy.utils import PeriodicSenderThread from scapy.contrib.isotp import ISOTP -from scapy.compat import Dict, Union -""" -UDS -""" +# Typing imports +from typing import ( + Dict, + 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.") + # 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} +conf.debug_dissector = True + class UDS(ISOTP): services = ObservableDict( @@ -116,8 +122,8 @@ def answers(self, other): def hashret(self): # type: () -> bytes - if self.service == 0x7f: - return struct.pack('B', self.requestServiceId) + if self.service == 0x7f and len(self) >= 3: + return struct.pack('B', bytes(self)[1] & ~0x40) return struct.pack('B', self.service & ~0x40) @@ -904,6 +910,30 @@ def answers(self, other): bind_layers(UDS, UDS_WMBAPR, service=0x7D) +# ##########################DTC##################################### +class DTC(Packet): + name = 'Diagnostic Trouble Code' + dtc_descriptions = {} # Customize this dictionary for each individual ECU / OEM + + 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' @@ -953,21 +983,42 @@ 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 = [ ByteEnumField('reportType', 0, reportTypes), - ConditionalField(ByteField('DTCSeverityMask', 0), + ConditionalField(FlagsField('DTCSeverityMask', 0, 8, dtcSeverityMask), lambda pkt: pkt.reportType in [0x07, 0x08]), - ConditionalField(XByteField('DTCStatusMask', 0), + ConditionalField(FlagsField('DTCStatusMask', 0, 8, dtcStatusMask), lambda pkt: pkt.reportType in [ 0x01, 0x02, 0x07, 0x08, 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(PacketField("dtc", None, pkt_cls=DTC), lambda pkt: pkt.reportType in [0x3, 0x4, 0x6, 0x10, 0x09]), ConditionalField(ByteField('DTCSnapshotRecordNumber', 0), @@ -980,16 +1031,75 @@ class UDS_RDTCI(Packet): bind_layers(UDS, UDS_RDTCI, service=0x19) +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) # for later extension + + @staticmethod + 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 + + +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 = [ 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', @@ -1000,19 +1110,33 @@ class UDS_RDTCIPR(Packet): ConditionalField(ShortField('DTCCount', 0), lambda pkt: pkt.reportType in [0x01, 0x07, 0x11, 0x12]), - ConditionalField(StrField('DTCAndStatusRecord', b""), - 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', b""), - lambda pkt: pkt.reportType in [0x03, 0x04, 0x05, - 0x06, 0x08, 0x09, - 0x10, 0x14]) + 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 isinstance(other, UDS_RDTCI) \ - and other.reportType == self.reportType + 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) @@ -1046,9 +1170,14 @@ class UDS_RCPR(Packet): ] def answers(self, other): - return isinstance(other, UDS_RC) \ - and other.routineControlType == self.routineControlType \ - and other.routineIdentifier == self.routineIdentifier + 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) @@ -1256,15 +1385,15 @@ def _contains_data_format_identifier(packet): fmt='B'), lambda p: p.modeOfOperation != 2), ConditionalField(StrLenField('maxNumberOfBlockLength', b"", - length_from=lambda p: p.lengthFormatIdentifier), + 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'), + None, + length_of='fileSizeUncompressedOrDirInfoLength'), lambda p: p.modeOfOperation not in [1, 2, 3]), ConditionalField(StrLenField('fileSizeUncompressedOrDirInfoLength', b"", @@ -1272,8 +1401,8 @@ def _contains_data_format_identifier(packet): p.fileSizeOrDirInfoParameterLength), lambda p: p.modeOfOperation not in [1, 2, 3]), ConditionalField(StrLenField('fileSizeCompressed', b"", - length_from=lambda p: - p.fileSizeOrDirInfoParameterLength), + length_from=lambda p: + p.fileSizeOrDirInfoParameterLength), lambda p: p.modeOfOperation not in [1, 2, 3, 5]), ] @@ -1287,11 +1416,8 @@ def answers(self, other): # #########################IOCBI################################### class UDS_IOCBI(Packet): name = 'InputOutputControlByIdentifier' - dataIdentifiers = ObservableDict() fields_desc = [ - XShortEnumField('dataIdentifier', 0, dataIdentifiers), - ByteField('controlOptionRecord', 0), - StrField('controlEnableMaskRecord', b"", fmt="B") + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] @@ -1301,8 +1427,7 @@ class UDS_IOCBI(Packet): class UDS_IOCBIPR(Packet): name = 'InputOutputControlByIdentifierPositiveResponse' fields_desc = [ - XShortField('dataIdentifier', 0), - StrField('controlStatusRecord', b"", fmt="B") + XShortEnumField('dataIdentifier', 0, UDS_RDBI.dataIdentifiers), ] def answers(self, other): @@ -1331,10 +1456,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', @@ -1384,7 +1524,7 @@ def answers(self, other): 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: @@ -1393,17 +1533,3 @@ def __init__(self, sock, pkt=UDS() / UDS_TP(), interval=2): interval: interval between two packets """ PeriodicSenderThread.__init__(self, sock, pkt, interval) - - def run(self): - # type: () -> None - while not self._stopped.is_set() and not self._socket.closed: - for p in self._pkts: - try: - self._socket.sr1(p, timeout=0.3, verbose=False) - except (OSError, ValueError, Scapy_Exception) as e: - log_automotive.exception( - "Exception in TesterPresentSender: %s", e) - break - time.sleep(self._interval) - if self._stopped.is_set() or self._socket.closed: - break diff --git a/scapy/contrib/automotive/uds_ecu_states.py b/scapy/contrib/automotive/uds_ecu_states.py index 9d29b9cbc9d..47b81f91211 100644 --- a/scapy/contrib/automotive/uds_ecu_states.py +++ b/scapy/contrib/automotive/uds_ecu_states.py @@ -5,7 +5,6 @@ # 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 @@ -22,6 +21,10 @@ 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) @@ -37,6 +40,10 @@ def UDS_SAPR_modify_ecu_state(self, req, state): 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) diff --git a/scapy/contrib/automotive/uds_logging.py b/scapy/contrib/automotive/uds_logging.py index cad7827a491..bb791c9ba32 100644 --- a/scapy/contrib/automotive/uds_logging.py +++ b/scapy/contrib/automotive/uds_logging.py @@ -14,9 +14,13 @@ 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.compat import Tuple, Any 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): diff --git a/scapy/contrib/automotive/uds_scan.py b/scapy/contrib/automotive/uds_scan.py index d597c6af1a8..6271b051868 100644 --- a/scapy/contrib/automotive/uds_scan.py +++ b/scapy/contrib/automotive/uds_scan.py @@ -2,51 +2,53 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Nils Weiss - -# scapy.contrib.description = UDS AutomotiveTestCaseExecutor -# scapy.contrib.status = loads - -import struct -import random -import time -import itertools import copy import inspect - +import itertools +import logging +import random +import struct +import time +from abc import ABC from collections import defaultdict -from typing import Sequence - -from scapy.compat import Dict, Optional, List, Type, Any, Iterable, \ - cast, Union, NamedTuple, orb, Set +# 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.packet import Raw, Packet -import scapy.libs.six as six -from scapy.error import Scapy_Exception -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 - 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.test_case import AutomotiveTestCaseABC, \ - _SocketUnion, _TransitionTuple, StateGenerator -from scapy.contrib.automotive.scanner.configuration import \ - AutomotiveTestCaseExecutorConfiguration # noqa: E501 +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.executor import AutomotiveTestCaseExecutor # 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 -if six.PY34: - from abc import ABC -else: - from abc import ABCMeta - - ABC = ABCMeta('ABC', (), {}) # type: ignore +# scapy.contrib.description = UDS AutomotiveTestCaseExecutor +# scapy.contrib.status = loads # Definition outside the class UDS_RMBASequentialEnumerator # to allow pickling @@ -86,7 +88,9 @@ class UDS_DSCEnumerator(UDS_Enumerator, StateGeneratingServiceEnumerator): _supported_kwargs = copy.copy(ServiceEnumerator._supported_kwargs) _supported_kwargs.update({ 'delay_state_change': (int, lambda x: x >= 0), - 'overwrite_timeout': (bool, None) + '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) @@ -105,7 +109,18 @@ class UDS_DSCEnumerator(UDS_Enumerator, StateGeneratingServiceEnumerator): unit-test scenarios, this value should be set to False, in order to use the timeout specified by the 'timeout' - argument.""" + 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] @@ -158,10 +173,44 @@ def get_new_edge(self, 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 @@ -177,9 +226,30 @@ def enter_state_with_tp(sock, # type: _SocketUnion delay = conf[UDS_DSCEnumerator.__name__]["delay_state_change"] except KeyError: delay = 5 - time.sleep(delay) + + 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 @@ -214,7 +284,7 @@ def enter(socket, # type: _SocketUnion return True UDS_TPEnumerator.cleanup(socket, configuration) - configuration["tps"] = UDS_TesterPresentSender(socket) + configuration["tps"] = UDS_TesterPresentSender(socket, interval=3) configuration["tps"].start() return True @@ -224,8 +294,9 @@ def cleanup(_, configuration): try: configuration["tps"].stop() configuration["tps"] = None - except (AttributeError, KeyError) as e: - log_automotive.debug("Cleanup TP-Sender Error: %s", e) + except (AttributeError, KeyError): + pass + # log_automotive.debug("Cleanup TP-Sender Error: %s", e) return True def get_transition_function(self, socket, edge): @@ -284,7 +355,7 @@ def _get_initial_requests(self, **kwargs): def _get_table_entry_y(self, tup): # type: (_AutomotiveTestCaseScanResult) -> str resp = tup[2] - if resp is not None: + if resp is not None and resp.service != 0x7f: return "0x%02x %s: %s" % ( tup[1].periodicDataIdentifier, tup[1].sprintf("%UDS_RDBPI.periodicDataIdentifier%"), @@ -298,16 +369,35 @@ def _get_table_entry_y(self, tup): 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)) - return (UDS(service=x) for x in scan_range) + 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 @@ -328,7 +418,8 @@ def _evaluate_response(self, def _get_table_entry_y(self, tup): # type: (_AutomotiveTestCaseScanResult) -> str - return "0x%02x: %s" % (tup[1].service, tup[1].sprintf("%UDS.service%")) + return "0x%02x-%d: %s" % ( + tup[1].service, len(tup[1]), tup[1].sprintf("%UDS.service%")) class UDS_RDBIEnumerator(UDS_Enumerator): @@ -549,7 +640,7 @@ def pre_execute(self, socket, state, global_configuration): # a required time delay not expired could have been received # on the previous attempt if not global_configuration.unittest: - time.sleep(11) + global_configuration.stop_event.wait(11) def _evaluate_retry(self, state, # type: EcuState @@ -675,7 +766,9 @@ def get_security_access(self, sock, level=1, seed_pkt=None): return False if not any(seed_pkt.securitySeed): - return False + 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: @@ -702,10 +795,7 @@ def get_security_access(self, sock, level=1, seed_pkt=None): def transition_function(self, sock, _, kwargs): # type: (_SocketUnion, AutomotiveTestCaseExecutorConfiguration, Dict[str, Any]) -> bool # noqa: E501 - if six.PY3: - spec = inspect.getfullargspec(self.get_security_access) - else: - spec = inspect.getargspec(self.get_security_access) + 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) @@ -845,8 +935,8 @@ def _get_table_entry_y(self, tup): resp = tup[2] if resp is not None: return "0x%04x: %s" % \ - (tup[1].dataIdentifier, - resp.controlStatusRecord) + (tup[1].dataIdentifier, + repr(resp.payload)) else: return "0x%04x: No response" % tup[1].dataIdentifier @@ -1021,7 +1111,7 @@ def __request_to_pois(self, req, resp): msl = req.memorySizeLen mal = req.memoryAddressLen - if (resp is None or resp.service == 0x7f) and size > 16: + if (resp is None or resp.service == 0x7f) and size > 1: size = size // 2 return [ @@ -1150,8 +1240,7 @@ def _random_memory_addr_pkt(addr_len=None): # noqa: E501 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)) + addr = random.randint(0, 2 ** (8 * pkt.memoryAddressLen) - 1) & (0xffffffff << (4 * pkt.memoryAddressLen)) # noqa: E501 UDS_RMBARandomEnumerator.set_addr(pkt, addr) return pkt @@ -1225,3 +1314,27 @@ def default_test_case_clss(self): 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/definitions.py b/scapy/contrib/automotive/volkswagen/definitions.py index f9b09dacf6b..17854a98289 100644 --- a/scapy/contrib/automotive/volkswagen/definitions.py +++ b/scapy/contrib/automotive/volkswagen/definitions.py @@ -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/scanner.py b/scapy/contrib/automotive/xcp/scanner.py index d2968783b62..af952004848 100644 --- a/scapy/contrib/automotive/xcp/scanner.py +++ b/scapy/contrib/automotive/xcp/scanner.py @@ -7,7 +7,6 @@ # scapy.contrib.status = loads import logging from collections import namedtuple -from scapy.compat import Optional, List, Type, Iterator from scapy.config import conf from scapy.contrib.automotive import log_automotive @@ -18,6 +17,14 @@ 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') diff --git a/scapy/contrib/bfd.py b/scapy/contrib/bfd.py index 06565a03ab9..a06a80bd9c2 100644 --- a/scapy/contrib/bfd.py +++ b/scapy/contrib/bfd.py @@ -10,15 +10,32 @@ # scapy.contrib.description = BFD # scapy.contrib.status = loads + from scapy.packet import Packet, bind_layers, bind_bottom_up -from scapy.fields import BitField, BitEnumField, FlagsField, ByteField +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", - } +_sta_names = { + 0: "AdminDown", + 1: "Down", + 2: "Init", + 3: "Up", +} + # https://www.iana.org/assignments/bfd-parameters/bfd-parameters.xhtml _diagnostics = { @@ -35,20 +52,88 @@ } +# 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", 0x00, 6, "MDACFP"), + FlagsField("flags", 0, 6, "MDACFP"), ByteField("detect_mult", 3), - ByteField("len", 24), + 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)] + BitField("echo_rx_interval", 1000000000, 32), + ConditionalField( + PacketField("optional_auth", None, OptionalAuth), + lambda pkt: pkt.flags.names[2] == "A", + ), + ] def mysummary(self): return self.sprintf( @@ -58,10 +143,12 @@ def mysummary(self): ) -for _bfd_port in [3784, # single-hop BFD - 4784, # multi-hop BFD - 6784, # BFD for LAG a.k.a micro-BFD - 7784]: # seamless BFD +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 4ba4dc60297..0060e5f9f32 100644 --- a/scapy/contrib/bgp.py +++ b/scapy/contrib/bgp.py @@ -9,7 +9,6 @@ BGP (Border Gateway Protocol). """ -from __future__ import absolute_import import struct import re import socket @@ -28,7 +27,6 @@ from scapy.config import conf, ConfClass from scapy.compat import orb, chb from scapy.error import log_runtime -import scapy.libs.six as six # @@ -463,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) @@ -522,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: @@ -635,7 +646,7 @@ 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. """ @@ -757,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, ) ] @@ -813,7 +825,8 @@ class BGPCapORF(BGPCapability): "orf", [], BGPCapORFBlock, - length_from=lambda p: p.length + length_from=lambda p: p.length, + max_count=20000, ) ] @@ -1044,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 @@ -1081,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) } @@ -1741,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: @@ -1907,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", [], BGPNLRI_IPv6)] + MPReachNLRIPacketListField("nlri", [], BGPNLRI_IPv6, + max_count=20000)] def post_build(self, p, pay): if self.nlri is None: @@ -1994,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 @@ -2025,7 +2071,8 @@ class BGPPAAS4Aggregator(Packet): 0x0F: "BGPPAMPUnreachNLRI", 0x10: "BGPPAExtComms", 0x11: "BGPPAAS4Path", - 0x19: "BGPPAIPv6AddressSpecificExtComm" + 0x19: "BGPPAIPv6AddressSpecificExtComm", + 0x20: "BGPPALargeCommunity" } @@ -2042,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) @@ -2050,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) @@ -2183,7 +2232,8 @@ class BGPUpdate(BGP): BGPPathAttr, length_from=lambda p: p.path_attr_len ), - BGPNLRIPacketListField("nlri", [], "IPv4") + BGPNLRIPacketListField("nlri", [], "IPv4", + max_count=20000) ] def post_build(self, p, pay): @@ -2541,6 +2591,7 @@ class BGPORF(Packet): [], Packet, length_from=lambda p: p.orf_len, + max_count=20000, ) ] diff --git a/scapy/contrib/cansocket.py b/scapy/contrib/cansocket.py index 27cb76d6343..a50d2352c76 100644 --- a/scapy/contrib/cansocket.py +++ b/scapy/contrib/cansocket.py @@ -13,7 +13,6 @@ from scapy.error import log_loading from scapy.consts import LINUX from scapy.config import conf -import scapy.libs.six as six PYTHON_CAN = False @@ -31,7 +30,7 @@ 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 six.PY3 and not conf.use_pypy: +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 diff --git a/scapy/contrib/cansocket_native.py b/scapy/contrib/cansocket_native.py index 3cd16f2230b..49efacd457c 100644 --- a/scapy/contrib/cansocket_native.py +++ b/scapy/contrib/cansocket_native.py @@ -15,12 +15,22 @@ 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.error import Scapy_Exception, warning, log_runtime from scapy.packet import Packet -from scapy.layers.can import CAN, CAN_MTU -from scapy.arch.linux import get_last_packet_timestamp -from scapy.compat import List, Dict, Type, Any, Optional, Tuple, raw, cast +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"} @@ -45,6 +55,7 @@ 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] ): @@ -56,6 +67,8 @@ def __init__(self, "the correct one to achieve compatibility with python-can" "/PythonCANSocket. \n'bustype=socketcan'") + self.MTU = CAN_MTU + self.fd = fd self.basecls = basecls self.channel = conf.contribs['NativeCANSocket']['channel'] if \ channel is None else channel @@ -71,6 +84,31 @@ def __init__(self, "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, @@ -94,8 +132,9 @@ 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 = self.ins.recv(x) + pkt, _, ts = self._recv_raw(self.ins, self.MTU) except BlockingIOError: # noqa: F821 warning("Captured no data, socket in non-blocking mode.") except socket.timeout: @@ -106,13 +145,22 @@ def recv_raw(self, x=CAN_MTU): # 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'] and pkt is not None: - pkt = struct.pack("I12s", pkt)) + if not conf.contribs['CAN']['swap-bytes'] and pkt: + pack_fmt = " int + if x is None: + return 0 + try: x.sent_time = time.time() except AttributeError: @@ -122,8 +170,11 @@ def send(self, x): # required by the underlying Linux SocketCAN frame format bs = raw(x) if not conf.contribs['CAN']['swap-bytes']: - bs = bs + b'\x00' * (CAN_MTU - len(bs)) - bs = struct.pack("I12s", bs)) + pack_fmt = " None - self.msg = msg - self.count = count - - def __eq__(self, other): - # type: (Any) -> bool - if not isinstance(other, PriotizedCanMessage): - return False - return self.msg.timestamp == other.msg.timestamp and \ - self.count == other.count - - def __lt__(self, other): - # type: (Any) -> bool - if not isinstance(other, PriotizedCanMessage): - return False - return self.msg.timestamp < other.msg.timestamp or \ - (self.msg.timestamp == other.msg.timestamp and - self.count < other.count) - - def __le__(self, other): - # type: (Any) -> bool - return self == other or self < other - - def __gt__(self, other): - # type: (Any) -> bool - return not self <= other - - def __ge__(self, other): - # type: (Any) -> bool - return not self < other - - -class SocketMapper: +class SocketMapper(object): """Internal Helper class to map a python-can bus object to a list of SocketWrapper instances """ @@ -94,19 +62,23 @@ def mux(self): object. If a message is received, this message gets forwarded to all receive queues of the SocketWrapper objects. """ + msgs = [] while True: - prio_count = 0 try: msg = self.bus.recv(timeout=0) if msg is None: - return - for sock in self.sockets: - if sock._matches_filters(msg): - prio_count += 1 - sock.rx_queue.put(PriotizedCanMessage(msg, prio_count)) + break + else: + msgs.append(msg) except Exception as e: warning("[MUX] python-can exception caught: %s" % e) + 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""" @@ -114,9 +86,10 @@ 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, prio=0): - # type: (SocketWrapper, can_Message, int) -> None + 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 @@ -128,7 +101,6 @@ def internal_send(self, sender, msg, prio=0): :param sender: SocketWrapper which initiated a send of a CAN message :param msg: CAN message to be sent - :param prio: Priority count for internal heapq """ if sender.name is None: raise TypeError("SocketWrapper.name should never be None") @@ -143,7 +115,8 @@ def internal_send(self, sender, msg, prio=0): if not sock._matches_filters(msg): continue - sock.rx_queue.put(PriotizedCanMessage(msg, prio)) + with sock.lock: + sock.rx_queue.append(msg) except KeyError: warning("[SND] Socket %s not found in pool" % sender.name) except can_CanError as e: @@ -154,9 +127,15 @@ def multiplex_rx_packets(self): """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 with self.pool_mutex: - for _, t in self.pool.items(): + for t in self.pool.values(): t.mux() + self.last_call = time.monotonic() def register(self, socket, *args, **kwargs): # type: (SocketWrapper, Tuple[Any, ...], Dict[str, Any]) -> None @@ -172,8 +151,12 @@ def register(self, socket, *args, **kwargs): :param args: Arguments for the python-can Bus object :param kwargs: Keyword arguments for the python-can Bus object """ - k = str(kwargs.get("bustype", "unknown_bustype")) + "_" + \ - str(kwargs.get("channel", "unknown_channel")) + 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] @@ -227,9 +210,9 @@ def __init__(self, *args, **kwargs): :param kwargs: Keyword arguments for the python-can Bus object """ super(SocketWrapper, self).__init__(*args, **kwargs) - self.rx_queue = queue.PriorityQueue() # type: queue.PriorityQueue[PriotizedCanMessage] # noqa: E501 + self.lock = threading.Lock() + self.rx_queue = deque() # type: deque[can_Message] self.name = None # type: Optional[str] - self.prio_counter = 0 SocketsPool.register(self, *args, **kwargs) def _recv_internal(self, timeout): @@ -243,13 +226,19 @@ def _recv_internal(self, timeout): :return: Returns a tuple of either a can_Message or None and a bool to indicate if filtering was already applied. """ - SocketsPool.multiplex_rx_packets() - try: - pm = self.rx_queue.get(block=True, timeout=timeout) - return pm.msg, True - except queue.Empty: + 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): # type: (can_Message, Optional[int]) -> None """Send function, following the ``can_BusABC`` interface of python-can. @@ -257,8 +246,7 @@ def send(self, msg, timeout=None): :param msg: Message to be sent. :param timeout: Not used. """ - self.prio_counter += 1 - SocketsPool.internal_send(self, msg, self.prio_counter) + SocketsPool.internal_send(self, msg) def shutdown(self): # type: () -> None @@ -266,6 +254,7 @@ def shutdown(self): python-can. """ SocketsPool.unregister(self) + super().shutdown() class PythonCANSocket(SuperSocket): @@ -284,13 +273,7 @@ class PythonCANSocket(SuperSocket): def __init__(self, **kwargs): # type: (Dict[str, Any]) -> None - - self.basecls = None # type: Optional[Type[Packet]] - try: - self.basecls = cast(Type[Packet], kwargs.pop("basecls")) - except KeyError: - self.basecls = CAN - + self.basecls = cast(Optional[Type[Packet]], kwargs.pop("basecls", CAN)) self.can_iface = SocketWrapper(**kwargs) def recv_raw(self, x=0xffff): @@ -304,18 +287,23 @@ 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 = msg.timestamp @@ -334,9 +322,19 @@ 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) """ + 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) + SocketsPool.multiplex_rx_packets() - return [s for s in sockets if isinstance(s, PythonCANSocket) and - not s.can_iface.rx_queue.empty()] + return cast(List[SuperSocket], ready_sockets) def close(self): # type: () -> None diff --git a/scapy/contrib/cdp.py b/scapy/contrib/cdp.py index 7998248f5e3..030b3b0772d 100644 --- a/scapy/contrib/cdp.py +++ b/scapy/contrib/cdp.py @@ -12,7 +12,6 @@ Cisco Discovery Protocol (CDP) extension for Scapy """ -from __future__ import absolute_import import struct from scapy.packet import Packet, bind_layers @@ -20,7 +19,9 @@ ByteEnumField, ByteField, FieldLenField, + FieldListField, FlagsField, + IntField, IP6Field, IPField, OUIField, @@ -64,8 +65,8 @@ # 0x0015: "CDPMsgSystemOID", 0x0016: "CDPMsgMgmtAddr", # 0x0017: "CDPMsgLocation", - 0x0019: "CDPMsgUnknown19", - # 0x001a: "CDPPowerAvailable" + 0x0019: "CDPMsgPowerRequest", + 0x001a: "CDPMsgPowerAvailable" } _cdp_tlv_types = {0x0001: "Device ID", @@ -92,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"} @@ -249,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): @@ -352,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/coap.py b/scapy/contrib/coap.py index a6f710f1b59..1c8eb3d7b14 100644 --- a/scapy/contrib/coap.py +++ b/scapy/contrib/coap.py @@ -107,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 @@ -120,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/diameter.py b/scapy/contrib/diameter.py index a53eee3c810..b5110709e08 100644 --- a/scapy/contrib/diameter.py +++ b/scapy/contrib/diameter.py @@ -33,7 +33,6 @@ XByteField, XIntField from scapy.layers.inet import TCP from scapy.layers.sctp import SCTPChunkData -import scapy.libs.six as six from scapy.compat import chb, orb, raw, bytes_hex, plain_str from scapy.error import warning from scapy.utils import inet_ntoa, inet_aton @@ -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,7 +4780,7 @@ 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 diff --git a/scapy/contrib/dtp.py b/scapy/contrib/dtp.py index dd4364c0e0a..603d16e7a34 100644 --- a/scapy/contrib/dtp.py +++ b/scapy/contrib/dtp.py @@ -17,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 7cd41b05ec7..554f8b3bdb6 100644 --- a/scapy/contrib/eddystone.py +++ b/scapy/contrib/eddystone.py @@ -24,7 +24,6 @@ StrFixedLenField, ShortField, FixedPointField, ByteEnumField from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \ EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper -import scapy.libs.six as six from scapy.packet import bind_layers, Packet EDDYSTONE_UUID = 0xfeaa @@ -94,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 0dd188ec09d..6bfb1b5aa78 100644 --- a/scapy/contrib/eigrp.py +++ b/scapy/contrib/eigrp.py @@ -25,7 +25,6 @@ http://trac.secdev.org/scapy/ticket/18 - IOS / EIGRP Version Representation FIX by Dirk Loss """ -from __future__ import absolute_import import socket import struct diff --git a/scapy/contrib/enipTCP.py b/scapy/contrib/enipTCP.py index c17686537a2..e35d2e27f67 100644 --- a/scapy/contrib/enipTCP.py +++ b/scapy/contrib/enipTCP.py @@ -2,6 +2,7 @@ # This file is part of Scapy # 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 @@ -18,10 +19,11 @@ 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 @@ -43,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" } @@ -57,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), ] @@ -66,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), ] - - -class BaseSendPacket(Packet): - """ Abstract Class""" - fields_desc = [ - LEIntField("interfaceHandle", 0), - LEShortField("timeout", 0), - PacketField("encapsulatedPacket", None, EncapsulatedPacket), - ] +# Unknown command (0x0001) -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" - - -class ENIPSendRRData(BaseSendPacket): - """Send RR Data Command Field""" - name = "ENIPSendRRData" +# List services (0x0004) -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 = [ @@ -165,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" @@ -175,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): @@ -217,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 index 69c3310cc32..3ef2d6157fc 100644 --- a/scapy/contrib/erspan.py +++ b/scapy/contrib/erspan.py @@ -4,6 +4,8 @@ """ ERSPAN - Encapsulated Remote SPAN + +https://datatracker.ietf.org/doc/html/draft-foschiano-erspan-03 """ # scapy.contrib.description = ERSPAN - Encapsulated Remote SPAN @@ -19,16 +21,24 @@ class ERSPAN(Packet): """ - A generic ERSPAN packet, pointing by default to ERSPAN II + 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 Packet.dispatch_hook(cls, _pkt, *args, **kargs) + return cls class ERSPAN_I(ERSPAN): @@ -40,7 +50,7 @@ class ERSPAN_I(ERSPAN): class ERSPAN_II(ERSPAN): name = "ERSPAN II" match_subclass = True - fields_desc = [BitField("ver", 0, 4), + fields_desc = [BitField("ver", 1, 4), BitField("vlan", 0, 12), BitField("cos", 0, 3), BitField("en", 0, 2), diff --git a/scapy/contrib/ethercat.py b/scapy/contrib/ethercat.py index 6257d64879b..2a8d63db981 100644 --- a/scapy/contrib/ethercat.py +++ b/scapy/contrib/ethercat.py @@ -44,7 +44,6 @@ from scapy.fields import BitField, ByteField, LEShortField, FieldListField, \ LEIntField, FieldLenField, _EnumField, EnumField from scapy.layers.l2 import Ether, Dot1Q -import scapy.libs.six as six from scapy.packet import bind_layers, Packet, Padding ''' @@ -252,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/geneve.py b/scapy/contrib/geneve.py index 77ce847a5ed..6515f299abb 100644 --- a/scapy/contrib/geneve.py +++ b/scapy/contrib/geneve.py @@ -9,7 +9,7 @@ """ Geneve: Generic Network Virtualization Encapsulation -draft-ietf-nvo3-geneve-16 +https://datatracker.ietf.org/doc/html/rfc8926 """ import struct @@ -19,7 +19,6 @@ 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 CLASS_IDS = {0x0100: "Linux", 0x0101: "Open vSwitch", @@ -42,12 +41,15 @@ class GeneveOptions(Packet): XByteField("type", 0x00), BitField("reserved", 0, 3), BitField("length", None, 5), - StrLenField('data', '', length_from=lambda x:x.length * 4)] + 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", tmp_len) + p[4:] + p = p[:3] + struct.pack("!B", (p[3] & 0x3) | (tmp_len & 0x1f)) + p[4:] return p + pay @@ -61,12 +63,13 @@ class GENEVE(Packet): XShortEnumField("proto", 0x0000, ETHER_TYPES), X3BytesField("vni", 0), XByteField("reserved2", 0x00), - PacketListField("options", [], GeneveOptions, length_from=lambda pkt:pkt.optionlen * 4)] + PacketListField("options", [], GeneveOptions, + length_from=lambda pkt: pkt.optionlen * 4)] def post_build(self, p, pay): if self.optionlen is None: tmp_len = (len(p) - 8) // 4 - p = chb(tmp_len & 0x2f | orb(p[0]) & 0xc0) + p[1:] + p = struct.pack("!B", (p[0] & 0xc0) | (tmp_len & 0x3f)) + p[1:] return p + pay def answers(self, other): diff --git a/scapy/contrib/gtp.py b/scapy/contrib/gtp.py index faaffba1a95..d4ad4d57493 100644 --- a/scapy/contrib/gtp.py +++ b/scapy/contrib/gtp.py @@ -17,7 +17,6 @@ Some IEs: 3GPP TS 24.008 """ -from __future__ import absolute_import import struct from scapy.compat import chb, orb, bytes_encode @@ -46,6 +45,7 @@ from scapy.layers.inet import IP, UDP from scapy.layers.inet6 import IPv6, IP6Field from scapy.layers.ppp import PPP +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 @@ -209,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" @@ -258,7 +276,10 @@ class GTPHeader(Packet): 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 @@ -449,7 +470,7 @@ def post_build(self, p, pay): 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 - 2) + p[3:] + p = p[:1] + struct.pack("!H", tmp_len - 4) + p[3:] return p + pay @@ -859,7 +880,7 @@ class IE_EvolvedAllocationRetentionPriority(IE_Base): 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"), @@ -1023,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) diff --git a/scapy/contrib/gtp_v2.py b/scapy/contrib/gtp_v2.py index b788f3b86b4..d89b35be358 100755 --- a/scapy/contrib/gtp_v2.py +++ b/scapy/contrib/gtp_v2.py @@ -591,8 +591,7 @@ class IE_FQDN(gtp.IE_Base): ShortField("length", None), BitField("CR_flag", 0, 4), BitField("instance", 0, 4), - ByteField("fqdn_tr_bit", 0), - StrLenField("fqdn", "", length_from=lambda x: x.length - 1)] + gtp.FQDNField("fqdn", b"", length_from=lambda x: x.length)] class IE_NotImplementedTLV(gtp.IE_Base): @@ -1360,11 +1359,14 @@ class IE_ChargingID(gtp.IE_Base): class IE_ChargingCharacteristics(gtp.IE_Base): 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): 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 a6db44447fc..d408815a38d 100644 --- a/scapy/contrib/homeplugav.py +++ b/scapy/contrib/homeplugav.py @@ -14,7 +14,6 @@ Key (type value) : Description """ -from __future__ import absolute_import import struct from scapy.packet import Packet, bind_layers @@ -99,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", @@ -642,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('= 0) # noqa: E501 + 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): @@ -206,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): @@ -332,14 +337,14 @@ def any2i(self, pkt, x): """ if isinstance(x, type(None)): return x - if isinstance(x, six.integer_types): + 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' @@ -632,8 +637,7 @@ def _compute_value(self, pkt): ############################################################################### -@six.add_metaclass(abc.ABCMeta) -class HPackStringsInterface(Sized): # type: ignore +class HPackStringsInterface(Sized, metaclass=abc.ABCMeta): # type: ignore @abc.abstractmethod def __str__(self): pass @@ -2049,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:] @@ -2407,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): @@ -2426,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): @@ -2614,7 +2618,7 @@ def _parse_header_line(self, line): 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 @@ -2660,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 af61cbe3d40..0326f915ef3 100644 --- a/scapy/contrib/ibeacon.py +++ b/scapy/contrib/ibeacon.py @@ -102,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 3ff05cf0884..393fa959341 100644 --- a/scapy/contrib/icmp_extensions.py +++ b/scapy/contrib/icmp_extensions.py @@ -2,188 +2,29 @@ # This file is part of Scapy # See https://scapy.net/ for more information -# scapy.contrib.description = ICMP Extensions -# scapy.contrib.status = loads - -from __future__ import absolute_import -import struct - -import scapy -from scapy.compat import chb -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.libs.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] + chb(ck >> 8) + chb(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, **kwargs): - 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, **kwargs) - - -# 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) +# 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 b71368b885f..ac93f2aebb9 100644 --- a/scapy/contrib/ife.py +++ b/scapy/contrib/ife.py @@ -59,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 21fc061720e..f01fa637c89 100644 --- a/scapy/contrib/igmp.py +++ b/scapy/contrib/igmp.py @@ -5,7 +5,6 @@ # 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 110f5798cef..620d389c133 100644 --- a/scapy/contrib/igmpv3.py +++ b/scapy/contrib/igmpv3.py @@ -5,7 +5,6 @@ # 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 diff --git a/scapy/contrib/ikev2.py b/scapy/contrib/ikev2.py index b0e52a0775a..b1ffd8cdc17 100644 --- a/scapy/contrib/ikev2.py +++ b/scapy/contrib/ikev2.py @@ -2,95 +2,173 @@ # This file is part of Scapy # See https://scapy.net/ for more information -# scapy.contrib.description = Internet Key Exchange v2 (IKEv2) +""" +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", @@ -128,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", @@ -177,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 = { @@ -201,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", @@ -347,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): @@ -438,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), ] @@ -567,233 +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 + 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), + 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 + 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), + 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 + 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 + 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 2d39a98c8c4..d277a27b568 100644 --- a/scapy/contrib/isis.py +++ b/scapy/contrib/isis.py @@ -42,7 +42,6 @@ """ -from __future__ import absolute_import import struct import random diff --git a/scapy/contrib/isotp/__init__.py b/scapy/contrib/isotp/__init__.py index 7fb0f8b8f51..142b381ed27 100644 --- a/scapy/contrib/isotp/__init__.py +++ b/scapy/contrib/isotp/__init__.py @@ -9,19 +9,21 @@ import logging from scapy.consts import LINUX -import scapy.libs.six as six 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 + 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", "ISOTPSoftSocket", "ISOTPSession", + "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"] @@ -30,7 +32,7 @@ log_isotp = logging.getLogger("scapy.contrib.isotp") log_isotp.setLevel(logging.INFO) -if six.PY3 and LINUX: +if LINUX: try: if conf.contribs['ISOTP']['use-can-isotp-kernel-module']: USE_CAN_ISOTP_KERNEL_MODULE = True diff --git a/scapy/contrib/isotp/isotp_native_socket.py b/scapy/contrib/isotp/isotp_native_socket.py index 6d7e747c4e3..76f4332ef15 100644 --- a/scapy/contrib/isotp/isotp_native_socket.py +++ b/scapy/contrib/isotp/isotp_native_socket.py @@ -7,21 +7,28 @@ # scapy.contrib.status = library import ctypes -from ctypes.util import find_library -import struct import socket +import struct +from ctypes.util import find_library +# Typing imports +from typing import ( + Any, + Optional, + Union, + Tuple, + Type, + cast, +) -from scapy.compat import 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.packet import Packet -import scapy.libs.six as six +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 -from scapy.data import SO_TIMESTAMPNS -from scapy.config import conf -from scapy.arch.linux import get_last_packet_timestamp, SIOCGIFINDEX -from scapy.contrib.isotp.isotp_packet import ISOTP -from scapy.layers.can import CAN_MTU, CAN_MAX_DLEN LIBC = ctypes.cdll.LoadLibrary(find_library("c")) # type: ignore @@ -58,9 +65,15 @@ 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 @@ -223,21 +236,21 @@ def __get_sock_ifreq(self, sock, iface): raise Scapy_Exception(m) return ifr - def __bind_socket(self, sock, iface, sid, did): + 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 sid > 0x7ff: - sid = sid | socket.CAN_EFF_FLAG - if did > 0x7ff: - did = did | socket.CAN_EFF_FLAG + 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(did), - ctypes.c_uint32(sid)))) + addr_info(tp(ctypes.c_uint32(rx_id), + ctypes.c_uint32(tx_id)))) error = LIBC.bind(socket_id, ctypes.byref(addr), ctypes.sizeof(addr)) @@ -290,11 +303,13 @@ def __init__(self, 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, six.string_types): + 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 @@ -308,40 +323,72 @@ def __init__(self, raise Scapy_Exception("Provide a string or a CANSocket " "object as iface parameter") - self.iface = cast(str, iface) or conf.contribs['NativeCANSocket']['iface'] # noqa: E501 - self.can_socket = socket.socket(socket.PF_CAN, socket.SOCK_DGRAM, - CAN_ISOTP) - self.__set_option_flags(self.can_socket, - ext_address, - rx_ext_address, - listen_only, - padding, - frame_txtime) - + 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.can_socket.setsockopt(SOL_CAN_ISOTP, - CAN_ISOTP_RECV_FC, - self.__build_can_isotp_fc_options( - stmin=stmin, bs=bs)) - self.can_socket.setsockopt(SOL_CAN_ISOTP, - CAN_ISOTP_LL_OPTS, - self.__build_can_isotp_ll_options()) - self.can_socket.setsockopt( + 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(self.can_socket, self.iface, tx_id, rx_id) - self.ins = self.can_socket - self.outs = self.can_socket - if basecls is None: - log_isotp.warning('Provide a basecls ') - self.basecls = basecls + 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 @@ -365,17 +412,23 @@ def recv_raw(self, x=0xffff): "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 ts is None: + if pkt and ts is None: ts = get_last_packet_timestamp(self.ins) return self.basecls, pkt, ts - def recv(self, x=0xffff): - # type: (int) -> Optional[Packet] - msg = SuperSocket.recv(self, x) + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = SuperSocket.recv(self, x, **kwargs) if msg is None: return msg diff --git a/scapy/contrib/isotp/isotp_packet.py b/scapy/contrib/isotp/isotp_packet.py index ec346bb1f49..3da14956dda 100644 --- a/scapy/contrib/isotp/isotp_packet.py +++ b/scapy/contrib/isotp/isotp_packet.py @@ -6,17 +6,27 @@ # scapy.contrib.description = ISO-TP (ISO 15765-2) Packet Definitions # scapy.contrib.status = library -import struct import logging +import struct +# Typing imports +from typing import ( + Optional, + List, + Tuple, + Any, + Type, + cast, +) -from scapy.compat import Optional, List, Tuple, Any, Type -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 +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") @@ -85,11 +95,22 @@ def fragment(self, *args, **kargs): """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 """ - data_bytes_in_frame = 7 + + 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 = 6 + 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") @@ -101,10 +122,10 @@ def fragment(self, *args, **kargs): frame_data = struct.pack('B', self.rx_ext_address) + frame_data if self.rx_id is None or self.rx_id <= 0x7ff: - pkt = CAN(identifier=self.rx_id, data=frame_data) + pkt = pkt_cls(identifier=self.rx_id, data=frame_data) else: - pkt = CAN(identifier=self.rx_id, flags="extended", - data=frame_data) + pkt = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_data) return [pkt] # Construct the first frame @@ -114,13 +135,13 @@ def fragment(self, *args, **kargs): 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 = 8 - len(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 = CAN(identifier=self.rx_id, data=frame_header + frame_data) + frame = pkt_cls(identifier=self.rx_id, data=frame_header + frame_data) else: - frame = CAN(identifier=self.rx_id, flags="extended", - data=frame_header + frame_data) + frame = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_header + frame_data) # Construct consecutive frames n = 1 @@ -135,12 +156,12 @@ def fragment(self, *args, **kargs): 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 = CAN(identifier=self.rx_id, data=frame_header + frame_data) # noqa: E501 + pkt = pkt_cls(identifier=self.rx_id, data=frame_header + frame_data) # noqa: E501 else: - pkt = CAN(identifier=self.rx_id, flags="extended", - data=frame_header + frame_data) + pkt = pkt_cls(identifier=self.rx_id, flags="extended", + data=frame_header + frame_data) pkts.append(pkt) - return pkts + return cast(List[Packet], pkts) @staticmethod def defragment(can_frames, use_extended_addressing=None): @@ -208,6 +229,10 @@ def post_build(self, pkt, pay): """ 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): @@ -218,17 +243,65 @@ def guess_payload_class(self, payload): :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: - return ISOTP_SF + length = (orb(payload[0]) & 0x0f) + if length == 0: + return ISOTP_SF_FD + else: + return ISOTP_SF elif t == 1: - return ISOTP_FF + 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 = [ @@ -241,7 +314,7 @@ class ISOTPHeaderEA(ISOTPHeader): XByteField('extended_address', 0) ] - def post_build(self, p, pay): + def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes """ This will set the ByteField 'length' to the correct value. @@ -249,8 +322,48 @@ def post_build(self, p, pay): 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 + 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', @@ -268,17 +381,37 @@ class ISOTP_SF(Packet): ] +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(BitField('extended_message_size', 0, 32), + 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 = [ diff --git a/scapy/contrib/isotp/isotp_scanner.py b/scapy/contrib/isotp/isotp_scanner.py index 0ff7efa7737..c3d4c5bdfc8 100644 --- a/scapy/contrib/isotp/isotp_scanner.py +++ b/scapy/contrib/isotp/isotp_scanner.py @@ -6,20 +6,31 @@ # 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 Iterable, Optional, Union, List, Tuple, Dict -from scapy.packet import Packet from scapy.compat import orb -from scapy.layers.can import CAN -from scapy.supersocket import SuperSocket 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 - + 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") @@ -45,22 +56,29 @@ def send_multiple_ext(sock, ext_id, packet, number_of_packets): sock.send(packet) -def get_isotp_packet(identifier=0x0, extended=False, extended_can_id=False): - # type: (int, bool, bool) -> 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: - pkt = ISOTPHeaderEA() / ISOTP_FF() + 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: - pkt = ISOTPHeader() / ISOTP_FF() + 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" @@ -160,7 +178,8 @@ def scan(sock, # type: SuperSocket sniff_time=0.1, # type: float extended_can_id=False, # type: bool verify_results=True, # type: bool - stop_event=None # type: Optional[Event] + stop_event=None, # type: Optional[Event] + fd=False # type: bool ): # type: (...) -> Dict[int, Tuple[Packet, int]] """Scan and return dictionary of detections @@ -177,6 +196,7 @@ def scan(sock, # type: SuperSocket :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]] @@ -185,7 +205,7 @@ def scan(sock, # type: SuperSocket break if noise_ids and value in noise_ids: continue - sock.send(get_isotp_packet(value, False, extended_can_id)) + 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) @@ -194,16 +214,20 @@ def scan(sock, # type: SuperSocket return return_values cleaned_ret_val = dict() # type: Dict[int, Tuple[Packet, int]] - for tested_id in return_values.keys(): + 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 - for value in range(max(0, tested_id - 2), tested_id + 2, 1): - if stop_event is not None and stop_event.is_set(): - break - sock.send(get_isotp_packet(value, False, extended_can_id)) - sock.sniff(prn=lambda pkt: get_isotp_fc(value, cleaned_ret_val, - noise_ids, False, pkt), - timeout=sniff_time * 10, store=False) + 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 @@ -215,7 +239,8 @@ def scan_extended(sock, # type: SuperSocket 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] + 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 @@ -233,19 +258,20 @@ def scan_extended(sock, # type: SuperSocket 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) + value, extended=True, extended_can_id=extended_can_id, fd=fd) id_list = [] # type: List[int] - r = list(extended_scan_range) 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 @@ -288,7 +314,8 @@ def isotp_scan(sock, # type: SuperSocket extended_can_id=False, # type: bool verify_results=True, # type: bool verbose=False, # type: bool - stop_event=None # type: Optional[Event] + 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 @@ -298,6 +325,7 @@ def isotp_scan(sock, # type: SuperSocket - 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 @@ -318,6 +346,7 @@ def isotp_scan(sock, # type: SuperSocket 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: @@ -326,9 +355,13 @@ def isotp_scan(sock, # type: SuperSocket 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 = CAN(identifier=0x123, - data=b'\xaa\xbb\xcc\xdd\xee\xff\xaa\xbb') + 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, @@ -342,36 +375,43 @@ def isotp_scan(sock, # type: SuperSocket noise_ids=noise_ids, sniff_time=sniff_time, extended_can_id=extended_can_id, - stop_event=stop_event) + 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) + stop_event=stop_event, + fd=fd) filter_periodic_packets(found_packets) if output_format == "text": - return generate_text_output(found_packets, extended_addressing) + return generate_text_output(found_packets, extended_addressing, fd) if output_format == "code": return generate_code_output(found_packets, can_interface, - extended_addressing) + 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) + extended_addressing, fd) -def generate_text_output(found_packets, extended_addressing=False): - # type: (Dict[int, Tuple[Packet, int]], bool) -> str +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: @@ -405,13 +445,16 @@ def generate_text_output(found_packets, extended_addressing=False): 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): - # type: (Dict[int, Tuple[Packet, int]], Optional[str], bool) -> str + 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. @@ -420,6 +463,7 @@ def generate_code_output(found_packets, can_interface="iface", 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 = "" @@ -437,26 +481,76 @@ def generate_code_output(found_packets, can_interface="iface", 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, " \ + "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) + ext_id, + fd) else: - result += "ISOTPSocket(%s, tx_id=0x%x, rx_id=0x%x, padding=%s, " \ + 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) + 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 + 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 @@ -467,6 +561,7 @@ def generate_isotp_list(found_packets, # type: Dict[int, Tuple[Packet, int]] 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 @@ -487,10 +582,12 @@ def generate_isotp_list(found_packets, # type: Dict[int, Tuple[Packet, int]] 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 index 5538698aaa4..4182d445336 100644 --- a/scapy/contrib/isotp/isotp_soft_socket.py +++ b/scapy/contrib/isotp/isotp_soft_socket.py @@ -4,30 +4,39 @@ # 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 -import heapq -import socket - +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.compat import Optional, Union, List, Tuple, Any, Type, cast, \ - Callable, TYPE_CHECKING -from scapy.packet import Packet -from scapy.layers.can import CAN -import scapy.libs.six as six -from scapy.error import Scapy_Exception -from scapy.supersocket import SuperSocket +from scapy.automaton import ObjectPipe, select_objects from scapy.config import conf from scapy.consts import LINUX -from scapy.utils import EDecimal -from scapy.automaton import ObjectPipe, select_objects 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 + 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 @@ -103,10 +112,9 @@ class ISOTPSoftSocket(SuperSocket): :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 - nonblocking_socket = True - def __init__(self, can_socket=None, # type: Optional["CANSocket"] tx_id=0, # type: int @@ -117,20 +125,22 @@ def __init__(self, stmin=0, # type: int padding=False, # type: bool listen_only=False, # type: bool - basecls=ISOTP # type: Type[Packet] + basecls=ISOTP, # type: Type[Packet] + fd=False # type: bool ): # type: (...) -> None - if six.PY3 and LINUX and isinstance(can_socket, six.string_types): + if LINUX and isinstance(can_socket, str): from scapy.contrib.cansocket_native import NativeCANSocket - can_socket = NativeCANSocket(can_socket) - elif isinstance(can_socket, six.string_types): + 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, @@ -141,7 +151,8 @@ def __init__(self, rx_ext_address=self.rx_ext_address, bs=bs, stmin=stmin, - listen_only=listen_only + listen_only=listen_only, + fd=fd ) # Cast for compatibility to functions from SuperSocket. @@ -174,9 +185,9 @@ def recv_raw(self, x=0xffff): return self.basecls, tup[0], float(tup[1]) return self.basecls, None, None - def recv(self, x=0xffff): - # type: (int) -> Optional[Packet] - msg = super(ISOTPSoftSocket, self).recv(x) + def recv(self, x=0xffff, **kwargs): + # type: (int, **Any) -> Optional[Packet] + msg = super(ISOTPSoftSocket, self).recv(x, **kwargs) if msg is None: return None @@ -196,8 +207,10 @@ def select(sockets, remain=None): """This function is called during sendrecv() routine to wait for sockets to be ready to receive """ - obj_pipes = [x.impl.rx_queue for x in sockets if - isinstance(x, ISOTPSoftSocket) and not x.closed] + 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) @@ -216,6 +229,8 @@ class TimeoutScheduler: # 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 @@ -304,13 +319,13 @@ def _wait(cls, handle): # Wait until the next timeout, # or until event.set() gets called in another thread. if to_wait > 0: - log_isotp.debug("TimeoutScheduler Thread going to sleep @ %f " + - "for %fs", now, to_wait) + cls.logger.debug("Thread going to sleep @ %f " + + "for %fs", now, to_wait) interrupted = cls._event.wait(to_wait) new = cls._time() - log_isotp.debug("TimeoutScheduler Thread awake @ %f, slept for" + - " %f, interrupted=%d", new, new - now, - interrupted) + 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(). @@ -323,7 +338,7 @@ def _task(cls): start when the first timeout is added and stop when the last timeout is removed or executed.""" - log_isotp.debug("TimeoutScheduler Thread spawning @ %f", cls._time()) + cls.logger.debug("Thread spawning @ %f", cls._time()) time_empty = None @@ -345,7 +360,7 @@ def _task(cls): finally: # Worst case scenario: if this thread dies, the next scheduled # timeout will start a new one - log_isotp.debug("TimeoutScheduler Thread died @ %f", cls._time()) + cls.logger.debug("Thread died @ %f", cls._time()) cls._thread = None @classmethod @@ -367,7 +382,7 @@ def _poll(cls): callback = handle._cb handle._cb = True - # Call the callback here, outside of the mutex + # Call the callback here, outside the mutex if callable(callback): try: callback() @@ -377,8 +392,6 @@ def _poll(cls): @staticmethod def _time(): # type: () -> float - if six.PY2: - return time.time() return time.monotonic() class Handle: @@ -481,7 +494,8 @@ def __init__(self, rx_ext_address=None, # type: Optional[int] bs=0, # type: int stmin=0, # type: int - listen_only=False # type: bool + listen_only=False, # type: bool + fd=False # type: bool ): # type: (...) -> None self.can_socket = can_socket @@ -491,6 +505,10 @@ def __init__(self, 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 @@ -515,7 +533,7 @@ def __init__(self, self.tx_queue = ObjectPipe[bytes]() self.txfc_bs = 0 self.txfc_stmin = 0 - self.tx_gap = 0 + self.tx_gap = 0. self.tx_buf = None # type: Optional[bytes] self.tx_sn = 0 @@ -548,13 +566,28 @@ def __del__(self): 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" * (CAN_MAX_DLEN - len(load)) + 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(CAN(identifier=self.tx_id, data=load)) + self.can_socket.send(pkt_cls(identifier=self.tx_id, data=load)) else: - self.can_socket.send(CAN(identifier=self.tx_id, flags="extended", - data=load)) + self.can_socket.send(pkt_cls(identifier=self.tx_id, flags="extended", + data=load)) def can_recv(self): # type: () -> None @@ -639,7 +672,7 @@ def _tx_timer_handler(self): elif self.tx_state == ISOTP_SENDING: # push out the next segmented pdu src_off = len(self.ea_hdr) - max_bytes = 7 - src_off + 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") @@ -689,10 +722,10 @@ def on_recv(self, cf): ae = 1 if len(data) < 3: return - if six.indexbytes(data, 0) != self.rx_ext_address: + if data[0] != self.rx_ext_address: return - n_pci = six.indexbytes(data, ae) & 0xf0 + n_pci = data[ae] & 0xf0 if n_pci == N_PCI_FC: self._recv_fc(data[ae:]) @@ -723,24 +756,23 @@ def _recv_fc(self, data): # 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) + 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 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 + if data[2] <= 127: + self.tx_gap = data[2] / 1000 + elif 0xf1 <= data[2] <= 0xf9: + self.tx_gap = (data[2] & 0x0f) / 10000 else: - tx_gap = 0 - self.tx_gap = tx_gap + self.tx_gap = 0. self.tx_state = ISOTP_WAIT_FC - isotp_fc = six.indexbytes(data, 0) & 0x0f + isotp_fc = data[0] & 0x0f if isotp_fc == ISOTP_FC_CTS: self.tx_bs = 0 @@ -778,11 +810,20 @@ def _recv_sf(self, data, ts): "single frame was received") self.rx_state = ISOTP_IDLE - length = six.indexbytes(data, 0) & 0xf + 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 = data[1:1 + length] + 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): @@ -804,17 +845,16 @@ def _recv_ff(self, data, ts): self.rx_ll_dl = len(data) # get the FF_DL - self.rx_len = (six.indexbytes(data, 0) & 0x0f) * 256 + six.indexbytes( - data, 1) + 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 = 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) + 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 @@ -863,7 +903,7 @@ def _recv_cf(self, data): log_isotp.warning("Received a CF with insufficient length") return - if six.indexbytes(data, 0) & 0x0f != self.rx_sn: + if data[0] & 0x0f != self.rx_sn: # Wrong sequence number if conf.verb > 2: log_isotp.warning("RX state was reset because wrong sequence " @@ -919,10 +959,15 @@ def begin_send(self, x): if length > ISOTP_MAX_DLEN_2015: log_isotp.warning("Too much data for ISOTP message") - if len(self.ea_hdr) + length <= 7: + 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 - data += struct.pack("B", length) + 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) @@ -934,7 +979,7 @@ def begin_send(self, x): data += struct.pack(">HI", 0x1000, length) else: data += struct.pack(">H", 0x1000 | length) - load = x[0:8 - len(data)] + load = x[0:self.max_dlen - len(data)] data += load self.can_send(data) @@ -972,9 +1017,5 @@ def send(self, 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 - for at most 'timeout' seconds.""" - try: - return self.rx_queue.recv() - except IndexError: - return None + """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 index 05bddd3fc4c..da1d5e73ab9 100644 --- a/scapy/contrib/isotp/isotp_utils.py +++ b/scapy/contrib/isotp/isotp_utils.py @@ -10,14 +10,27 @@ import struct -from scapy.compat import Iterable, Optional, Union, List, Tuple, Dict, Any, \ - Type +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 -import scapy.libs.six as six + +# Typing imports +from typing import ( + cast, + Iterable, + Iterator, + Optional, + Union, + List, + Tuple, + Dict, + Any, + Type, +) class ISOTPMessageBuilderIter(object): @@ -92,17 +105,14 @@ 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)) + 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[Packet] + basecls=ISOTP # type: Type[ISOTP] ): # type: (...) -> None self.ready = [] # type: List[Tuple[int, Optional[int], ISOTPMessageBuilder.Bucket]] # noqa: E501 @@ -141,7 +151,7 @@ def feed(self, can): 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 = six.indexbytes(data, 0) + ea = data[0] self._try_feed(can.identifier, ea, data[1:], can.time) @property @@ -159,7 +169,7 @@ def __len__(self): return self.count def pop(self, identifier=None, ext_addr=None): - # type: (Optional[int], Optional[int]) -> Optional[Packet] + # type: (Optional[int], Optional[int]) -> Optional[ISOTP] """Returns a built ISOTP message :param identifier: if not None, only return isotp messages with this @@ -190,12 +200,18 @@ def __iter__(self): @staticmethod def _build( t, # type: Tuple[int, Optional[int], ISOTPMessageBuilder.Bucket] - basecls=ISOTP # type: Type[Packet] + basecls=ISOTP # type: Type[ISOTP] ): - # type: (...) -> Packet + # type: (...) -> ISOTP bucket = t[2] data = bucket.ready or b"" - p = basecls(data) + 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"): @@ -235,7 +251,7 @@ def _feed_single_frame(self, identifier, ea, data, ts): # At least 2 bytes are necessary: 1 for length and 1 for data return False - length = six.indexbytes(data, 0) & 0x0f + length = data[0] & 0x0f isotp_data = data[1:length + 1] if length > len(isotp_data): @@ -253,7 +269,7 @@ def _feed_consecutive_frame(self, identifier, ea, data): # 1 for data return False - first_byte = six.indexbytes(data, 0) + first_byte = data[0] seq_no = first_byte & 0x0f isotp_data = data[1:] @@ -303,7 +319,7 @@ def _feed_flow_control_frame(self, identifier, ea, data): def _try_feed(self, identifier, ea, data, ts): # type: (int, Optional[int], bytes, Union[EDecimal, float]) -> None - first_byte = six.indexbytes(data, 0) + 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: @@ -323,20 +339,23 @@ class ISOTPSession(DefaultSession): def __init__(self, *args, **kwargs): # type: (Any, Any) -> None - super(ISOTPSession, self).__init__(*args, **kwargs) 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 on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None + 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 = self.m.pop() - if self._supersession: - self._supersession.on_packet_received(rcvd) - else: - super(ISOTPSession, self).on_packet_received(rcvd) + rcvd = cast(Optional[Packet], self.m.pop()) + if rcvd: + rcvd = self.process(rcvd) + if rcvd: + yield rcvd diff --git a/scapy/contrib/ldp.py b/scapy/contrib/ldp.py index 43e3528893b..bd08ee8f58f 100644 --- a/scapy/contrib/ldp.py +++ b/scapy/contrib/ldp.py @@ -13,13 +13,19 @@ """ -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.config import conf @@ -169,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]) @@ -359,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 @@ -394,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 9888f283f70..6ab62755419 100644 --- a/scapy/contrib/lldp.py +++ b/scapy/contrib/lldp.py @@ -38,14 +38,15 @@ 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.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' @@ -55,6 +56,13 @@ ETHER_TYPES[LLDP_ETHER_TYPE] = 'LLDP' +class LLDPInvalidFieldValue(Scapy_Exception): + """ + field value is out of allowed range + """ + pass + + class LLDPInvalidFrameStructure(Scapy_Exception): """ basic frame structure not standard conform @@ -101,6 +109,40 @@ class LLDPDU(Packet): 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 @@ -113,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 @@ -280,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 @@ -310,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([ @@ -320,10 +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: 0 if pkt._length is - None else pkt._length - 1) + ( + IP6Field('id', None), + lambda pkt: pkt.subtype == 0x05 and pkt.family == 0x02 + ), + ], StrLenField('id', '', length_from=_ldp_id_lengthfrom) ) ] @@ -365,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([ @@ -375,10 +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: 0 if pkt._length is - None else pkt._length - 1) + ( + IP6Field('id', None), + lambda pkt: pkt.subtype == 0x04 and pkt.family == 0x02 + ), + ], StrLenField('id', '', length_from=_ldp_id_lengthfrom) ) ] @@ -523,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 @@ -620,7 +654,7 @@ 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: 0 if pkt._management_address_string_length is None else @@ -678,6 +712,515 @@ class LLDPDUGenericOrganisationSpecific(LLDPDU): 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 = { @@ -690,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 index 08b570f0572..3750466aa95 100644 --- a/scapy/contrib/loraphy2wan.py +++ b/scapy/contrib/loraphy2wan.py @@ -16,12 +16,29 @@ """ from scapy.packet import Packet -from scapy.fields import BitField, ByteEnumField, ByteField, \ - ConditionalField, IntField, LEShortField, PacketListField, \ - StrFixedLenField, X3BytesField, XByteField, XIntField, \ - XShortField, BitFieldLenField, LEX3BytesField, XBitField, \ - BitEnumField, XLEIntField, StrField, PacketField, \ - MultipleTypeField +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): @@ -63,7 +80,7 @@ def extract_padding(self, p): class DevAddrElem(Packet): name = "DevAddrElem" fields_desc = [XByteField("NwkID", 0x0), - LEX3BytesField("NwkAddr", b"\x00" * 3)] + XLE3BytesField("NwkAddr", b"\x00" * 3)] CIDs_up = {0x01: "ResetInd", @@ -592,8 +609,8 @@ class Join_Request(Packet): class Join_Accept(Packet): name = "Join_Accept" dcflist = False - fields_desc = [LEX3BytesField("JoinAppNonce", 0), - LEX3BytesField("NetID", 0), + fields_desc = [XLE3BytesField("JoinAppNonce", 0), + XLE3BytesField("NetID", 0), XLEIntField("DevAddr", 0), DLsettings, XByteField("RxDelay", 0), @@ -692,9 +709,9 @@ class PHYPayload(Packet): name = "PHYPayload" fields_desc = [MHDR, MACPayload, - ConditionalField(XIntField("MIC", 0), - lambda pkt:(pkt.MType != 0b001 or - LoRa.encrypted is False))] + 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 diff --git a/scapy/contrib/ltp.py b/scapy/contrib/ltp.py index 1de7ed6a8d8..bf2e903ab52 100755 --- a/scapy/contrib/ltp.py +++ b/scapy/contrib/ltp.py @@ -16,7 +16,6 @@ # scapy.contrib.description = Licklider Transmission Protocol (LTP) # scapy.contrib.status = loads -import scapy.libs.six as six from scapy.packet import Packet, bind_layers, bind_top_down from scapy.fields import BitEnumField, BitField, BitFieldLenField, \ ByteEnumField, ConditionalField, PacketListField, StrLenField @@ -100,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 @@ -172,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/macsec.py b/scapy/contrib/macsec.py index 45b5ed47230..ac90972246a 100755 --- a/scapy/contrib/macsec.py +++ b/scapy/contrib/macsec.py @@ -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.libs.six as six if conf.crypto_valid: from cryptography.hazmat.backends import default_backend @@ -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 @@ -244,9 +241,9 @@ class MACsec(Packet): 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/modbus.py b/scapy/contrib/modbus.py index 3b23506e082..9e9cd2ed915 100644 --- a/scapy/contrib/modbus.py +++ b/scapy/contrib/modbus.py @@ -102,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): @@ -684,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%") @@ -694,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%") @@ -704,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%") @@ -740,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%") @@ -752,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%") @@ -764,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%") diff --git a/scapy/contrib/mpls.py b/scapy/contrib/mpls.py index 95620c31bf1..0808b8a70b0 100644 --- a/scapy/contrib/mpls.py +++ b/scapy/contrib/mpls.py @@ -6,12 +6,24 @@ # 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): @@ -37,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: @@ -45,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 d27205d9684..afd75e78a02 100644 --- a/scapy/contrib/mqtt.py +++ b/scapy/contrib/mqtt.py @@ -7,8 +7,17 @@ # 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 @@ -250,10 +259,18 @@ class MQTTSubscribe(Packet): 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', } @@ -261,7 +278,7 @@ class MQTTSuback(Packet): name = "MQTT suback" fields_desc = [ ShortField("msgid", None), - ByteEnumField("retcode", None, ALLOWED_RETURN_CODE) + FieldListField("retcodes", None, ByteEnumField("", None, ALLOWED_RETURN_CODE)) ] diff --git a/scapy/contrib/nfs.py b/scapy/contrib/nfs.py index 5f6ca940f56..faaa431f1ef 100644 --- a/scapy/contrib/nfs.py +++ b/scapy/contrib/nfs.py @@ -12,7 +12,6 @@ from scapy.fields import IntField, IntEnumField, FieldListField, LongField, \ XIntField, XLongField, ConditionalField, PacketListField, StrLenField, \ PacketField -from scapy.libs.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)) 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=">> 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 v1.7+. " + "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/roce.py b/scapy/contrib/roce.py index c927d1e67f1..93dceab693b 100644 --- a/scapy/contrib/roce.py +++ b/scapy/contrib/roce.py @@ -14,12 +14,16 @@ 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 scapy.compat import Tuple + +from typing import ( + Tuple +) _transports = { 'RC': 0x00, @@ -176,8 +180,25 @@ def compute_icrc(self, p): 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: - # TODO support IPv6 warning("The underlayer protocol %s is not supported.", ip and ip.name) return self.pack_icrc(0) diff --git a/scapy/contrib/rtcp.py b/scapy/contrib/rtcp.py index 25414182e11..a41673870dd 100644 --- a/scapy/contrib/rtcp.py +++ b/scapy/contrib/rtcp.py @@ -51,6 +51,9 @@ class SenderInfo(Packet): IntField('sender_octet_count', None) ] + def extract_padding(self, p): + return "", p + class ReceptionReport(Packet): name = "Reception report" @@ -64,6 +67,9 @@ class ReceptionReport(Packet): IntField('delay_since_last_SR', None) ] + def extract_padding(self, p): + return "", p + _sdes_chunk_types = { 0: "END", @@ -94,7 +100,12 @@ class SDESChunk(Packet): name = "SDES chunk" fields_desc = [ IntField('sourcesync', None), - PacketListField('items', None, pkt_cls=SDESItem) + PacketListField( + 'items', None, + next_cls_cb=( + lambda x, y, p, z: None if (p and p.chunk_type == 0) else SDESItem + ) + ) ] diff --git a/scapy/contrib/rtps/common_types.py b/scapy/contrib/rtps/common_types.py index adf8cf8b30f..38913eff0b8 100644 --- a/scapy/contrib/rtps/common_types.py +++ b/scapy/contrib/rtps/common_types.py @@ -36,7 +36,7 @@ FORMAT_LE = "<" FORMAT_BE = ">" STR_MAX_LEN = 8192 -DEFAULT_ENDIANESS = FORMAT_LE +DEFAULT_ENDIANNESS = FORMAT_LE def is_le(pkt): @@ -279,23 +279,27 @@ def extract_padding(self, p): _rtps_vendor_ids = { 0x0000: "VENDOR_ID_UNKNOWN (0x0000)", - 0x0101: "Real-Time Innovations, Inc. - Connext DDS", - 0x0102: "PrismTech Inc. - OpenSplice DDS", - 0x0103: "Object Computing Incorporated, Inc. (OCI) - OpenDDS", - 0x0104: "MilSoft", - 0x0105: "Gallium Visual Systems Inc. - InterCOM DDS", - 0x0106: "TwinOaks Computing, Inc. - CoreDX DDS", + 0x0101: "Real-Time Innovations, Inc. (RTI) - Connext DDS", + 0x0102: "ADLink Ltd. - OpenSplice DDS", + 0x0103: "Object Computing Inc. (OCI) - OpenDDS", + 0x0104: "MilSoft - Mil-DDS", + 0x0105: "Kongsberg - InterCOM DDS", + 0x0106: "Twin Oaks Computing, Inc. - CoreDX DDS", 0x0107: "Lakota Technical Solutions, Inc.", 0x0108: "ICOUP Consulting", - 0x0109: "ETRI Electronics and Telecommunication Research Institute", + 0x0109: "Electronics and Telecommunication Research Institute (ETRI) - Diamond DDS", 0x010A: "Real-Time Innovations, Inc. (RTI) - Connext DDS Micro", - 0x010B: "PrismTech - OpenSplice Mobile", - 0x010C: "PrismTech - OpenSplice Gateway", - 0x010D: "PrismTech - OpenSplice Lite", - 0x010E: "Technicolor Inc. - Qeo", - 0x010F: "eProsima - Fast-RTPS", - 0x0110: "ADLINK - Cyclone DDS", - 0x0111: "GurumNetworks - GurumDDS", + 0x010B: "ADLink Ltd. - VortexCafe", + 0x010C: "PrismTech Ltd", + 0x010D: "ADLink Ltd. - Vortex Lite", + 0x010E: "Technicolor - Qeo", + 0x010F: "eProsima - FastRTPS, FastDDS", + 0x0110: "Eclipse Foundation - Cyclone DDS", + 0x0111: "Gurum Networks, Inc. - GurumDDS", + 0x0112: "Atostek - RustDDS", + 0x0113: "Nanjing Zhenrong Software Technology Co. \ + - Zhenrong Data Distribution Service (ZRDDS)", + 0x0114: "S2E Software Systems B.V. - Dust DDS", } diff --git a/scapy/contrib/rtps/rtps.py b/scapy/contrib/rtps/rtps.py index 82c9ffb8d1e..134ff7c7b5a 100644 --- a/scapy/contrib/rtps/rtps.py +++ b/scapy/contrib/rtps/rtps.py @@ -25,7 +25,6 @@ X3BytesField, XByteField, XIntField, - XLongField, XNBytesField, XShortField, XStrLenField, @@ -352,7 +351,8 @@ class RTPSSubMessage_ACKNACK(EPacket): "readerSNState", 0, length_from=lambda pkt: pkt.octetsToNextHeader - 8 - 4 ), - XNBytesField("count", 0, 4), + EField(IntField("count", 0), + endianness_from=e_flags), ] @@ -396,8 +396,14 @@ class RTPSSubMessage_HEARTBEAT(EPacket): fmt="4s", enum=_rtps_reserved_entity_ids, ), - XLongField("firstAvailableSeqNum", 0), - XLongField("lastSeqNum", 0), + EField(IntField("firstAvailableSeqNumHi", 0), + endianness_from=e_flags), + EField(IntField("firstAvailableSeqNumLow", 0), + endianness_from=e_flags), + EField(IntField("lastSeqNumHi", 0), + endianness_from=e_flags), + EField(IntField("lastSeqNumLow", 0), + endianness_from=e_flags), EField(IntField("count", 0), endianness_from=e_flags), ] 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/iec104/iec104_information_elements.py b/scapy/contrib/scada/iec104/iec104_information_elements.py index 13444213d9c..f82813d1720 100644 --- a/scapy/contrib/scada/iec104/iec104_information_elements.py +++ b/scapy/contrib/scada/iec104/iec104_information_elements.py @@ -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): @@ -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), @@ -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), @@ -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/pcom.py b/scapy/contrib/scada/pcom.py index 533109cdb47..98603260588 100755 --- a/scapy/contrib/scada/pcom.py +++ b/scapy/contrib/scada/pcom.py @@ -21,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 @@ -83,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(" + +# 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 index 92876c4a077..ee243551597 100644 --- a/scapy/contrib/stun.py +++ b/scapy/contrib/stun.py @@ -20,7 +20,7 @@ from scapy.layers.inet import UDP, TCP from scapy.config import conf -from scapy.packet import Packet, bind_layers +from scapy.packet import Packet, bind_bottom_up, bind_top_down from scapy.utils import inet_ntoa, inet_aton from scapy.fields import ( BitField, @@ -39,7 +39,9 @@ XLongField, XIntField, XBitField, - IPField + IPField, + IP6Field, + MultipleTypeField, ) MAGIC_COOKIE = 0x2112A442 @@ -149,7 +151,27 @@ def m2i(self, pkt, x): 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)) ^ MAGIC_COOKIE) + 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): @@ -157,11 +179,36 @@ class STUNXorMappedAddress(STUNGenericTlv): fields_desc = [ XShortField("type", 0x0020), - ShortField("length", 8), + 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), - XorIp("xip", 0) # FIXME <- only IPv4 addresses will work + 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"), + ), ] @@ -207,6 +254,7 @@ class STUNGoogNetworkInfo(STUNGenericTlv): _stun_tlv_class = { + 0x0001: STUNMappedAddress, 0x0006: STUNUsername, 0x0008: STUNMessageIntegrity, 0x0020: STUNXorMappedAddress, @@ -253,11 +301,16 @@ 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.tlvlist: + for attr in self.attributes: if isinstance(attr, STUNMessageIntegrity): pass # TODO Fill hmac-sha1 in MESSAGE-INTEGRITY attribute return pkt -bind_layers(UDP, STUN, dport=3478) -bind_layers(TCP, STUN, dport=3478) +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 ed1ca0640be..814136dd87d 100755 --- a/scapy/contrib/tacacs.py +++ b/scapy/contrib/tacacs.py @@ -362,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): @@ -420,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 index 406bf5b5145..6f18aee9d8a 100644 --- a/scapy/contrib/tcpao.py +++ b/scapy/contrib/tcpao.py @@ -9,7 +9,7 @@ """Packet-processing utilities implementing RFC5925 and RFC5926""" import logging -from scapy.compat import orb, Union +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 @@ -18,6 +18,10 @@ import socket import struct +from typing import ( + Union, +) + logger = logging.getLogger(__name__) 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/ubberlogger.py b/scapy/contrib/ubberlogger.py deleted file mode 100644 index 3fd509ae748..00000000000 --- a/scapy/contrib/ubberlogger.py +++ /dev/null @@ -1,119 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -# This file is part of Scapy -# See https://scapy.net/ for more information - -# 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/wpa_eapol.py b/scapy/contrib/wpa_eapol.py deleted file mode 100644 index 77853d12c7e..00000000000 --- a/scapy/contrib/wpa_eapol.py +++ /dev/null @@ -1,41 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -# This file is part of Scapy -# See https://scapy.net/ for more information - -# 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 f233dad5364..fa3679e6746 100644 --- a/scapy/dadict.py +++ b/scapy/dadict.py @@ -7,22 +7,23 @@ Direct Access dictionary. """ -from __future__ import absolute_import -from __future__ import print_function from scapy.error import Scapy_Exception -import scapy.libs.six as six from scapy.compat import plain_str -from scapy.compat import ( +# Typing +from typing import ( Any, Dict, Generic, Iterator, List, + Tuple, + Type, TypeVar, Union, - cast, ) +from scapy.compat import Self + ############################### # Direct Access dictionary # @@ -70,11 +71,13 @@ class DADict(Generic[_K, _V]): ETHER_TYPES.IPv4 -> 2048 """ + __slots__ = ["_name", "d"] + def __init__(self, _name="DADict", **kargs): # type: (str, **Any) -> None self._name = _name self.d = {} # type: Dict[_K, _V] - self.update(kargs) + self.update(kargs) # type: ignore def ident(self, v): # type: (_V) -> str @@ -86,13 +89,13 @@ def ident(self, v): return "unknown" def update(self, *args, **kwargs): - # type: (*Dict[str, _V], **Dict[str, _V]) -> None - for k, v in six.iteritems(dict(*args, **kwargs)): - self[k] = v + # 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 six.iterkeys(self.d): + for x in self.d: if not isinstance(x, str) or x[0] != "_": yield x @@ -106,7 +109,7 @@ def __iter__(self): def itervalues(self): # type: () -> Iterator[_V] - return six.itervalues(self.d) # type: ignore + return self.d.values() # type: ignore def values(self): # type: () -> List[_V] @@ -144,11 +147,20 @@ def __getattr__(self, attr): try: return object.__getattribute__(self, attr) # type: ignore except AttributeError: - for k, v in six.iteritems(self.d): + for k, v in self.d.items(): if self.ident(v) == attr: - return cast(_K, k) + 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 f58efbea9a8..b6bff82e9ab 100644 --- a/scapy/data.py +++ b/scapy/data.py @@ -8,17 +8,17 @@ """ import calendar +import hashlib import os +import pickle import warnings - 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.libs.six as six -from scapy.compat import ( +# Typing imports +from typing import ( Any, Callable, Dict, @@ -26,8 +26,10 @@ List, Optional, Tuple, + Union, cast, ) +from scapy.compat import DecoratorCallable ############ @@ -128,8 +130,10 @@ 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 @@ -198,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, @@ -288,17 +292,73 @@ } +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 + + 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[bytes], int, type) -> DADict[int, str] - """"Parse /etc/protocols and return values as a dictionary.""" + # 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[bytes]) -> None + # 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() @@ -318,11 +378,11 @@ 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(iter(_fallback.split(b"\n"))) + _process_data(_fallback()) else: log_loading.info("Can't open %s file", filename) return dct @@ -352,18 +412,23 @@ def __getitem__(self, attr): return super(EtherDA, self).__getitem__(attr) -def load_ethertypes(filename): +@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 - prot = load_protocols(filename or "Scapy's backup ETHER_TYPES", - _fallback=DATA, + 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): # type: (str) -> Tuple[DADict[int, str], DADict[int, str], DADict[int, str]] # noqa: E501 tdct = DADict(_name="%s-tcp" % filename) # type: DADict[int, str] @@ -457,8 +522,7 @@ def reverse_lookup(self, name, case_sensitive=False): else: name = name.lower() filtr = lambda x, l: any(x in z.lower() for z in l) - return {k: v for k, v in six.iteritems(self.d) - if filtr(name, v)} + return {k: v for k, v in self.d.items() if filtr(name, v)} # type: ignore def __dir__(self): # type: () -> List[str] @@ -471,33 +535,53 @@ def __dir__(self): ] + super(ManufDA, self).__dir__() -def load_manuf(filename): - # type: (str) -> ManufDA +@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) - ouib, shrt = parts[:2] - lng = parts[2].lstrip(b"#").strip() if len(parts) > 2 else b"" + oui, shrt = parts[:2] + lng = parts[2].lstrip("#").strip() if len(parts) > 2 else "" lng = lng or shrt - oui = plain_str(ouib) - 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""" @@ -523,35 +607,34 @@ def select_path(directories, filename): "etc", "services", )) - # Default values, will be updated by arch.windows - ETHER_TYPES = load_ethertypes(None) - MANUFDB = ManufDA() + ETHER_TYPES = load_ethertypes() + MANUFDB = load_manuf() else: IP_PROTOS = load_protocols("/etc/protocols") - ETHER_TYPES = load_ethertypes("/etc/ethertypes") TCP_SERVICES, UDP_SERVICES, SCTP_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" + ETHER_TYPES = load_ethertypes("/etc/ethertypes") + 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 # type: Optional[str] + self.base = None # type: KBBaseType def lazy_init(self): # type: () -> None @@ -568,7 +651,7 @@ def reload(self, filename=None): self.base = oldbase def get_base(self): - # type: () -> str + # type: () -> Union[str, List[Tuple[str, Dict[str,Dict[str,str]]]]] if self.base is None: self.lazy_init() - return cast(str, 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 e846b86bc40..ff3fdc13eb4 100644 --- a/scapy/error.py +++ b/scapy/error.py @@ -15,14 +15,12 @@ import logging import traceback import time -import warnings from scapy.consts import WINDOWS -import scapy.libs.six as six # Typing imports from logging import LogRecord -from scapy.compat import ( +from typing import ( Any, Dict, Tuple, @@ -70,7 +68,7 @@ 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 False self.warning_table[caller] = (tm, nb) @@ -110,6 +108,7 @@ def format(self, record): # get Scapy's master logger log_scapy = logging.getLogger("scapy") +log_scapy.propagate = False # override the level if not already set if log_scapy.level == logging.NOTSET: log_scapy.setLevel(logging.WARNING) @@ -130,17 +129,6 @@ def format(self, record): # logs when loading Scapy log_loading = logging.getLogger("scapy.loading") -# Apply warnings filters for python 2 -if six.PY2: - try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - from cryptography.utils import CryptographyDeprecationWarning - warnings.filterwarnings("ignore", - category=CryptographyDeprecationWarning) - except ImportError: - pass - def warning(x, *args, **kargs): # type: (str, *Any, **Any) -> None diff --git a/scapy/fields.py b/scapy/fields.py index b41aae16897..46064b726e2 100644 --- a/scapy/fields.py +++ b/scapy/fields.py @@ -9,10 +9,10 @@ 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 @@ -22,6 +22,7 @@ from types import MethodType from uuid import UUID +from enum import Enum from scapy.config import conf from scapy.dadict import DADict @@ -31,24 +32,21 @@ 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, EDecimal from scapy.utils6 import in6_6to4ExtractAddr, in6_isaddr6to4, \ in6_isaddrTeredo, in6_ptop, Net6, teredoAddrExtractInfo -from scapy.base_classes import Gen, Net, BasePacket, Field_metaclass -from scapy.error import warning - -import scapy.libs.six as six -from scapy.libs.six import integer_types - -try: - from enum import Enum -except ImportError: - Enum = None # type: ignore +from scapy.base_classes import ( + _ScopedIP, + BasePacket, + Field_metaclass, + Net, + ScopedIP, +) # Typing imports -from scapy.compat import ( +from typing import ( Any, AnyStr, Callable, @@ -144,8 +142,7 @@ def update(self, anotherDict): # type: ignore M = TypeVar('M') # Machine storage -@six.add_metaclass(Field_metaclass) -class Field(Generic[I, M]): +class Field(Generic[I, M], metaclass=Field_metaclass): """ For more information on how this works, please refer to the 'Adding new protocols' chapter in the online documentation: @@ -313,6 +310,7 @@ class _FieldContainer(object): """ A field that acts as a container for another field """ + __slots__ = ["fld"] def __getattr__(self, attr): # type: (str) -> Any @@ -334,13 +332,35 @@ def __eq__(self, other): # type: (Any) -> bool return bool(self.fld == other) - def __ne__(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 + + def __eq__(self, other): # type: (Any) -> bool - # Python 2.7 compat - return not self == other + return bool(self.fld == other) - # mypy doesn't support __hash__ = None - __hash__ = None # type: ignore + def __hash__(self): + # type: () -> int + return hash(self.fld) class ActionField(_FieldContainer): @@ -362,7 +382,7 @@ class ConditionalField(_FieldContainer): __slots__ = ["fld", "cond"] def __init__(self, - fld, # type: Field[Any, Any] + fld, # type: AnyField cond # type: Callable[[Packet], bool] ): # type: (...) -> None @@ -380,7 +400,7 @@ def any2i(self, pkt, x): # 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 re-using the same conditional field, or + # 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. @@ -414,48 +434,56 @@ def __getattr__(self, attr): class MultipleTypeField(_FieldContainer): - """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. + """ + 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", "default"] + __slots__ = ["flds", "dflt", "hints", "name", "default"] - def __init__(self, - flds, # type: List[Tuple[Field[Any, Any], Any]] - dflt # type: Field[Any, Any] - ): - # type: (...) -> None - self.flds = flds + 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)") % self.name, + "MultipleTypeField (%s). Use hints.") % self.name, SyntaxWarning ) @@ -573,7 +601,10 @@ def i2len(self, 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 @@ -581,6 +612,10 @@ def register_owner(self, cls): fld.owners.append(cls) self.dflt.owners.append(cls) + def get_fields_list(self): + # type: () -> List[Any] + return [self] + @property def fld(self): # type: () -> Field[Any, Any] @@ -593,7 +628,7 @@ class PadField(_FieldContainer): __slots__ = ["fld", "_align", "_padwith"] def __init__(self, fld, align, padwith=None): - # type: (Field[Any, Any], int, Optional[bytes]) -> None + # type: (AnyField, int, Optional[bytes]) -> None self.fld = fld self._align = align self._padwith = padwith or b"\x00" @@ -618,11 +653,8 @@ def addfield(self, ): # type: (...) -> bytes sval = self.fld.addfield(pkt, b"", val) - return s + sval + struct.pack( - "%is" % ( - self.padlen(len(sval), pkt) - ), - self._padwith + return s + sval + ( + self.padlen(len(sval), pkt) * self._padwith ) @@ -675,11 +707,6 @@ def __getitem__(self, item): # type: ignore item = slice(new_start, new_stop, step) return super(self.__class__, self).__getitem__(item) - if six.PY2: - def __getslice__(self, i, j): - # Python 2 compat - return self.__getitem__(slice(i, j)) - class TrailerField(_FieldContainer): """Special Field that gets its value from the end of the *packet* @@ -754,7 +781,7 @@ def dst_from_pkt(self, pkt): 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)): + for field, value in condition.items()): return addr # type: ignore except AttributeError: pass @@ -775,11 +802,11 @@ def __init__(self, name, default): def i2m(self, pkt, x): # type: (Optional[Packet], Optional[str]) -> bytes - if x is None: + if not x: return b"\0\0\0\0\0\0" try: y = mac2str(x) - except (struct.error, OverflowError): + except (struct.error, OverflowError, ValueError): y = bytes_encode(x) return y @@ -807,6 +834,16 @@ def randval(self): return RandMAC() +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 @@ -816,11 +853,18 @@ def h2i(self, pkt, x): # type: (Optional[Packet], Union[AnyStr, List[AnyStr]]) -> Any if isinstance(x, bytes): x = plain_str(x) # type: ignore - if isinstance(x, str): + if isinstance(x, _ScopedIP): + return x + elif isinstance(x, str): + x = ScopedIP(x) try: inet_aton(x) except socket.error: return Net(x) + elif isinstance(x, tuple): + if len(x) != 2: + raise ValueError("Invalid IP format") + return Net(*x) elif isinstance(x, list): return [self.h2i(pkt, n) for n in x] return x @@ -857,6 +901,8 @@ def any2i(self, 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) @@ -866,29 +912,16 @@ def randval(self): class SourceIPField(IPField): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): - # type: (str, Optional[str]) -> None + def __init__(self, name): + # type: (str) -> None IPField.__init__(self, name, None) - self.dstname = dstname def __findaddr(self, pkt): - # type: (Packet) -> str + # 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 - } # type: Set[Tuple[str, str, str]] - 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): # type: (Optional[Packet], Optional[Union[str, Net]]) -> bytes @@ -909,14 +942,21 @@ def __init__(self, name, default): Field.__init__(self, name, default, "16s") def h2i(self, pkt, x): - # type: (Optional[Packet], Optional[str]) -> str + # 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: 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 # type: ignore @@ -950,6 +990,8 @@ 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) @@ -959,36 +1001,27 @@ def randval(self): class SourceIP6Field(IP6Field): - __slots__ = ["dstname"] - - def __init__(self, name, dstname): - # type: (str, str) -> None + 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): # type: (Optional[Packet], Optional[Union[str, Net6]]) -> bytes - if x is None: - dst = ("::" if self.dstname is None else - getattr(pkt, self.dstname) or "::") - iff, x, nh = conf.route6.route(dst) + 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): # type: (Optional[Packet], Optional[Union[str, Net6]]) -> str - 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] + if x is None and pkt is not None: + x = self.__findaddr(pkt) return super(SourceIP6Field, self).i2h(pkt, x) @@ -1066,12 +1099,21 @@ def getfield(self, pkt, s): 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) +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 @@ -1093,12 +1135,12 @@ def m2i(self, pkt, x): 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(x)) + 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, integer_types): + if isinstance(x, int): return '%i' % x return super(NBytesField, self).i2repr(pkt, x) @@ -1111,11 +1153,15 @@ def getfield(self, pkt, s): 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, integer_types): + 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)): @@ -1133,6 +1179,10 @@ class FieldValueRangeException(Scapy_Exception): pass +class MaximumItemsCount(Scapy_Exception): + pass + + class FieldAttributeException(Scapy_Exception): pass @@ -1387,14 +1437,14 @@ def i2len(self, pkt, x): def any2i(self, pkt, x): # type: (Optional[Packet], Any) -> I - if isinstance(x, six.text_type): + if isinstance(x, str): x = bytes_encode(x) return super(_StrField, self).any2i(pkt, x) # type: ignore def i2repr(self, pkt, x): # type: (Optional[Packet], I) -> str - if isinstance(x, bytes): - return repr(plain_str(x)) + if x and isinstance(x, bytes): + return repr(x) return super(_StrField, self).i2repr(pkt, x) def i2m(self, pkt, x): @@ -1426,13 +1476,9 @@ class StrField(_StrField[bytes]): class StrFieldUtf16(StrField): - def h2i(self, pkt, x): - # type: (Optional[Packet], Optional[str]) -> bytes - return plain_str(x).encode('utf-16')[2:] - def any2i(self, pkt, x): # type: (Optional[Packet], Optional[str]) -> bytes - if isinstance(x, six.text_type): + if isinstance(x, str): return self.h2i(pkt, x) return super(StrFieldUtf16, self).any2i(pkt, x) @@ -1440,9 +1486,13 @@ 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', errors="replace") + return bytes_encode(x).decode('utf-16-le', errors="replace") class _StrEnumField: @@ -1502,7 +1552,7 @@ def i2m(self, return b"" return raw(i) - def m2i(self, pkt, m): + def m2i(self, pkt, m): # type: ignore # type: (Optional[Packet], bytes) -> Packet try: # we want to set parent wherever possible @@ -1510,6 +1560,14 @@ def m2i(self, pkt, m): except TypeError: return self.cls(m) + +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 @@ -1521,23 +1579,17 @@ def getfield(self, r = i[conf.padding_layer] 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()) # type: ignore -class PacketField(_PacketField[BasePacket]): - def any2i(self, pkt, x): - # type: (Optional[Packet], BasePacket) -> BasePacket - if x and pkt and hasattr(x, "add_parent"): - cast("Packet", x).add_parent(pkt) - return super(PacketField, self).any2i(pkt, x) - - -class PacketLenField(_PacketField[Optional[BasePacket]]): +class PacketLenField(_PacketFieldSingle[Optional[BasePacket]]): __slots__ = ["length_from"] def __init__(self, @@ -1554,7 +1606,7 @@ def getfield(self, pkt, # type: Packet s, # type: bytes ): - # type: (...) -> Tuple[bytes, Optional[Packet]] + # type: (...) -> Tuple[bytes, Optional[BasePacket]] len_pkt = self.length_from(pkt) i = None if len_pkt: @@ -1575,7 +1627,7 @@ class PacketListField(_PacketField[List[BasePacket]]): (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__( @@ -1585,7 +1637,8 @@ def __init__( 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], Type[Packet]]] # noqa: E501 + 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 """ @@ -1643,7 +1696,7 @@ def __init__( cbk(pkt:Packet, lst:List[Packet], cur:Optional[Packet], - remain:str + remain:bytes, ) -> Optional[Type[Packet]] The pkt argument contains a reference to the Packet instance @@ -1685,6 +1738,8 @@ class object defining a ``dispatch_hook`` class method :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 @@ -1696,6 +1751,7 @@ class object defining a ``dispatch_hook`` class method 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] @@ -1776,7 +1832,18 @@ 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] @@ -1798,7 +1865,7 @@ def __init__( name, # type: str default, # type: Optional[bytes] length=None, # type: Optional[int] - length_from=None, # type: Optional[Callable[[Packet], int]] # noqa: E501 + length_from=None, # type: Optional[Callable[[Packet], int]] ): # type: (...) -> None super(StrFixedLenField, self).__init__(name, default) @@ -1812,13 +1879,15 @@ def i2repr(self, v, # type: bytes ): # type: (...) -> str - if isinstance(v, bytes): + 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): @@ -1861,6 +1930,12 @@ def __init__(self, name, default, length=31): # type: (str, bytes, int) -> None StrFixedLenField.__init__(self, name, default, length) + 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: @@ -1871,21 +1946,25 @@ def i2m(self, pkt, y): x += b" " * len_pkt x = x[:len_pkt] x = b"".join( - chb(0x41 + (orb(b) >> 4)) + - chb(0x41 + (orb(b) & 0xf)) + struct.pack( + "!BB", + 0x41 + (b >> 4), + 0x41 + (b & 0xf), + ) for b in x - ) # noqa: E501 + ) return b" " + x def m2i(self, pkt, x): # type: (Optional[Packet], bytes) -> bytes - x = x.strip(b"\x00").strip(b" ") + x = x[1:].strip(b"\x00") return b"".join(map( - lambda x, y: chb( - (((orb(x) - 1) & 0xf) << 4) + ((orb(y) - 1) & 0xf) + lambda x, y: struct.pack( + "!B", + (((x - 1) & 0xf) << 4) + ((y - 1) & 0xf) ), x[::2], x[1::2] - )) + )).rstrip(b" ") class StrLenField(StrField): @@ -1915,6 +1994,8 @@ def getfield(self, pkt, s): 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): @@ -1964,6 +2045,21 @@ class StrLenFieldUtf16(StrLenField, StrFieldUtf16): pass +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"] @@ -1986,7 +2082,7 @@ def randval(self): class FieldListField(Field[List[Any], List[Any]]): - __slots__ = ["field", "count_from", "length_from"] + __slots__ = ["field", "count_from", "length_from", "max_count"] islist = 1 def __init__( @@ -1996,6 +2092,7 @@ def __init__( 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: @@ -2004,6 +2101,7 @@ def __init__( 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 @@ -2063,7 +2161,18 @@ def getfield(self, 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[int, int]): @@ -2105,7 +2214,6 @@ def i2m(self, pkt, x): class StrNullField(StrField): DELIMITER = b"\x00" - ALIGNMENT = 1 def addfield(self, pkt, s, val): # type: (Packet, bytes, Optional[bytes]) -> bytes @@ -2122,7 +2230,7 @@ def getfield(self, if len_str < 0: # DELIMITER not found: return empty return b"", s - if len_str % self.ALIGNMENT: + if len_str % len(self.DELIMITER): len_str += 1 else: break @@ -2139,7 +2247,6 @@ def i2len(self, pkt, x): class StrNullFieldUtf16(StrNullField, StrFieldUtf16): DELIMITER = b"\x00\x00" - ALIGNMENT = 2 class StrStopField(StrField): @@ -2346,7 +2453,7 @@ class BitField(_BitField[int]): __doc__ = _BitField.__doc__ -class BitFixedLenField(BitField): +class BitLenField(BitField): __slots__ = ["length_from"] def __init__(self, @@ -2356,7 +2463,7 @@ def __init__(self, ): # type: (...) -> None self.length_from = length_from - super(BitFixedLenField, self).__init__(name, default, 0) + super(BitLenField, self).__init__(name, default, 0) def getfield(self, # type: ignore pkt, # type: Packet @@ -2364,7 +2471,7 @@ def getfield(self, # type: ignore ): # type: (...) -> Union[Tuple[Tuple[bytes, int], int], Tuple[bytes, int]] # noqa: E501 self.size = self.length_from(pkt) - return super(BitFixedLenField, self).getfield(pkt, s) + return super(BitLenField, self).getfield(pkt, s) def addfield(self, # type: ignore pkt, # type: Packet @@ -2373,11 +2480,11 @@ def addfield(self, # type: ignore ): # type: (...) -> Union[Tuple[bytes, int, int], bytes] self.size = self.length_from(pkt) - return super(BitFixedLenField, self).addfield(pkt, s, val) + return super(BitLenField, self).addfield(pkt, s, val) class BitFieldLenField(BitField): - __slots__ = ["length_of", "count_of", "adjust"] + __slots__ = ["length_of", "count_of", "adjust", "tot_size", "end_tot_size"] def __init__(self, name, # type: str @@ -2386,20 +2493,19 @@ def __init__(self, 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) + 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): # type: (Optional[Packet], Optional[Any]) -> int - if six.PY2: - func = FieldLenField.i2m.__func__ - else: - func = FieldLenField.i2m - return func(self, pkt, x) # type: ignore + return FieldLenField.i2m(self, pkt, x) # type: ignore class XBitField(BitField): @@ -2408,11 +2514,14 @@ def i2repr(self, pkt, x): return lhex(self.i2h(pkt, x)) +_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: Union[Dict[I, str], Dict[str, I], List[str], DADict[I, str], Type[Enum], Tuple[Callable[[I], str], Callable[[str], I]]] # noqa: E501 + enum, # type: _EnumType[I] fmt="H", # type: str ): # type: (...) -> None @@ -2439,7 +2548,7 @@ def __init__(self, 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 Enum and isinstance(enum, type) and issubclass(enum, Enum): + elif isinstance(enum, type) and issubclass(enum, Enum): # Python's Enum i2s = self.i2s = {} s2i = self.s2i = {} @@ -2472,7 +2581,7 @@ def __init__(self, def any2i_one(self, pkt, x): # type: (Optional[Packet], Any) -> I - if Enum and isinstance(x, Enum): + if isinstance(x, Enum): return cast(I, x.value) elif isinstance(x, str): if self.s2i: @@ -2481,6 +2590,10 @@ def any2i_one(self, pkt, x): x = self.s2i_cb(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): @@ -2493,7 +2606,7 @@ def i2repr_one(self, pkt, x): 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]] @@ -2537,11 +2650,11 @@ class CharEnumField(EnumField[str]): def __init__(self, name, # type: str default, # type: str - enum, # type: Union[Dict[str, str], Tuple[Callable[[str], str], Callable[[str], str]]] # noqa: E501 + enum, # type: _EnumType[str] fmt="1s", # type: str ): # type: (...) -> None - EnumField.__init__(self, name, default, enum, fmt) + 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: @@ -2560,8 +2673,14 @@ def any2i_one(self, pkt, x): class BitEnumField(_BitField[Union[List[int], int]], _EnumField[int]): __slots__ = EnumField.__slots__ - def __init__(self, name, default, size, enum, **kwargs): - # type: (str, Optional[int], int, Dict[int, str], **Any) -> None + 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) _BitField.__init__(self, name, default, size, **kwargs) @@ -2577,34 +2696,82 @@ def i2repr(self, return _EnumField.i2repr(self, pkt, x) +class BitLenEnumField(BitLenField, _EnumField[int]): + __slots__ = EnumField.__slots__ + + 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 ShortEnumField(EnumField[int]): __slots__ = EnumField.__slots__ def __init__(self, name, # type: str - default, # type: int - enum, # type: Union[Dict[int, str], Dict[str, int], Tuple[Callable[[int], str], Callable[[str], int]], DADict[int, str]] # noqa: E501 + default, # type: Optional[int] + enum, # type: _EnumType[int] ): # type: (...) -> None - EnumField.__init__(self, name, default, enum, "H") + super(ShortEnumField, self).__init__(name, default, enum, "H") class LEShortEnumField(EnumField[int]): - def __init__(self, name, default, enum): - # type: (str, int, Union[Dict[int, str], List[str]]) -> None - EnumField.__init__(self, name, default, enum, " 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, default, enum): - # type: (str, int, Union[Dict[int, str], List[str]]) -> None - EnumField.__init__(self, name, default, enum, " None + super(LELongEnumField, self).__init__(name, default, enum, " None - EnumField.__init__(self, name, default, enum, "B") + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: _EnumType[int] + ): + # type: (...) -> None + super(ByteEnumField, self).__init__(name, default, enum, "B") class XByteEnumField(ByteEnumField): @@ -2624,36 +2791,71 @@ def i2repr_one(self, pkt, x): class IntEnumField(EnumField[int]): - def __init__(self, name, default, enum): - # type: (str, Optional[int], Dict[int, str]) -> None - EnumField.__init__(self, name, default, enum, "I") + 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[int]): - def __init__(self, name, default, enum): - # type: (str, Optional[int], Dict[int, str]) -> None - EnumField.__init__(self, name, default, enum, "i") + 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[int]): - def __init__(self, name, default, enum): - # type: (str, int, Dict[int, str]) -> None - 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): - # type: (Optional[Packet], int) -> str - if self not in conf.noenum and not isinstance(x, VolatileValue): - if self.i2s is not None: - try: - return self.i2s[x] - except KeyError: - pass - elif self.i2s_cb: - 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 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) @@ -2674,7 +2876,7 @@ def __init__(self, for m in enum: s2i = {} # type: Dict[str, I] self.s2i_multi[m] = s2i - for k, v in six.iteritems(enum[m]): + for k, v in enum[m].items(): s2i[v] = k self.s2i_all[v] = k Field.__init__(self, name, default, fmt) @@ -2724,7 +2926,11 @@ def __init__( def any2i(self, pkt, x): # type: (Optional[Packet], Any) -> Union[List[int], int] - return _MultiEnumField.any2i(self, pkt, x) + return _MultiEnumField[int].any2i( + self, # type: ignore + pkt, + x + ) def i2repr( # type: ignore self, @@ -2732,7 +2938,11 @@ def i2repr( # type: ignore x # type: Union[List[int], int] ): # type: (...) -> Union[str, List[str]] - return _MultiEnumField.i2repr(self, pkt, x) + return _MultiEnumField[int].i2repr( + self, # type: ignore + pkt, + x + ) class ByteEnumKeysField(ByteEnumField): @@ -2766,7 +2976,7 @@ class LEFieldLenField(FieldLenField): def __init__( self, name, # type: str - default, # type: int + default, # type: Optional[Any] length_of=None, # type: Optional[str] fmt=" 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 @@ -2953,7 +3163,7 @@ def __getattr__(self, attr): def __setattr__(self, attr, value): # type: (str, Union[List[str], int, str]) -> None - if attr == "value" and not isinstance(value, six.integer_types): + if attr == "value" and not isinstance(value, int): raise ValueError(value) if attr in self.__slots__: return super(FlagValue, self).__setattr__(attr, value) @@ -3019,18 +3229,19 @@ def __init__(self, name, # type: str default, # type: Optional[Union[int, FlagValue]] size, # type: int - names # type: Union[List[str], str, Dict[int, str]] + 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 six.viewitems(names): + 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 - super(FlagsField, self).__init__(name, default, size) + super(FlagsField, self).__init__(name, default, size, **kwargs) def _fixup_val(self, x): # type: (Any) -> Optional[FlagValue] @@ -3101,7 +3312,7 @@ 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 @@ -3122,7 +3333,7 @@ def i2m(self, pkt, x): 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 @@ -3154,7 +3365,7 @@ def i2repr(self, pkt, x): 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 @@ -3180,8 +3391,10 @@ def any2i(self, pkt, val): return (ival << self.frac_bits) | fract def i2h(self, pkt, val): - # type: (Optional[Packet], int) -> EDecimal + # 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 pw = 2.0**self.frac_bits frac_part = EDecimal(val & (1 << self.frac_bits) - 1) @@ -3329,8 +3542,6 @@ class UTCTimeField(Field[float, int]): __slots__ = ["epoch", "delta", "strf", "use_msec", "use_micro", "use_nano", "custom_scaling"] - # Do not change the order of the keywords in here - # Netflow heavily rely on this def __init__(self, name, # type: str default, # type: int @@ -3365,9 +3576,14 @@ def i2repr(self, pkt, x): x = x / 1e9 elif self.custom_scaling: x = x / self.custom_scaling - x = int(x) + self.delta - t = time.strftime(self.strf, time.gmtime(x)) - return "%s (%d)" % (t, x) + 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): # type: (Optional[Packet], Optional[float]) -> int @@ -3388,8 +3604,6 @@ def i2m(self, pkt, 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, @@ -3715,9 +3929,9 @@ def i2repr(self, return _EnumField.i2repr(self, pkt, x) -class BitExtendedField(Field[Optional[int], bytes]): +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: @@ -3733,101 +3947,44 @@ class BitExtendedField(Field[Optional[int], bytes]): __slots__ = ["extension_bit"] - def prepare_byte(self, x): - # type: (int) -> int - # 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=b""): - # type: (bytes) -> Tuple[bytes, Optional[int]] - # 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 b"", None - else: - return end, bits - - def extended2str(self, x): - # type: (Optional[int]) -> bytes - if x is None: - return b"" - 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): - # type: (Optional[Any], Optional[int]) -> bytes - return self.extended2str(x) - - def m2i(self, pkt, x): - # type: (Optional[Any], bytes) -> Optional[int] - return self.str2extended(x)[1] - def addfield(self, pkt, s, val): # type: (Optional[Packet], bytes, Optional[int]) -> bytes - return s + self.i2m(pkt, val) + 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): # type: (Optional[Any], bytes) -> Tuple[bytes, Optional[int]] - return self.str2extended(s) + 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): diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py new file mode 100644 index 00000000000..023c002354c --- /dev/null +++ b/scapy/fwdmachine.py @@ -0,0 +1,499 @@ +# 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) + 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 index 0afc9d7fe2d..70846b91be5 100644 --- a/scapy/interfaces.py +++ b/scapy/interfaces.py @@ -12,16 +12,15 @@ from collections import defaultdict from scapy.config import conf -from scapy.consts import WINDOWS +from scapy.consts import WINDOWS, LINUX from scapy.utils import pretty_list from scapy.utils6 import in6_isvalid -from scapy.libs.six.moves import UserDict -import scapy.libs.six as six - # Typing imports import scapy -from scapy.compat import ( +from scapy.compat import UserDict +from typing import ( + cast, Any, DefaultDict, Dict, @@ -36,7 +35,7 @@ class InterfaceProvider(object): name = "Unknown" - headers = ("Index", "Name", "MAC", "IPv4", "IPv6") + headers: Tuple[str, ...] = ("Index", "Name", "MAC", "IPv4", "IPv6") header_sort = 1 libpcap = False @@ -51,19 +50,27 @@ def reload(self): """Same than load() but for reloads. By default calls load""" return self.load() - def l2socket(self): - # type: () -> Type[scapy.supersocket.SuperSocket] + 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): - # type: () -> Type[scapy.supersocket.SuperSocket] + 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): - # type: () -> Type[scapy.supersocket.SuperSocket] + 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): @@ -75,7 +82,7 @@ def _format(self, dev, # type: NetworkInterface **kwargs # type: Any ): - # type: (...) -> Tuple[str, str, str, List[str], List[str]] + # 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 @@ -90,6 +97,12 @@ def _format(self, 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, @@ -104,6 +117,7 @@ def __init__(self, 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: @@ -119,6 +133,7 @@ def update(self, data): 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) @@ -158,15 +173,15 @@ def is_valid(self): def l2socket(self): # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l2socket() + return self.provider._l2socket(self) def l2listen(self): # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l2listen() + return self.provider._l2listen(self) - def l3socket(self): - # type: () -> Type[scapy.supersocket.SuperSocket] - return self.provider.l3socket() + def l3socket(self, ipv6=False): + # type: (bool) -> Type[scapy.supersocket.SuperSocket] + return self.provider._l3socket(self, ipv6) def __repr__(self): # type: () -> str @@ -190,20 +205,20 @@ def __radd__(self, other): _GlobInterfaceType = Union[NetworkInterface, str] -class NetworkInterfaceDict(UserDict): +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 - UserDict.__init__(self) + super(NetworkInterfaceDict, self).__init__() def _load(self, dat, # type: Dict[str, NetworkInterface] prov, # type: InterfaceProvider ): # type: (...) -> None - for ifname, iface in six.iteritems(dat): + for ifname, iface in dat.items(): if ifname in self.data: # Handle priorities: keep except if libpcap if prov.libpcap: @@ -215,6 +230,9 @@ 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 @@ -224,7 +242,7 @@ def load_confiface(self): # 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() + conf.iface = get_working_if() # type: ignore def _reload_provs(self): # type: () -> None @@ -235,8 +253,10 @@ def _reload_provs(self): def reload(self): # type: () -> None self._reload_provs() - if conf.route: - self.load_confiface() + if not conf.route: + # routes are not loaded yet. + return + self.load_confiface() def dev_from_name(self, name): # type: (str) -> NetworkInterface @@ -244,7 +264,7 @@ def dev_from_name(self, name): device name. """ try: - return next(iface for iface in six.itervalues(self) # type: ignore + 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) @@ -253,7 +273,7 @@ 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 six.itervalues(self) # type: ignore + return next(iface for iface in self.values() # type: ignore if iface.network_name == network_name) except (StopIteration, RuntimeError): raise ValueError( @@ -265,7 +285,7 @@ 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) # type: ignore + return next(iface for iface in self.values() if iface.index == if_index) except (StopIteration, RuntimeError): if str(if_index) == "1": @@ -273,8 +293,11 @@ def dev_from_index(self, if_index): return self.dev_from_networkname(conf.loopback_name) raise ValueError("Unknown network interface index %r" % if_index) - def _add_fake_iface(self, ifname): - # type: (str) -> None + 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, @@ -282,13 +305,14 @@ def _add_fake_iface(self, ifname): 'network_name': ifname, 'index': -1000, 'dummy': True, - 'mac': '00:00:00:00:00:00', + 'mac': mac, 'flags': 0, - 'ips': ["127.0.0.1", "::"], + 'ips': ips, # Windows only 'guid': "{%s}" % uuid.uuid1(), 'ipv4_metric': 0, 'ipv6_metric': 0, + 'nameservers': [], } if WINDOWS: from scapy.arch.windows import NetworkInterface_Win, \ @@ -318,15 +342,16 @@ def show(self, print_result=True, hidden=False, **kwargs): if not hidden and not dev.is_valid(): continue prov = dev.provider - res[prov].append( + res[(prov.headers, prov.header_sort)].append( (prov.name,) + prov._format(dev, **kwargs) ) output = "" - for provider in res: + for key in res: + hdrs, sortBy = key output += pretty_list( - res[provider], - [("Source",) + provider.headers], - sortBy=provider.header_sort + res[key], + [("Source",) + hdrs], + sortBy=sortBy ) + "\n" output = output[:-1] if print_result: @@ -350,7 +375,7 @@ def get_if_list(): def get_working_if(): - # type: () -> NetworkInterface + # type: () -> Optional[NetworkInterface] """Return an interface that works""" # return the interface associated with the route with smallest # mask (route by default if it exists) @@ -360,11 +385,17 @@ def get_working_if(): # 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()): - iface = resolve_iface(ifname) - if iface.is_valid(): - return iface + try: + iface = conf.ifaces.dev_from_networkname(ifname) # type: ignore + if iface.is_valid(): + return iface + except ValueError: + pass # There is no hope left - return resolve_iface(conf.loopback_name) + try: + return conf.ifaces.dev_from_networkname(conf.loopback_name) + except ValueError: + return None def get_working_ifaces(): @@ -385,8 +416,8 @@ def dev_from_index(if_index): return conf.ifaces.dev_from_index(if_index) -def resolve_iface(dev): - # type: (_GlobInterfaceType) -> NetworkInterface +def resolve_iface(dev, retry=True): + # type: (_GlobInterfaceType, bool) -> NetworkInterface """ Resolve an interface name into the interface """ @@ -396,19 +427,14 @@ def resolve_iface(dev): return conf.ifaces.dev_from_name(dev) except ValueError: try: - return dev_from_networkname(dev) + return conf.ifaces.dev_from_networkname(dev) except ValueError: pass - # Return a dummy interface - return NetworkInterface( - InterfaceProvider(), - data={ - "name": dev, - "description": dev, - "network_name": dev, - "dummy": True - } - ) + 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): diff --git a/scapy/layers/__init__.py b/scapy/layers/__init__.py index 79156832c8e..74f9906a2f4 100644 --- a/scapy/layers/__init__.py +++ b/scapy/layers/__init__.py @@ -6,3 +6,6 @@ """ 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 ad6e4d0c6ae..58b9a8bf679 100644 --- a/scapy/layers/all.py +++ b/scapy/layers/all.py @@ -7,17 +7,16 @@ All layers. Configurable with conf.load_layers. """ -from __future__ import absolute_import + +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.libs.six as six - -ignored = list(six.moves.builtins.__dict__) + ["sys"] +ignored = list(builtins.__dict__) + ["sys"] log = logging.getLogger("scapy.loading") __all__ = [] diff --git a/scapy/layers/bluetooth.py b/scapy/layers/bluetooth.py index 51a13e9ee65..0122559e5f0 100644 --- a/scapy/layers/bluetooth.py +++ b/scapy/layers/bluetooth.py @@ -4,6 +4,7 @@ # Copyright (C) Philippe Biondi # Copyright (C) Mike Ryan # Copyright (C) Michael Farrell +# Copyright (C) Haram Park """ Bluetooth layers, sockets and send/receive functions. @@ -17,76 +18,66 @@ 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 ( BitField, + XBitField, ByteEnumField, ByteField, - Field, 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, ) 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.libs 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]) +############ +# Consts # +############ - 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 +# From hci.h +HCI_CHANNEL_RAW = 0 +HCI_CHANNEL_USER = 1 +HCI_CHANNEL_MONITOR = 2 +HCI_CHANNEL_CONTROL = 3 +HCI_CHANNEL_LOGGING = 4 - def i2repr(self, pkt, x): - x = self.i2h(pkt, x) - if self in conf.resolve: - x = conf.manufdb._resolve_MAC(x) - return x - - def randval(self): - return RandMAC() +HCI_DEV_NONE = 0xffff ########## @@ -197,7 +188,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", @@ -209,6 +200,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" @@ -247,12 +323,33 @@ def post_build(self, p, pay): class L2CAP_CmdHdr(Packet): name = "L2CAP command header" fields_desc = [ - ByteEnumField("code", 8, {1: "rej", 2: "conn_req", 3: "conn_resp", - 4: "conf_req", 5: "conf_resp", 6: "disconn_req", # noqa: E501 - 7: "disconn_resp", 8: "echo_req", 9: "echo_resp", # noqa: E501 - 10: "info_req", 11: "info_resp", 18: "conn_param_update_req", # noqa: E501 - 19: "conn_param_update_resp"}), - ByteField("id", 0), + ByteEnumField("code", 8, {1: "rej", + 2: "conn_req", + 3: "conn_resp", + 4: "conf_req", + 5: "conf_resp", + 6: "disconn_req", + 7: "disconn_resp", + 8: "echo_req", + 9: "echo_resp", + 10: "info_req", + 11: "info_resp", + 12: "create_channel_req", + 13: "create_channel_resp", + 14: "move_channel_req", + 15: "move_channel_resp", + 16: "move_channel_confirm_req", + 17: "move_channel_confirm_resp", + 18: "conn_param_update_req", + 19: "conn_param_update_resp", + 20: "LE_credit_based_conn_req", + 21: "LE_credit_based_conn_resp", + 22: "flow_control_credit_ind", + 23: "credit_based_conn_req", + 24: "credit_based_conn_resp", + 25: "credit_based_reconf_req", + 26: "credit_based_reconf_resp"}), + ByteField("id", 1), LEShortField("len", None)] def post_build(self, p, pay): @@ -274,7 +371,22 @@ def answers(self, other): class L2CAP_ConnReq(Packet): name = "L2CAP Conn Req" - fields_desc = [LEShortEnumField("psm", 0, {1: "SDP", 3: "RFCOMM", 5: "telephony control"}), # noqa: E501 + fields_desc = [LEShortEnumField("psm", 0, {1: "SDP", + 3: "RFCOMM", + 5: "TCS-BIN", + 7: "TCS-BIN-CORDLESS", + 15: "BNEP", + 17: "HID-Control", + 19: "HID-Interrupt", + 21: "UPnP", + 23: "AVCTP-Control", + 25: "AVDTP", + 27: "AVCTP-Browsing", + 29: "UDI_C-Plane", + 31: "ATT", + 33: "3DSP", + 35: "IPSP", + 37: "OTS"}), LEShortField("scid", 0), ] @@ -332,6 +444,16 @@ def answers(self, other): return self.scid == other.scid +class L2CAP_EchoReq(Packet): + name = "L2CAP Echo Req" + fields_desc = [StrField("data", ""), ] + + +class L2CAP_EchoResp(Packet): + name = "L2CAP Echo Resp" + fields_desc = [StrField("data", ""), ] + + class L2CAP_InfoReq(Packet): name = "L2CAP Info Req" fields_desc = [LEShortEnumField("type", 0, {1: "CL_MTU", 2: "FEAT_MASK"}), @@ -349,6 +471,78 @@ def answers(self, other): return self.type == other.type +class L2CAP_Create_Channel_Request(Packet): + name = "L2CAP Create Channel Request" + fields_desc = [LEShortEnumField("psm", 0, {1: "SDP", + 3: "RFCOMM", + 5: "TCS-BIN", + 7: "TCS-BIN-CORDLESS", + 15: "BNEP", + 17: "HID-Control", + 19: "HID-Interrupt", + 21: "UPnP", + 23: "AVCTP-Control", + 25: "AVDTP", + 27: "AVCTP-Browsing", + 29: "UDI_C-Plane", + 31: "ATT", + 33: "3DSP", + 35: "IPSP", + 37: "OTS"}), + LEShortField("scid", 0), + ByteField("controller_id", 0), ] + + +class L2CAP_Create_Channel_Response(Packet): + name = "L2CAP Create Channel Response" + fields_desc = [LEShortField("dcid", 0), + LEShortField("scid", 0), + LEShortEnumField("result", 0, { + 0: "Connection successful", + 1: "Connection pending", + 2: "Connection refused - PSM not supported", + 3: "Connection refused - security block", + 4: "Connection refused - no resources available", + 5: "Connection refused - cont_ID not supported", + 6: "Connection refused - invalid scid", + 7: "Connection refused - scid already allocated"}), + LEShortEnumField("status", 0, { + 0: "No further information available", + 1: "Authentication pending", + 2: "Authorization pending"}), ] + + +class L2CAP_Move_Channel_Request(Packet): + name = "L2CAP Move Channel Request" + fields_desc = [LEShortField("icid", 0), + ByteField("dest_controller_id", 0), ] + + +class L2CAP_Move_Channel_Response(Packet): + name = "L2CAP Move Channel Response" + fields_desc = [LEShortField("icid", 0), + LEShortEnumField("result", 0, { + 0: "Move success", + 1: "Move pending", + 2: "Move refused - Cont_ID not supported", + 3: "Move refused - Cont_ID is same as old one", + 4: "Move refused - Configuration not supported", + 5: "Move refused - Move channel collision", + 6: "Move refused - Not allowed to be moved"}), ] + + +class L2CAP_Move_Channel_Confirmation_Request(Packet): + name = "L2CAP Move Channel Confirmation Request" + fields_desc = [LEShortField("icid", 0), + LEShortEnumField("result", 0, {0: "Move success", + 1: "Move failure"}), ] + + +class L2CAP_Move_Channel_Confirmation_Response(Packet): + name = "L2CAP Move Channel Confirmation Response" + fields_desc = [LEShortField("icid", 0), ] + + class L2CAP_Connection_Parameter_Update_Request(Packet): name = "L2CAP Connection Parameter Update Request" fields_desc = [LEShortField("min_interval", 0), @@ -362,6 +556,86 @@ class L2CAP_Connection_Parameter_Update_Response(Packet): fields_desc = [LEShortField("move_result", 0), ] +class L2CAP_LE_Credit_Based_Connection_Request(Packet): + name = "L2CAP LE Credit Based Connection Request" + fields_desc = [LEShortField("spsm", 0), + LEShortField("scid", 0), + LEShortField("mtu", 0), + LEShortField("mps", 0), + LEShortField("initial_credits", 0), ] + + +class L2CAP_LE_Credit_Based_Connection_Response(Packet): + name = "L2CAP LE Credit Based Connection Response" + fields_desc = [LEShortField("dcid", 0), + LEShortField("mtu", 0), + LEShortField("mps", 0), + LEShortField("initial_credits", 0), + LEShortEnumField("result", 0, { + 0: "Connection successful", + 2: "Connection refused - SPSM not supported", + 4: "Connection refused - no resources available", + 5: "Connection refused - authentication error", + 6: "Connection refused - authorization error", + 7: "Connection refused - encrypt_key size error", + 8: "Connection refused - insufficient encryption", + 9: "Connection refused - invalid scid", + 10: "Connection refused - scid already allocated", + 11: "Connection refused - parameters error"}), ] + + +class L2CAP_Flow_Control_Credit_Ind(Packet): + name = "L2CAP Flow Control Credit Ind" + fields_desc = [LEShortField("cid", 0), + LEShortField("credits", 0), ] + + +class L2CAP_Credit_Based_Connection_Request(Packet): + name = "L2CAP Credit Based Connection Request" + fields_desc = [LEShortField("spsm", 0), + LEShortField("mtu", 0), + LEShortField("mps", 0), + LEShortField("initial_credits", 0), + LEShortField("scid", 0), ] + + +class L2CAP_Credit_Based_Connection_Response(Packet): + name = "L2CAP Credit Based Connection Response" + fields_desc = [LEShortField("mtu", 0), + LEShortField("mps", 0), + LEShortField("initial_credits", 0), + LEShortEnumField("result", 0, { + 0: "All connection successful", + 2: "All connection refused - SPSM not supported", + 4: "Some connections refused - resources error", + 5: "All connection refused - authentication error", + 6: "All connection refused - authorization error", + 7: "All connection refused - encrypt_key size error", + 8: "All connection refused - encryption error", + 9: "Some connection refused - invalid scid", + 10: "Some connection refused - scid already allocated", + 11: "All Connection refused - unacceptable parameters", + 12: "All connections refused - invalid parameters"}), + LEShortField("dcid", 0), ] + + +class L2CAP_Credit_Based_Reconfigure_Request(Packet): + name = "L2CAP Credit Based Reconfigure Request" + fields_desc = [LEShortField("mtu", 0), + LEShortField("mps", 0), + LEShortField("dcid", 0), ] + + +class L2CAP_Credit_Based_Reconfigure_Response(Packet): + name = "L2CAP Credit Based Reconfigure Response" + fields_desc = [LEShortEnumField("result", 0, { + 0: "Reconfig successful", + 1: "Reconfig failed - MTU size reduction not allowed", + 2: "Reconfig failed - MPS size reduction not allowed", + 3: "Reconfig failed - one or more dcids invalid", + 4: "Reconfig failed - unacceptable parameters"}), ] + + class ATT_Hdr(Packet): name = "ATT header" fields_desc = [XByteField("opcode", None), ] @@ -664,6 +938,11 @@ class SM_Signing_Information(Packet): fields_desc = [StrFixedLenField("csrk", b'\x00' * 16, 16), ] +class SM_Security_Request(Packet): + name = "Security Request" + fields_desc = [BitField("auth_req", 0, 8), ] + + class SM_Public_Key(Packet): name = "Public Key" fields_desc = [StrFixedLenField("key_x", b'\x00' * 32, 32), @@ -733,6 +1012,12 @@ class EIR_Hdr(Packet): def mysummary(self): return self.sprintf("EIR %type%") + def guess_payload_class(self, payload): + if self.len == 0: + # For Extended_Inquiry_Response, stop when len=0 + return conf.padding_layer + return super(EIR_Hdr, self).guess_payload_class(payload) + class EIR_Element(Packet): name = "EIR Element" @@ -780,6 +1065,19 @@ class EIR_IncompleteList16BitServiceUUIDs(EIR_CompleteList16BitServiceUUIDs): name = "Incomplete list of 16-bit service UUIDs" +class EIR_CompleteList32BitServiceUUIDs(EIR_Element): + name = 'Complete list of 32-bit service UUIDs' + fields_desc = [ + # https://www.bluetooth.com/specifications/assigned-numbers + FieldListField('svc_uuids', None, XLEIntField('uuid', 0), + length_from=EIR_Element.length_from) + ] + + +class EIR_IncompleteList32BitServiceUUIDs(EIR_CompleteList32BitServiceUUIDs): + name = 'Incomplete list of 32-bit service UUIDs' + + class EIR_CompleteList128BitServiceUUIDs(EIR_Element): name = "Complete list of 128-bit service UUIDs" fields_desc = [ @@ -809,11 +1107,78 @@ class EIR_TX_Power_Level(EIR_Element): fields_desc = [SignedByteField("level", 0)] +class EIR_ClassOfDevice(EIR_Element): + name = 'Class of device' + fields_desc = [ + FlagsField('major_service_classes', 0, 11, [ + 'limited_discoverable_mode', + 'le_audio', + 'reserved', + 'positioning', + 'networking', + 'rendering', + 'capturing', + 'object_transfer', + 'audio', + 'telephony', + 'information' + ], tot_size=-3), + BitEnumField('major_device_class', 0, 5, { + 0x00: 'miscellaneous', + 0x01: 'computer', + 0x02: 'phone', + 0x03: 'lan', + 0x04: 'audio_video', + 0x05: 'peripheral', + 0x06: 'imaging', + 0x07: 'wearable', + 0x08: 'toy', + 0x09: 'health', + 0x1f: 'uncategorized' + }), + BitField('minor_device_class', 0, 6), + BitField('fixed', 0, 2, end_tot_size=-3) + ] + + +class EIR_SecureSimplePairingHashC192(EIR_Element): + name = 'Secure Simple Pairing Hash C-192' + fields_desc = [NBytesField('hash', 0, 16)] + + +class EIR_SecureSimplePairingRandomizerR192(EIR_Element): + name = 'Secure Simple Pairing Randomizer R-192' + fields_desc = [NBytesField('randomizer', 0, 16)] + + +class EIR_SecurityManagerOOBFlags(EIR_Element): + name = 'Security Manager Out of Band Flags' + fields_desc = [ + BitField('oob_flags_field', 0, 1), + BitField('le_supported', 0, 1), + BitField('previously_used', 0, 1), + BitField('address_type', 0, 1), + BitField('reserved', 0, 4) + ] + + +class EIR_PeripheralConnectionIntervalRange(EIR_Element): + name = 'Peripheral Connection Interval Range' + fields_desc = [ + LEShortField('conn_interval_min', 0xFFFF), + LEShortField('conn_interval_max', 0xFFFF) + ] + + class EIR_Manufacturer_Specific_Data(EIR_Element): name = "EIR Manufacturer Specific Data" + deprecated_fields = { + "company_id": ("company_identifier", "2.6.2"), + } fields_desc = [ # https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers - XLEShortField("company_id", None), + LEShortEnumField("company_identifier", None, + BLUETOOTH_CORE_COMPANY_IDENTIFIERS), ] registered_magic_payloads = {} @@ -859,8 +1224,9 @@ def register_magic_payload(cls, payload_cls, magic_check=None): cls.registered_magic_payloads[payload_cls] = magic_check def default_payload_class(self, payload): - for cls, check in six.iteritems( - EIR_Manufacturer_Specific_Data.registered_magic_payloads): + for cls, check in ( + EIR_Manufacturer_Specific_Data.registered_magic_payloads.items() + ): if check(payload): return cls @@ -882,6 +1248,30 @@ class EIR_Device_ID(EIR_Element): ] +class EIR_ServiceSolicitation16BitUUID(EIR_Element): + name = "EIR Service Solicitation - 16-bit UUID" + fields_desc = [ + XLEShortField("svc_uuid", None) + ] + + def extract_padding(self, s): + # Needed to end each EIR_Element packet and make PacketListField work. + plen = EIR_Element.length_from(self) - 2 + return s[:plen], s[plen:] + + +class EIR_ServiceSolicitation128BitUUID(EIR_Element): + name = "EIR Service Solicitation - 128-bit UUID" + fields_desc = [ + UUIDField('svc_uuid', None, uuid_fmt=UUIDField.FORMAT_REV) + ] + + def extract_padding(self, s): + # Needed to end each EIR_Element packet and make PacketListField work. + plen = EIR_Element.length_from(self) - 2 + return s[:plen], s[plen:] + + class EIR_ServiceData16BitUUID(EIR_Element): name = "EIR Service Data - 16-bit UUID" fields_desc = [ @@ -895,14 +1285,345 @@ def extract_padding(self, s): return s[:plen], s[plen:] +class EIR_PublicTargetAddress(EIR_Element): + name = "Public Target Address" + fields_desc = [ + LEMACField('bd_addr', None) + ] + + +class EIR_AdvertisingInterval(EIR_Element): + name = "Advertising Interval" + fields_desc = [ + MultipleTypeField( + [ + (ByteField("advertising_interval", 0), + lambda p: p.underlayer.len - 1 == 1), + (LEShortField("advertising_interval", 0), + lambda p: p.underlayer.len - 1 == 2), + (LEThreeBytesField("advertising_interval", 0), + lambda p: p.underlayer.len - 1 == 3), + (LEIntField("advertising_interval", 0), + lambda p: p.underlayer.len - 1 == 4), + ], + LEShortField("advertising_interval", 0) + ) + ] + + +class EIR_LEBluetoothDeviceAddress(EIR_Element): + name = "LE Bluetooth Device Address" + fields_desc = [ + XBitField('reserved', 0, 7, tot_size=-1), + BitEnumField('addr_type', 0, 1, end_tot_size=-1, enum={ + 0x0: 'Public', + 0x1: 'Random' + }), + LEMACField('bd_addr', None) + ] + + +class EIR_Appearance(EIR_Element): + name = "EIR_Appearance" + fields_desc = [ + BitEnumField('category', 0, 10, tot_size=-2, enum={ + 0x000: 'Unknown', + 0x001: 'Phone', + 0x002: 'Computer', + 0x003: 'Watch', + 0x004: 'Clock', + 0x005: 'Display', + 0x006: 'Remote Control', + 0x007: 'Eyeglasses', + 0x008: 'Tag', + 0x009: 'Keyring', + 0x00A: 'Media Player', + 0x00B: 'Barcode Scanner', + 0x00C: 'Thermometer', + 0x00D: 'Heart Rate Sensor', + 0x00E: 'Blood Pressure', + 0x00F: 'Human Interface Device', + 0x010: 'Glucose Meter', + 0x011: 'Running Walking Sensor', + 0x012: 'Cycling', + 0x013: 'Control Device', + 0x014: 'Network Device', + 0x015: 'Sensor', + 0x016: 'Light Fixtures', + 0x017: 'Fan', + 0x018: 'HVAC', + 0x019: 'Air Conditioning', + 0x01A: 'Humidifier', + 0x01B: 'Heating', + 0x01C: 'Access Control', + 0x01D: 'Motorized Device', + 0x01E: 'Power Device', + 0x01F: 'Light Source', + 0x020: 'Window Covering', + 0x021: 'Audio Sink', + 0x022: 'Audio Source', + 0x023: 'Motorized Vehicle', + 0x024: 'Domestic Appliance', + 0x025: 'Wearable Audio Device', + 0x026: 'Aircraft', + 0x027: 'AV Equipment', + 0x028: 'Display Equipment', + 0x029: 'Hearing aid', + 0x02A: 'Gaming', + 0x02B: 'Signage', + 0x031: 'Pulse Oximeter', + 0x032: 'Weight Scale', + 0x033: 'Personal Mobility Device', + 0x034: 'Continuous Glucose Monitor', + 0x035: 'Insulin Pump', + 0x036: 'Medication Delivery', + 0x037: 'Spirometer', + 0x051: 'Outdoor Sports Activity' + }), + XBitField('subcategory', 0, 6, end_tot_size=-2) + ] + + @property + def appearance(self): + return (self.category << 6) + self.subcategory + + +class EIR_ServiceData32BitUUID(EIR_Element): + name = 'EIR Service Data - 32-bit UUID' + fields_desc = [ + XLEIntField('svc_uuid', 0), + ] + + def extract_padding(self, s): + # Needed to end each EIR_Element packet and make PacketListField work. + plen = EIR_Element.length_from(self) - 4 + return s[:plen], s[plen:] + + +class EIR_ServiceData128BitUUID(EIR_Element): + name = 'EIR Service Data - 128-bit UUID' + fields_desc = [ + UUIDField('svc_uuid', None, uuid_fmt=UUIDField.FORMAT_REV) + ] + + def extract_padding(self, s): + # Needed to end each EIR_Element packet and make PacketListField work. + plen = EIR_Element.length_from(self) - 16 + return s[:plen], s[plen:] + + +class EIR_URI(EIR_Element): + name = 'EIR URI' + fields_desc = [ + ByteEnumField('scheme', 0, { + 0x01: '', + 0x02: 'aaa:', + 0x03: 'aaas:', + 0x04: 'about:', + 0x05: 'acap:', + 0x06: 'acct:', + 0x07: 'cap:', + 0x08: 'cid:', + 0x09: 'coap:', + 0x0A: 'coaps:', + 0x0B: 'crid:', + 0x0C: 'data:', + 0x0D: 'dav:', + 0x0E: 'dict:', + 0x0F: 'dns:', + 0x10: 'file:', + 0x11: 'ftp:', + 0x12: 'geo:', + 0x13: 'go:', + 0x14: 'gopher:', + 0x15: 'h323:', + 0x16: 'http:', + 0x17: 'https:', + 0x18: 'iax:', + 0x19: 'icap:', + 0x1A: 'im:', + 0x1B: 'imap:', + 0x1C: 'info:', + 0x1D: 'ipp:', + 0x1E: 'ipps:', + 0x1F: 'iris:', + 0x20: 'iris.beep:', + 0x21: 'iris.xpc:', + 0x22: 'iris.xpcs:', + 0x23: 'iris.lwz:', + 0x24: 'jabber:', + 0x25: 'ldap:', + 0x26: 'mailto:', + 0x27: 'mid:', + 0x28: 'msrp:', + 0x29: 'msrps:', + 0x2A: 'mtqp:', + 0x2B: 'mupdate:', + 0x2C: 'news:', + 0x2D: 'nfs:', + 0x2E: 'ni:', + 0x2F: 'nih:', + 0x30: 'nntp:', + 0x31: 'opaquelocktoken:', + 0x32: 'pop:', + 0x33: 'pres:', + 0x34: 'reload:', + 0x35: 'rtsp:', + 0x36: 'rtsps:', + 0x37: 'rtspu:', + 0x38: 'service:', + 0x39: 'session:', + 0x3A: 'shttp:', + 0x3B: 'sieve:', + 0x3C: 'sip:', + 0x3D: 'sips:', + 0x3E: 'sms:', + 0x3F: 'snmp:', + 0x40: 'soap.beep:', + 0x41: 'soap.beeps:', + 0x42: 'stun:', + 0x43: 'stuns:', + 0x44: 'tag:', + 0x45: 'tel:', + 0x46: 'telnet:', + 0x47: 'tftp:', + 0x48: 'thismessage:', + 0x49: 'tn3270:', + 0x4A: 'tip:', + 0x4B: 'turn:', + 0x4C: 'turns:', + 0x4D: 'tv:', + 0x4E: 'urn:', + 0x4F: 'vemmi:', + 0x50: 'ws:', + 0x51: 'wss:', + 0x52: 'xcon:', + 0x53: 'xconuserid:', + 0x54: 'xmlrpc.beep:', + 0x55: 'xmlrpc.beeps:', + 0x56: 'xmpp:', + 0x57: 'z39.50r:', + 0x58: 'z39.50s:', + 0x59: 'acr:', + 0x5A: 'adiumxtra:', + 0x5B: 'afp:', + 0x5C: 'afs:', + 0x5D: 'aim:', + 0x5E: 'apt:', + 0x5F: 'attachment:', + 0x60: 'aw:', + 0x61: 'barion:', + 0x62: 'beshare:', + 0x63: 'bitcoin:', + 0x64: 'bolo:', + 0x65: 'callto:', + 0x66: 'chrome:', + 0x67: 'chromeextension:', + 0x68: 'comeventbriteattendee:', + 0x69: 'content:', + 0x6A: 'cvs:', + 0x6B: 'dlnaplaysingle:', + 0x6C: 'dlnaplaycontainer:', + 0x6D: 'dtn:', + 0x6E: 'dvb:', + 0x6F: 'ed2k:', + 0x70: 'facetime:', + 0x71: 'feed:', + 0x72: 'feedready:', + 0x73: 'finger:', + 0x74: 'fish:', + 0x75: 'gg:', + 0x76: 'git:', + 0x77: 'gizmoproject:', + 0x78: 'gtalk:', + 0x79: 'ham:', + 0x7A: 'hcp:', + 0x7B: 'icon:', + 0x7C: 'ipn:', + 0x7D: 'irc:', + 0x7E: 'irc6:', + 0x7F: 'ircs:', + 0x80: 'itms:', + 0x81: 'jar:', + 0x82: 'jms:', + 0x83: 'keyparc:', + 0x84: 'lastfm:', + 0x85: 'ldaps:', + 0x86: 'magnet:', + 0x87: 'maps:', + 0x88: 'market:', + 0x89: 'message:', + 0x8A: 'mms:', + 0x8B: 'mshelp:', + 0x8C: 'mssettingspower:', + 0x8D: 'msnim:', + 0x8E: 'mumble:', + 0x8F: 'mvn:', + 0x90: 'notes:', + 0x91: 'oid:', + 0x92: 'palm:', + 0x93: 'paparazzi:', + 0x94: 'pkcs11:', + 0x95: 'platform:', + 0x96: 'proxy:', + 0x97: 'psyc:', + 0x98: 'query:', + 0x99: 'res:', + 0x9A: 'resource:', + 0x9B: 'rmi:', + 0x9C: 'rsync:', + 0x9D: 'rtmfp:', + 0x9E: 'rtmp:', + 0x9F: 'secondlife:', + 0xA0: 'sftp:', + 0xA1: 'sgn:', + 0xA2: 'skype:', + 0xA3: 'smb:', + 0xA4: 'smtp:', + 0xA5: 'soldat:', + 0xA6: 'spotify:', + 0xA7: 'ssh:', + 0xA8: 'steam:', + 0xA9: 'submit:', + 0xAA: 'svn:', + 0xAB: 'teamspeak:', + 0xAC: 'teliaeid:', + 0xAD: 'things:', + 0xAE: 'udp:', + 0xAF: 'unreal:', + 0xB0: 'ut2004:', + 0xB1: 'ventrilo:', + 0xB2: 'viewsource:', + 0xB3: 'webcal:', + 0xB4: 'wtai:', + 0xB5: 'wyciwyg:', + 0xB6: 'xfire:', + 0xB7: 'xri:', + 0xB8: 'ymsgr:', + 0xB9: 'example:', + 0xBA: 'mssettingscloudstorage:' + }), + StrLenField('uri_hier_part', None, length_from=EIR_Element.length_from) + ] + + @property + def uri(self): + return EIR_URI.scheme.i2s[self.scheme] + self.uri_hier_part.decode('utf-8') + + class HCI_Command_Hdr(Packet): name = "HCI Command header" - fields_desc = [XLEShortField("opcode", 0), + fields_desc = [XBitField("ogf", 0, 6, tot_size=-2), + XBitField("ocf", 0, 10, end_tot_size=-2), LenField("len", None, fmt="B"), ] def answers(self, other): return False + @property + def opcode(self): + return (self.ogf << 10) + self.ocf + def post_build(self, p, pay): p += pay if self.len is None: @@ -910,180 +1631,560 @@ def post_build(self, p, pay): return p -class HCI_Cmd_Reset(Packet): - name = "Reset" +# BUETOOTH CORE SPECIFICATION 5.4 | Vol 3, Part C +# 8 EXTENDED INQUIRY RESPONSE +class HCI_Extended_Inquiry_Response(Packet): + fields_desc = [ + PadField( + PacketListField( + "eir_data", [], + next_cls_cb=lambda *args: ( + (not args[2] or args[2].len != 0) and EIR_Hdr or conf.raw_layer + ) + ), + align=31, padwith=b"\0", + ), + ] -class HCI_Cmd_Set_Event_Filter(Packet): - name = "Set Event Filter" - fields_desc = [ByteEnumField("type", 0, {0: "clear"}), ] +# BLUETOOTH CORE SPECIFICATION Version 5.4 | Vol 4, Part E +# 7 HCI COMMANDS AND EVENTS +# 7.1 LINK CONTROL COMMANDS, the OGF is defined as 0x01 -class HCI_Cmd_Connect_Accept_Timeout(Packet): - name = "Connection Attempt Timeout" - fields_desc = [LEShortField("timeout", 32000)] # 32000 slots is 20000 msec +class HCI_Cmd_Inquiry(Packet): + """ + 7.1.1 Inquiry command + """ + name = "HCI_Inquiry" + fields_desc = [XLE3BytesField("lap", 0x9E8B33), + ByteField("inquiry_length", 0), + ByteField("num_responses", 0)] -class HCI_Cmd_LE_Host_Supported(Packet): - name = "LE Host Supported" - fields_desc = [ByteField("supported", 1), - ByteField("simultaneous", 1), ] +class HCI_Cmd_Inquiry_Cancel(Packet): + """ + 7.1.2 Inquiry Cancel command + """ + name = "HCI_Inquiry_Cancel" -class HCI_Cmd_Set_Event_Mask(Packet): - name = "Set Event Mask" - fields_desc = [StrFixedLenField("mask", b"\xff\xff\xfb\xff\x07\xf8\xbf\x3d", 8)] # noqa: E501 +class HCI_Cmd_Periodic_Inquiry_Mode(Packet): + """ + 7.1.3 Periodic Inquiry Mode command + """ + name = "HCI_Periodic_Inquiry_Mode" + fields_desc = [LEShortField("max_period_length", 0x0003), + LEShortField("min_period_length", 0x0002), + XLE3BytesField("lap", 0x9E8B33), + ByteField("inquiry_length", 0), + ByteField("num_responses", 0)] -class HCI_Cmd_Read_BD_Addr(Packet): - name = "Read BD Addr" +class HCI_Cmd_Exit_Peiodic_Inquiry_Mode(Packet): + """ + 7.1.4 Exit Periodic Inquiry Mode command + """ + name = "HCI_Exit_Periodic_Inquiry_Mode" -class HCI_Cmd_Write_Local_Name(Packet): - name = "Write Local Name" - fields_desc = [StrField("name", "")] +class HCI_Cmd_Create_Connection(Packet): + """ + 7.1.5 Create Connection command + """ + name = "HCI_Create_Connection" + fields_desc = [LEMACField("bd_addr", None), + LEShortField("packet_type", 0xcc18), + ByteField("page_scan_repetition_mode", 0x02), + ByteField("reserved", 0x0), + LEShortField("clock_offset", 0x0), + ByteField("allow_role_switch", 0x1), ] -class HCI_Cmd_Write_Extended_Inquiry_Response(Packet): - name = "Write Extended Inquiry Response" - fields_desc = [ByteField("fec_required", 0), - PacketListField("eir_data", [], EIR_Hdr, - length_from=lambda pkt:pkt.len)] +class HCI_Cmd_Disconnect(Packet): + """ + 7.1.6 Disconnect command + """ + name = "HCI_Disconnect" + fields_desc = [XLEShortField("handle", 0), + ByteField("reason", 0x13), ] -class HCI_Cmd_LE_Set_Scan_Parameters(Packet): - name = "LE Set Scan Parameters" - fields_desc = [ByteEnumField("type", 1, {1: "active"}), - XLEShortField("interval", 16), - XLEShortField("window", 16), - ByteEnumField("atype", 0, {0: "public"}), - ByteEnumField("policy", 0, {0: "all", 1: "whitelist"})] +class HCI_Cmd_Create_Connection_Cancel(Packet): + """ + 7.1.7 Create Connection Cancel command + """ + name = "HCI_Create_Connection_Cancel" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Set_Scan_Enable(Packet): - name = "LE Set Scan Enable" - fields_desc = [ByteField("enable", 1), - ByteField("filter_dups", 1), ] +class HCI_Cmd_Accept_Connection_Request(Packet): + """ + 7.1.8 Accept Connection Request command + """ + name = "HCI_Accept_Connection_Request" + fields_desc = [LEMACField("bd_addr", None), + ByteField("role", 0x1), ] -class HCI_Cmd_Disconnect(Packet): - name = "Disconnect" - fields_desc = [XLEShortField("handle", 0), - ByteField("reason", 0x13), ] +class HCI_Cmd_Reject_Connection_Response(Packet): + """ + 7.1.9 Reject Connection Request command + """ + name = "HCI_Reject_Connection_Response" + fields_desc = [LEMACField("bd_addr", None), + ByteField("reason", 0x1), ] -class HCI_Cmd_LE_Create_Connection(Packet): - name = "LE Create Connection" - fields_desc = [LEShortField("interval", 96), - LEShortField("window", 48), - ByteEnumField("filter", 0, {0: "address"}), - ByteEnumField("patype", 0, {0: "public", 1: "random"}), - LEMACField("paddr", None), - ByteEnumField("atype", 0, {0: "public", 1: "random"}), - LEShortField("min_interval", 40), - LEShortField("max_interval", 56), - LEShortField("latency", 0), - LEShortField("timeout", 42), - LEShortField("min_ce", 0), - LEShortField("max_ce", 0), ] +class HCI_Cmd_Link_Key_Request_Reply(Packet): + """ + 7.1.10 Link Key Request Reply command + """ + name = "HCI_Link_Key_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + NBytesField("link_key", None, 16), ] -class HCI_Cmd_LE_Create_Connection_Cancel(Packet): - name = "LE Create Connection Cancel" +class HCI_Cmd_Link_Key_Request_Negative_Reply(Packet): + """ + 7.1.11 Link Key Request Negative Reply command + """ + name = "HCI_Link_Key_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Read_White_List_Size(Packet): - name = "LE Read White List Size" +class HCI_Cmd_PIN_Code_Request_Reply(Packet): + """ + 7.1.12 PIN Code Request Reply command + """ + name = "HCI_PIN_Code_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + ByteField("pin_code_length", 7), + NBytesField("pin_code", b"\x00" * 16, sz=16), ] -class HCI_Cmd_LE_Clear_White_List(Packet): - name = "LE Clear White List" +class HCI_Cmd_PIN_Code_Request_Negative_Reply(Packet): + """ + 7.1.13 PIN Code Request Negative Reply command + """ + name = "HCI_PIN_Code_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Add_Device_To_White_List(Packet): - name = "LE Add Device to White List" - fields_desc = [ByteEnumField("atype", 0, {0: "public", 1: "random"}), - LEMACField("address", None)] +class HCI_Cmd_Change_Connection_Packet_Type(Packet): + """ + 7.1.14 Change Connection Packet Type command + """ + name = "HCI_Cmd_Change_Connection_Packet_Type" + fields_desc = [XLEShortField("connection_handle", None), + LEShortField("packet_type", 0), ] -class HCI_Cmd_LE_Remove_Device_From_White_List(HCI_Cmd_LE_Add_Device_To_White_List): # noqa: E501 - name = "LE Remove Device from White List" +class HCI_Cmd_Authentication_Requested(Packet): + """ + 7.1.15 Authentication Requested command + """ + name = "HCI_Authentication_Requested" + fields_desc = [LEShortField("handle", 0)] -class HCI_Cmd_LE_Connection_Update(Packet): - name = "LE Connection Update" - fields_desc = [XLEShortField("handle", 0), - XLEShortField("min_interval", 0), - XLEShortField("max_interval", 0), - XLEShortField("latency", 0), - XLEShortField("timeout", 0), - LEShortField("min_ce", 0), - LEShortField("max_ce", 0xffff), ] +class HCI_Cmd_Set_Connection_Encryption(Packet): + """ + 7.1.16 Set Connection Encryption command + """ + name = "HCI_Set_Connection_Encryption" + fields_desc = [LEShortField("handle", 0), ByteField("encryption_enable", 0)] -class HCI_Cmd_LE_Read_Buffer_Size(Packet): - name = "LE Read Buffer Size" +class HCI_Cmd_Change_Connection_Link_Key(Packet): + """ + 7.1.17 Change Connection Link Key command + """ + name = "HCI_Change_Connection_Link_Key" + fields_desc = [LEShortField("handle", 0), ] -class HCI_Cmd_LE_Read_Remote_Used_Features(Packet): - name = "LE Read Remote Used Features" - fields_desc = [LEShortField("handle", 64)] +class HCI_Cmd_Link_Key_Selection(Packet): + """ + 7.1.18 Change Connection Link Key command + """ + name = "HCI_Cmd_Link_Key_Selection" + fields_desc = [ByteEnumField("handle", 0, {0: "Use semi-permanent Link Keys", + 1: "Use Temporary Link Key", }), ] -class HCI_Cmd_LE_Set_Random_Address(Packet): - name = "LE Set Random Address" - fields_desc = [LEMACField("address", None)] +class HCI_Cmd_Remote_Name_Request(Packet): + """ + 7.1.19 Remote Name Request command + """ + name = "HCI_Remote_Name_Request" + fields_desc = [LEMACField("bd_addr", None), + ByteField("page_scan_repetition_mode", 0x02), + ByteField("reserved", 0x0), + LEShortField("clock_offset", 0x0), ] -class HCI_Cmd_LE_Set_Advertising_Parameters(Packet): - name = "LE Set Advertising Parameters" - fields_desc = [LEShortField("interval_min", 0x0800), - LEShortField("interval_max", 0x0800), - ByteEnumField("adv_type", 0, {0: "ADV_IND", 1: "ADV_DIRECT_IND", 2: "ADV_SCAN_IND", 3: "ADV_NONCONN_IND", 4: "ADV_DIRECT_IND_LOW"}), # noqa: E501 - ByteEnumField("oatype", 0, {0: "public", 1: "random"}), - ByteEnumField("datype", 0, {0: "public", 1: "random"}), - LEMACField("daddr", None), - ByteField("channel_map", 7), - ByteEnumField("filter_policy", 0, {0: "all:all", 1: "connect:all scan:whitelist", 2: "connect:whitelist scan:all", 3: "all:whitelist"}), ] # noqa: E501 +class HCI_Cmd_Remote_Name_Request_Cancel(Packet): + """ + 7.1.20 Remote Name Request Cancel command + """ + name = "HCI_Remote_Name_Request_Cancel" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Set_Advertising_Data(Packet): - name = "LE Set Advertising Data" - fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), - PadField( - PacketListField("data", [], EIR_Hdr, - length_from=lambda pkt:pkt.len), - align=31, padwith=b"\0"), ] +class HCI_Cmd_Read_Remote_Supported_Features(Packet): + """ + 7.1.21 Read Remote Supported Features command + """ + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEShortField("connection_handle", None), ] -class HCI_Cmd_LE_Set_Scan_Response_Data(Packet): - name = "LE Set Scan Response Data" - fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), - StrLenField("data", "", length_from=lambda pkt:pkt.len), ] +class HCI_Cmd_Read_Remote_Extended_Features(Packet): + """ + 7.1.22 Read Remote Extended Features command + """ + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEShortField("connection_handle", None), + ByteField("page_number", None), ] -class HCI_Cmd_LE_Set_Advertise_Enable(Packet): - name = "LE Set Advertise Enable" - fields_desc = [ByteField("enable", 0)] +class HCI_Cmd_IO_Capability_Request_Reply(Packet): + """ + 7.1.29 IO Capability Request Reply command + """ + name = "HCI_Read_Remote_Supported_Features" + fields_desc = [LEMACField("bd_addr", None), + ByteEnumField("io_capability", None, {0x00: "DisplayOnly", + 0x01: "DisplayYesNo", + 0x02: "KeyboardOnly", + 0x03: "NoInputNoOutput", }), + ByteEnumField("oob_data_present", None, {0x00: "Not Present", + 0x01: "P-192", + 0x02: "P-256", + 0x03: "P-192 + P-256", }), + ByteEnumField("authentication_requirement", None, + {0x00: "MITM Not Required", + 0x01: "MITM Required, No Bonding", + 0x02: "MITM Not Required + Dedicated Pairing", + 0x03: "MITM Required + Dedicated Pairing", + 0x04: "MITM Not Required, General Bonding", + 0x05: "MITM Required + General Bonding"}), ] + + +class HCI_Cmd_User_Confirmation_Request_Reply(Packet): + """ + 7.1.30 User Confirmation Request Reply command + """ + name = "HCI_User_Confirmation_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Start_Encryption_Request(Packet): - name = "LE Start Encryption" - fields_desc = [LEShortField("handle", 0), - StrFixedLenField("rand", None, 8), - XLEShortField("ediv", 0), - StrFixedLenField("ltk", b'\x00' * 16, 16), ] +class HCI_Cmd_User_Confirmation_Request_Negative_Reply(Packet): + """ + 7.1.31 User Confirmation Request Negative Reply command + """ + name = "HCI_User_Confirmation_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] -class HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply(Packet): - name = "LE Long Term Key Request Negative Reply" - fields_desc = [LEShortField("handle", 0), ] +class HCI_Cmd_User_Passkey_Request_Reply(Packet): + """ + 7.1.32 User Passkey Request Reply command + """ + name = "HCI_User_Passkey_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + LEIntField("numeric_value", None), ] -class HCI_Cmd_LE_Long_Term_Key_Request_Reply(Packet): - name = "LE Long Term Key Request Reply" - fields_desc = [LEShortField("handle", 0), - StrFixedLenField("ltk", b'\x00' * 16, 16), ] +class HCI_Cmd_User_Passkey_Request_Negative_Reply(Packet): + """ + 7.1.33 User Passkey Request Negative Reply command + """ + name = "HCI_User_Passkey_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +class HCI_Cmd_Remote_OOB_Data_Request_Reply(Packet): + """ + 7.1.34 Remote OOB Data Request Reply command + """ + name = "HCI_Remote_OOB_Data_Request_Reply" + fields_desc = [LEMACField("bd_addr", None), + NBytesField("C", b"\x00" * 16, sz=16), + NBytesField("R", b"\x00" * 16, sz=16), ] + + +class HCI_Cmd_Remote_OOB_Data_Request_Negative_Reply(Packet): + """ + 7.1.35 Remote OOB Data Request Negative Reply command + """ + name = "HCI_Remote_OOB_Data_Request_Negative_Reply" + fields_desc = [LEMACField("bd_addr", None), ] + + +# 7.2 Link Policy commands, the OGF is defined as 0x02 + +class HCI_Cmd_Hold_Mode(Packet): + name = "HCI_Hold_Mode" + fields_desc = [LEShortField("connection_handle", 0), + LEShortField("hold_mode_max_interval", 0x0002), + LEShortField("hold_mode_min_interval", 0x0002), ] + + +# 7.3 CONTROLLER & BASEBAND COMMANDS, the OGF is defined as 0x03 + +class HCI_Cmd_Set_Event_Mask(Packet): + """ + 7.3.1 Set Event Mask command + """ + name = "HCI_Set_Event_Mask" + fields_desc = [StrFixedLenField("mask", b"\xff\xff\xfb\xff\x07\xf8\xbf\x3d", 8)] # noqa: E501 + + +class HCI_Cmd_Reset(Packet): + """ + 7.3.2 Reset command + """ + name = "HCI_Reset" + + +class HCI_Cmd_Set_Event_Filter(Packet): + """ + 7.3.3 Set Event Filter command + """ + name = "HCI_Set_Event_Filter" + fields_desc = [ByteEnumField("type", 0, {0: "clear"}), ] + + +class HCI_Cmd_Write_Local_Name(Packet): + """ + 7.3.11 Write Local Name command + """ + name = "HCI_Write_Local_Name" + fields_desc = [StrFixedLenField('name', '', length=248)] + + +class HCI_Cmd_Read_Local_Name(Packet): + """ + 7.3.12 Read Local Name command + """ + name = "HCI_Read_Local_Name" + + +class HCI_Cmd_Write_Connect_Accept_Timeout(Packet): + name = "HCI_Write_Connection_Accept_Timeout" + fields_desc = [LEShortField("timeout", 32000)] # 32000 slots is 20000 msec + + +class HCI_Cmd_Write_Extended_Inquiry_Response(Packet): + name = "HCI_Write_Extended_Inquiry_Response" + fields_desc = [ByteField("fec_required", 0), + HCI_Extended_Inquiry_Response] + + +class HCI_Cmd_Read_LE_Host_Support(Packet): + name = "HCI_Read_LE_Host_Support" + + +class HCI_Cmd_Write_LE_Host_Support(Packet): + name = "HCI_Write_LE_Host_Support" + fields_desc = [ByteField("supported", 1), + ByteField("unused", 1), ] + + +# 7.4 INFORMATIONAL PARAMETERS, the OGF is defined as 0x04 + +class HCI_Cmd_Read_Local_Version_Information(Packet): + """ + 7.4.1 Read Local Version Information command + """ + name = "HCI_Read_Local_Version_Information" + + +class HCI_Cmd_Read_Local_Extended_Features(Packet): + """ + 7.4.4 Read Local Extended Features command + """ + name = "HCI_Read_Local_Extended_Features" + fields_desc = [ByteField("page_number", 0)] + + +class HCI_Cmd_Read_BD_Addr(Packet): + """ + 7.4.6 Read BD_ADDR command + """ + name = "HCI_Read_BD_ADDR" + + +# 7.5 STATUS PARAMETERS, the OGF is defined as 0x05 + +class HCI_Cmd_Read_Link_Quality(Packet): + name = "HCI_Read_Link_Quality" + fields_desc = [LEShortField("handle", 0)] + + +class HCI_Cmd_Read_RSSI(Packet): + name = "HCI_Read_RSSI" + fields_desc = [LEShortField("handle", 0)] + + +# 7.6 TESTING COMMANDS, the OGF is defined as 0x06 + +class HCI_Cmd_Read_Loopback_Mode(Packet): + name = "HCI_Read_Loopback_Mode" + + +class HCI_Cmd_Write_Loopback_Mode(Packet): + name = "HCI_Write_Loopback_Mode" + fields_desc = [ByteEnumField("loopback_mode", 0, + {0: "no loopback", + 1: "enable local loopback", + 2: "enable remote loopback"})] + + +# 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 + +class HCI_Cmd_LE_Read_Buffer_Size_V1(Packet): + name = "HCI_LE_Read_Buffer_Size [v1]" + + +class HCI_Cmd_LE_Read_Buffer_Size_V2(Packet): + name = "HCI_LE_Read_Buffer_Size [v2]" + + +class HCI_Cmd_LE_Read_Local_Supported_Features(Packet): + name = "HCI_LE_Read_Local_Supported_Features" + + +class HCI_Cmd_LE_Set_Random_Address(Packet): + name = "HCI_LE_Set_Random_Address" + fields_desc = [LEMACField("address", None)] + + +class HCI_Cmd_LE_Set_Advertising_Parameters(Packet): + name = "HCI_LE_Set_Advertising_Parameters" + fields_desc = [LEShortField("interval_min", 0x0800), + LEShortField("interval_max", 0x0800), + ByteEnumField("adv_type", 0, {0: "ADV_IND", 1: "ADV_DIRECT_IND", 2: "ADV_SCAN_IND", 3: "ADV_NONCONN_IND", 4: "ADV_DIRECT_IND_LOW"}), # noqa: E501 + ByteEnumField("oatype", 0, {0: "public", 1: "random"}), + ByteEnumField("datype", 0, {0: "public", 1: "random"}), + LEMACField("daddr", None), + ByteField("channel_map", 7), + ByteEnumField("filter_policy", 0, {0: "all:all", 1: "connect:all scan:whitelist", 2: "connect:whitelist scan:all", 3: "all:whitelist"}), ] # noqa: E501 + + +class HCI_Cmd_LE_Set_Advertising_Data(Packet): + name = "HCI_LE_Set_Advertising_Data" + fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), + PadField( + PacketListField("data", [], EIR_Hdr, + length_from=lambda pkt: pkt.len), + align=31, padwith=b"\0"), ] + + +class HCI_Cmd_LE_Set_Scan_Response_Data(Packet): + name = "HCI_LE_Set_Scan_Response_Data" + fields_desc = [FieldLenField("len", None, length_of="data", fmt="B"), + StrLenField("data", "", length_from=lambda pkt: pkt.len), ] + + +class HCI_Cmd_LE_Set_Advertise_Enable(Packet): + name = "HCI_LE_Set_Advertising_Enable" + fields_desc = [ByteField("enable", 0)] + + +class HCI_Cmd_LE_Set_Scan_Parameters(Packet): + name = "HCI_LE_Set_Scan_Parameters" + fields_desc = [ByteEnumField("type", 0, {0: "passive", 1: "active"}), + XLEShortField("interval", 16), + XLEShortField("window", 16), + ByteEnumField("atype", 0, {0: "public", + 1: "random", + 2: "rpa (pub)", + 3: "rpa (random)"}), + ByteEnumField("policy", 0, {0: "all", 1: "whitelist"})] + + +class HCI_Cmd_LE_Set_Scan_Enable(Packet): + name = "HCI_LE_Set_Scan_Enable" + fields_desc = [ByteField("enable", 1), + ByteField("filter_dups", 1), ] + + +class HCI_Cmd_LE_Create_Connection(Packet): + name = "HCI_LE_Create_Connection" + fields_desc = [LEShortField("interval", 96), + LEShortField("window", 48), + ByteEnumField("filter", 0, {0: "address"}), + ByteEnumField("patype", 0, {0: "public", 1: "random"}), + LEMACField("paddr", None), + ByteEnumField("atype", 0, {0: "public", 1: "random"}), + LEShortField("min_interval", 40), + LEShortField("max_interval", 56), + LEShortField("latency", 0), + LEShortField("timeout", 42), + LEShortField("min_ce", 0), + LEShortField("max_ce", 0), ] + + +class HCI_Cmd_LE_Create_Connection_Cancel(Packet): + name = "HCI_LE_Create_Connection_Cancel" + + +class HCI_Cmd_LE_Read_Filter_Accept_List_Size(Packet): + name = "HCI_LE_Read_Filter_Accept_List_Size" + + +class HCI_Cmd_LE_Clear_Filter_Accept_List(Packet): + name = "HCI_LE_Clear_Filter_Accept_List" + + +class HCI_Cmd_LE_Add_Device_To_Filter_Accept_List(Packet): + name = "HCI_LE_Add_Device_To_Filter_Accept_List" + fields_desc = [ByteEnumField("address_type", 0, {0: "public", + 1: "random", + 0xff: "anonymous"}), + LEMACField("address", None)] + + +class HCI_Cmd_LE_Remove_Device_From_Filter_Accept_List(HCI_Cmd_LE_Add_Device_To_Filter_Accept_List): # noqa: E501 + name = "HCI_LE_Remove_Device_From_Filter_Accept_List" + + +class HCI_Cmd_LE_Connection_Update(Packet): + name = "HCI_LE_Connection_Update" + fields_desc = [XLEShortField("handle", 0), + XLEShortField("min_interval", 0), + XLEShortField("max_interval", 0), + XLEShortField("latency", 0), + XLEShortField("timeout", 0), + LEShortField("min_ce", 0), + LEShortField("max_ce", 0xffff), ] + + +class HCI_Cmd_LE_Read_Remote_Features(Packet): + name = "HCI_LE_Read_Remote_Features" + fields_desc = [LEShortField("handle", 64)] + + +class HCI_Cmd_LE_Enable_Encryption(Packet): + name = "HCI_LE_Enable_Encryption" + fields_desc = [LEShortField("handle", 0), + StrFixedLenField("rand", None, 8), + XLEShortField("ediv", 0), + StrFixedLenField("ltk", b'\x00' * 16, 16), ] + + +class HCI_Cmd_LE_Long_Term_Key_Request_Reply(Packet): + name = "HCI_LE_Long_Term_Key_Request_Reply" + fields_desc = [LEShortField("handle", 0), + StrFixedLenField("ltk", b'\x00' * 16, 16), ] + + +class HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply(Packet): + name = "HCI_LE_Long_Term_Key_Request _Negative_Reply" + fields_desc = [LEShortField("handle", 0), ] class HCI_Event_Hdr(Packet): @@ -1099,22 +2200,113 @@ def answers(self, other): return self.payload.answers(other) +class HCI_Event_Inquiry_Complete(Packet): + """ + 7.7.1 Inquiry Complete event + """ + name = "HCI_Inquiry_Complete" + fields_desc = [ + ByteEnumField('status', 0, _bluetooth_error_codes) + ] + + +class HCI_Event_Inquiry_Result(Packet): + """ + 7.7.2 Inquiry Result event + """ + name = "HCI_Inquiry_Result" + fields_desc = [ + ByteField("num_response", 0x00), + FieldListField("addr", None, LEMACField("addr", None), + count_from=lambda p: p.num_response), + FieldListField("page_scan_repetition_mode", None, + ByteField("page_scan_repetition_mode", 0), + count_from=lambda p: p.num_response), + FieldListField("reserved", None, LEShortField("reserved", 0), + count_from=lambda p: p.num_response), + FieldListField("device_class", None, XLE3BytesField("device_class", 0), + count_from=lambda p: p.num_response), + FieldListField("clock_offset", None, LEShortField("clock_offset", 0), + count_from=lambda p: p.num_response) + ] + + +class HCI_Event_Connection_Complete(Packet): + """ + 7.7.3 Connection Complete event + """ + name = "HCI_Connection_Complete" + fields_desc = [ByteEnumField('status', 0, _bluetooth_error_codes), + LEShortField("handle", 0x0100), + LEMACField("bd_addr", None), + ByteEnumField("link_type", 0, {0: "SCO connection", + 1: "ACL connection", }), + ByteEnumField("encryption_enabled", 0, + {0: "link level encryption disabled", + 1: "link level encryption enabled", }), ] + + class HCI_Event_Disconnection_Complete(Packet): - name = "Disconnection Complete" - fields_desc = [ByteEnumField("status", 0, {0: "success"}), + """ + 7.7.5 Disconnection Complete event + """ + name = "HCI_Disconnection_Complete" + fields_desc = [ByteEnumField("status", 0, _bluetooth_error_codes), LEShortField("handle", 0), XByteField("reason", 0), ] +class HCI_Event_Remote_Name_Request_Complete(Packet): + """ + 7.7.7 Remote Name Request Complete event + """ + name = "HCI_Remote_Name_Request_Complete" + fields_desc = [ByteEnumField("status", 0, _bluetooth_error_codes), + LEMACField("bd_addr", None), + StrFixedLenField("remote_name", b"\x00", 248), ] + + class HCI_Event_Encryption_Change(Packet): - name = "Encryption Change" + """ + 7.7.8 Encryption Change event + """ + name = "HCI_Encryption_Change" fields_desc = [ByteEnumField("status", 0, {0: "change has occurred"}), LEShortField("handle", 0), ByteEnumField("enabled", 0, {0: "OFF", 1: "ON (LE)", 2: "ON (BR/EDR)"}), ] # noqa: E501 +class HCI_Event_Read_Remote_Supported_Features_Complete(Packet): + """ + 7.7.11 Read Remote Supported Features Complete event + """ + name = "HCI_Read_Remote_Supported_Features_Complete" + fields_desc = [ + ByteEnumField('status', 0, _bluetooth_error_codes), + LEShortField('handle', 0), + FlagsField('lmp_features', 0, -64, _bluetooth_features) + ] + + +class HCI_Event_Read_Remote_Version_Information_Complete(Packet): + """ + 7.7.12 Read Remote Version Information Complete event + """ + name = "HCI_Read_Remote_Version_Information" + fields_desc = [ + ByteEnumField('status', 0, _bluetooth_error_codes), + LEShortField('handle', 0), + ByteField('version', 0x00), + LEShortField('manufacturer_name', 0x0000), + LEShortField('subversion', 0x0000) + ] + + class HCI_Event_Command_Complete(Packet): - name = "Command Complete" + """ + 7.7.14 Command Complete event + """ + name = "HCI_Command_Complete" fields_desc = [ByteField("number", 0), XLEShortField("opcode", 0), ByteEnumField("status", 0, _bluetooth_error_codes)] @@ -1126,19 +2318,11 @@ def answers(self, other): return other[HCI_Command_Hdr].opcode == self.opcode -class HCI_Cmd_Complete_Read_BD_Addr(Packet): - name = "Read BD Addr" - fields_desc = [LEMACField("addr", None), ] - - -class HCI_Cmd_Complete_LE_Read_White_List_Size(Packet): - name = "LE Read White List Size" - fields_desc = [ByteField("status", 0), - ByteField("size", 0), ] - - class HCI_Event_Command_Status(Packet): - name = "Command Status" + """ + 7.7.15 Command Status event + """ + name = "HCI_Command_Status" fields_desc = [ByteEnumField("status", 0, {0: "pending"}), ByteField("number", 0), XLEShortField("opcode", None), ] @@ -1151,17 +2335,115 @@ def answers(self, other): class HCI_Event_Number_Of_Completed_Packets(Packet): - name = "Number Of Completed Packets" - fields_desc = [ByteField("number", 0)] + """ + 7.7.19 Number Of Completed Packets event + """ + name = "HCI_Number_Of_Completed_Packets" + fields_desc = [ByteField("num_handles", 0), + FieldListField("connection_handle_list", None, + LEShortField("connection_handle", 0), + count_from=lambda p: p.num_handles), + FieldListField("num_completed_packets_list", None, + LEShortField("num_completed_packets", 0), + count_from=lambda p: p.num_handles)] + + +class HCI_Event_Link_Key_Request(Packet): + """ + 7.7.23 Link Key Request event + """ + name = 'HCI_Link_Key_Request' + fields_desc = [ + LEMACField('bd_addr', None) + ] + + +class HCI_Event_Inquiry_Result_With_Rssi(Packet): + """ + 7.7.33 Inquiry Result with RSSI event + """ + name = "HCI_Inquiry_Result_with_RSSI" + fields_desc = [ + ByteField("num_response", 0x00), + FieldListField("bd_addr", None, LEMACField, + count_from=lambda p: p.num_response), + FieldListField("page_scan_repetition_mode", None, ByteField, + count_from=lambda p: p.num_response), + FieldListField("reserved", None, LEShortField, + count_from=lambda p: p.num_response), + FieldListField("device_class", None, XLE3BytesField, + count_from=lambda p: p.num_response), + FieldListField("clock_offset", None, LEShortField, + count_from=lambda p: p.num_response), + FieldListField("rssi", None, SignedByteField, + count_from=lambda p: p.num_response) + ] + + +class HCI_Event_Read_Remote_Extended_Features_Complete(Packet): + """ + 7.7.34 Read Remote Extended Features Complete event + """ + name = "HCI_Read_Remote_Extended_Features_Complete" + fields_desc = [ + ByteEnumField('status', 0, _bluetooth_error_codes), + LEShortField('handle', 0), + ByteField('page', 0x00), + ByteField('max_page', 0x00), + XLELongField('extended_features', 0) + ] + + +class HCI_Event_Extended_Inquiry_Result(Packet): + """ + 7.7.38 Extended Inquiry Result event + """ + name = "HCI_Extended_Inquiry_Result" + fields_desc = [ + ByteField('num_response', 0x01), + LEMACField('bd_addr', None), + ByteField('page_scan_repetition_mode', 0x00), + ByteField('reserved', 0x00), + XLE3BytesField('device_class', 0x000000), + LEShortField('clock_offset', 0x0000), + SignedByteField('rssi', 0x00), + HCI_Extended_Inquiry_Response, + ] + + +class HCI_Event_IO_Capability_Response(Packet): + """ + 7.7.41 IO Capability Response event + """ + name = "HCI_IO_Capability_Response" + fields_desc = [ + LEMACField('bd_addr', None), + ByteField('io_capability', 0x00), + ByteField('oob_data_present', 0x00), + ByteField('authentication_requirements', 0x00) + ] class HCI_Event_LE_Meta(Packet): - name = "LE Meta" + """ + 7.7.65 LE Meta event + """ + name = "HCI_LE_Meta" fields_desc = [ByteEnumField("event", 0, { - 1: "connection_complete", - 2: "advertising_report", - 3: "connection_update_complete", - 5: "long_term_key_request", + 0x01: "connection_complete", + 0x02: "advertising_report", + 0x03: "connection_update_complete", + 0x04: "read_remote_features_page_0_complete", + 0x05: "long_term_key_request", + 0x06: "remote_connection_parameter_request", + 0x07: "data_length_change", + 0x08: "read_local_p256_public_key_complete", + 0x09: "generate_dhkey_complete", + 0x0a: "enhanced_connection_complete_v1", + 0x0b: "directed_advertising_report", + 0x0c: "phy_update_complete", + 0x0d: "extended_advertising_report", + 0x29: "enhanced_connection_complete_v2" }), ] def answers(self, other): @@ -1172,6 +2454,53 @@ def answers(self, other): return self.payload.answers(other) +class HCI_Cmd_Complete_Read_Local_Name(Packet): + """ + 7.3.12 Read Local Name command complete + """ + name = 'Read Local Name command complete' + fields_desc = [StrFixedLenField('local_name', '', length=248)] + + +class HCI_Cmd_Complete_Read_Local_Version_Information(Packet): + """ + 7.4.1 Read Local Version Information command complete + """ + name = 'Read Local Version Information' + fields_desc = [ + ByteEnumField('hci_version', 0, _bluetooth_core_specification_versions), + LEShortField('hci_subversion', 0), + ByteEnumField('lmp_version', 0, _bluetooth_core_specification_versions), + LEShortEnumField('company_identifier', 0, BLUETOOTH_CORE_COMPANY_IDENTIFIERS), + LEShortField('lmp_subversion', 0)] + + +class HCI_Cmd_Complete_Read_Local_Extended_Features(Packet): + """ + 7.4.4 Read Local Extended Features command complete + """ + name = 'Read Local Extended Features command complete' + fields_desc = [ + ByteField('page', 0x00), + ByteField('max_page', 0x00), + XLELongField('extended_features', 0) + ] + + +class HCI_Cmd_Complete_Read_BD_Addr(Packet): + """ + 7.4.6 Read BD_ADDR command complete + """ + name = "Read BD Addr" + fields_desc = [LEMACField("addr", None), ] + + +class HCI_Cmd_Complete_LE_Read_White_List_Size(Packet): + name = "LE Read White List Size" + fields_desc = [ByteField("status", 0), + ByteField("size", 0), ] + + class HCI_LE_Meta_Connection_Complete(Packet): name = "Connection Complete" fields_desc = [ByteEnumField("status", 0, {0: "success"}), @@ -1208,7 +2537,7 @@ class HCI_LE_Meta_Advertising_Report(Packet): LEMACField("addr", None), FieldLenField("len", None, length_of="data", fmt="B"), PacketListField("data", [], EIR_Hdr, - length_from=lambda pkt:pkt.len), + length_from=lambda pkt: pkt.len), SignedByteField("rssi", 0)] def extract_padding(self, s): @@ -1220,7 +2549,7 @@ class HCI_LE_Meta_Advertising_Reports(Packet): fields_desc = [FieldLenField("len", None, count_of="reports", fmt="B"), PacketListField("reports", None, HCI_LE_Meta_Advertising_Report, - count_from=lambda pkt:pkt.len)] + count_from=lambda pkt: pkt.len)] class HCI_LE_Meta_Long_Term_Key_Request(Packet): @@ -1230,6 +2559,69 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): XLEShortField("ediv", 0), ] +class HCI_LE_Meta_Extended_Advertising_Report(Packet): + name = "Extended Advertising Report" + fields_desc = [ + BitField("reserved0", 0, 1), + BitEnumField("data_status", 0, 2, { + 0b00: "complete", + 0b01: "incomplete", + 0b10: "incomplete_truncated", + 0b11: "reserved" + }), + BitField("legacy", 0, 1), + BitField("scan_response", 0, 1), + BitField("directed", 0, 1), + BitField("scannable", 0, 1), + BitField("connectable", 0, 1), + ByteField("reserved", 0), + ByteEnumField("address_type", 0, { + 0x00: "public_device_address", + 0x01: "random_device_address", + 0x02: "public_identity_address", + 0x03: "random_identity_address", + 0xff: "anonymous" + }), + LEMACField('address', None), + ByteEnumField("primary_phy", 0, { + 0x01: "le_1m", + 0x03: "le_coded_s8", + 0x04: "le_coded_s2" + }), + ByteEnumField("secondary_phy", 0, { + 0x01: "le_1m", + 0x02: "le_2m", + 0x03: "le_coded_s8", + 0x04: "le_coded_s2" + }), + ByteField("advertising_sid", 0xff), + ByteField("tx_power", 0x7f), + SignedByteField("rssi", 0x00), + LEShortField("periodic_advertising_interval", 0x0000), + ByteEnumField("direct_address_type", 0, { + 0x00: "public_device_address", + 0x01: "non_resolvable_private_address", + 0x02: "resolvable_private_address_resolved_0", + 0x03: "resolvable_private_address_resolved_1", + 0xfe: "resolvable_private_address_unable_resolve"}), + LEMACField("direct_address", None), + FieldLenField("data_length", None, length_of="data", fmt="B"), + PacketListField("data", [], EIR_Hdr, + length_from=lambda pkt: pkt.data_length), + ] + + def extract_padding(self, s): + return '', s + + +class HCI_LE_Meta_Extended_Advertising_Reports(Packet): + name = "Extended Advertising Reports" + fields_desc = [FieldLenField("num_reports", None, count_of="reports", fmt="B"), + PacketListField("reports", None, + HCI_LE_Meta_Extended_Advertising_Report, + count_from=lambda pkt: pkt.num_reports)] + + bind_layers(HCI_PHDR_Hdr, HCI_Hdr) bind_layers(HCI_Hdr, HCI_Command_Hdr, type=1) @@ -1240,65 +2632,154 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): conf.l2types.register(DLT_BLUETOOTH_HCI_H4, HCI_Hdr) conf.l2types.register(DLT_BLUETOOTH_HCI_H4_WITH_PHDR, HCI_PHDR_Hdr) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, opcode=0x0c03) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Mask, opcode=0x0c01) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Filter, opcode=0x0c05) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Connect_Accept_Timeout, opcode=0x0c16) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Host_Supported, opcode=0x0c6d) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Extended_Inquiry_Response, opcode=0x0c52) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_BD_Addr, opcode=0x1009) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, opcode=0x0c13) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size, opcode=0x2002) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, opcode=0x2005) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, opcode=0x2006) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Data, opcode=0x2008) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Response_Data, opcode=0x2009) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, opcode=0x200a) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, opcode=0x200b) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, opcode=0x200c) -bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, opcode=0x406) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, opcode=0x200d) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, opcode=0x200e) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_White_List_Size, opcode=0x200f) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Clear_White_List, opcode=0x2010) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Add_Device_To_White_List, opcode=0x2011) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Remove_Device_From_White_List, opcode=0x2012) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Connection_Update, opcode=0x2013) -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Remote_Used_Features, opcode=0x2016) # noqa: E501 - - -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Start_Encryption_Request, opcode=0x2019) # noqa: E501 - -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Start_Encryption_Request, opcode=0x2019) # noqa: E501 - -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, opcode=0x201a) # noqa: E501 -bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, opcode=0x201b) # noqa: E501 - -bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x5) -bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x8) -bind_layers(HCI_Event_Hdr, HCI_Event_Command_Complete, code=0xe) -bind_layers(HCI_Event_Hdr, HCI_Event_Command_Status, code=0xf) + +# 7.1 LINK CONTROL COMMANDS, the OGF is defined as 0x01 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Inquiry, ogf=0x01, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Inquiry_Cancel, ogf=0x01, ocf=0x0002) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Periodic_Inquiry_Mode, ogf=0x01, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Exit_Peiodic_Inquiry_Mode, ogf=0x01, ocf=0x0004) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection, ogf=0x01, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Disconnect, ogf=0x01, ocf=0x0006) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Create_Connection_Cancel, ogf=0x01, ocf=0x0008) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Accept_Connection_Request, ogf=0x01, ocf=0x0009) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Reject_Connection_Response, ogf=0x01, ocf=0x000a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Reply, ogf=0x01, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Link_Key_Request_Negative_Reply, + ogf=0x01, ocf=0x000c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_PIN_Code_Request_Reply, ogf=0x01, ocf=0x000d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Change_Connection_Packet_Type, + ogf=0x01, ocf=0x000f) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Authentication_Requested, ogf=0x01, ocf=0x0011) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Connection_Encryption, ogf=0x01, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Change_Connection_Link_Key, ogf=0x01, ocf=0x0017) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request, ogf=0x01, ocf=0x0019) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_Name_Request_Cancel, ogf=0x01, ocf=0x001a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Remote_Supported_Features, + ogf=0x01, ocf=0x001b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Remote_Extended_Features, + ogf=0x01, ocf=0x001c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_IO_Capability_Request_Reply, ogf=0x01, ocf=0x002b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Confirmation_Request_Reply, + ogf=0x01, ocf=0x002c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Confirmation_Request_Negative_Reply, + ogf=0x01, ocf=0x002d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Passkey_Request_Reply, ogf=0x01, ocf=0x002e) +bind_layers(HCI_Command_Hdr, HCI_Cmd_User_Passkey_Request_Negative_Reply, + ogf=0x01, ocf=0x002f) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_OOB_Data_Request_Reply, + ogf=0x01, ocf=0x0030) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Remote_OOB_Data_Request_Negative_Reply, + ogf=0x01, ocf=0x0033) + +# 7.2 Link Policy commands, the OGF is defined as 0x02 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Hold_Mode, ogf=0x02, ocf=0x0001) + +# 7.3 CONTROLLER & BASEBAND COMMANDS, the OGF is defined as 0x03 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Mask, ogf=0x03, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Reset, ogf=0x03, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Set_Event_Filter, ogf=0x03, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Local_Name, ogf=0x03, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Local_Name, ogf=0x03, ocf=0x0014) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Connect_Accept_Timeout, ogf=0x03, ocf=0x0016) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Extended_Inquiry_Response, ogf=0x03, ocf=0x0052) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_LE_Host_Support, ogf=0x03, ocf=0x006c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_LE_Host_Support, ogf=0x03, ocf=0x006d) + +# 7.4 INFORMATIONAL PARAMETERS, the OGF is defined as 0x04 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Local_Version_Information, ogf=0x04, ocf=0x0001) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Local_Extended_Features, ogf=0x04, ocf=0x0004) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_BD_Addr, ogf=0x04, ocf=0x0009) + +# 7.5 STATUS PARAMETERS, the OGF is defined as 0x05 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Link_Quality, ogf=0x05, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_RSSI, ogf=0x05, ocf=0x0005) + +# 7.6 TESTING COMMANDS, the OGF is defined as 0x06 +bind_layers(HCI_Command_Hdr, HCI_Cmd_Read_Loopback_Mode, ogf=0x06, ocf=0x0001) +bind_layers(HCI_Command_Hdr, HCI_Cmd_Write_Loopback_Mode, ogf=0x06, ocf=0x0002) + +# 7.8 LE CONTROLLER COMMANDS, the OGF code is defined as 0x08 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V1, ogf=0x08, ocf=0x0002) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Buffer_Size_V2, ogf=0x08, ocf=0x0060) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Local_Supported_Features, + ogf=0x08, ocf=0x0003) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Random_Address, ogf=0x08, ocf=0x0005) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Parameters, ogf=0x08, ocf=0x0006) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertising_Data, ogf=0x08, ocf=0x0008) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Response_Data, ogf=0x08, ocf=0x0009) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Advertise_Enable, ogf=0x08, ocf=0x000a) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Parameters, ogf=0x08, ocf=0x000b) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Set_Scan_Enable, ogf=0x08, ocf=0x000c) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection, ogf=0x08, ocf=0x000d) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Create_Connection_Cancel, ogf=0x08, ocf=0x000e) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Filter_Accept_List_Size, + ogf=0x08, ocf=0x000f) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Clear_Filter_Accept_List, ogf=0x08, ocf=0x0010) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Add_Device_To_Filter_Accept_List, ogf=0x08, ocf=0x0011) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Remove_Device_From_Filter_Accept_List, ogf=0x08, ocf=0x0012) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Connection_Update, ogf=0x08, ocf=0x0013) +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Read_Remote_Features, ogf=0x08, ocf=0x0016) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Enable_Encryption, ogf=0x08, ocf=0x0019) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Reply, ogf=0x08, ocf=0x001a) # noqa: E501 +bind_layers(HCI_Command_Hdr, HCI_Cmd_LE_Long_Term_Key_Request_Negative_Reply, ogf=0x08, ocf=0x001b) # noqa: E501 + +# 7.7 EVENTS +bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Complete, code=0x01) +bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Result, code=0x02) +bind_layers(HCI_Event_Hdr, HCI_Event_Connection_Complete, code=0x03) +bind_layers(HCI_Event_Hdr, HCI_Event_Disconnection_Complete, code=0x05) +bind_layers(HCI_Event_Hdr, HCI_Event_Remote_Name_Request_Complete, code=0x07) +bind_layers(HCI_Event_Hdr, HCI_Event_Encryption_Change, code=0x08) +bind_layers(HCI_Event_Hdr, HCI_Event_Read_Remote_Supported_Features_Complete, code=0x0b) +bind_layers(HCI_Event_Hdr, HCI_Event_Read_Remote_Version_Information_Complete, code=0x0c) # noqa: E501 +bind_layers(HCI_Event_Hdr, HCI_Event_Command_Complete, code=0x0e) +bind_layers(HCI_Event_Hdr, HCI_Event_Command_Status, code=0x0f) bind_layers(HCI_Event_Hdr, HCI_Event_Number_Of_Completed_Packets, code=0x13) +bind_layers(HCI_Event_Hdr, HCI_Event_Link_Key_Request, code=0x17) +bind_layers(HCI_Event_Hdr, HCI_Event_Inquiry_Result_With_Rssi, code=0x22) +bind_layers(HCI_Event_Hdr, HCI_Event_Read_Remote_Extended_Features_Complete, code=0x23) +bind_layers(HCI_Event_Hdr, HCI_Event_Extended_Inquiry_Result, code=0x2f) +bind_layers(HCI_Event_Hdr, HCI_Event_IO_Capability_Response, code=0x32) bind_layers(HCI_Event_Hdr, HCI_Event_LE_Meta, code=0x3e) +bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Name, opcode=0x0c14) # noqa: E501 +bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Version_Information, opcode=0x1001) # noqa: E501 +bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_Local_Extended_Features, opcode=0x1004) # noqa: E501 bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_Read_BD_Addr, opcode=0x1009) # noqa: E501 bind_layers(HCI_Event_Command_Complete, HCI_Cmd_Complete_LE_Read_White_List_Size, opcode=0x200f) # noqa: E501 -bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Complete, event=1) -bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Advertising_Reports, event=2) -bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Update_Complete, event=3) -bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Long_Term_Key_Request, event=5) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Complete, event=0x01) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Advertising_Reports, event=0x02) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Connection_Update_Complete, event=0x03) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Long_Term_Key_Request, event=0x05) +bind_layers(HCI_Event_LE_Meta, HCI_LE_Meta_Extended_Advertising_Reports, event=0x0d) bind_layers(EIR_Hdr, EIR_Flags, type=0x01) bind_layers(EIR_Hdr, EIR_IncompleteList16BitServiceUUIDs, type=0x02) bind_layers(EIR_Hdr, EIR_CompleteList16BitServiceUUIDs, type=0x03) +bind_layers(EIR_Hdr, EIR_IncompleteList32BitServiceUUIDs, type=0x04) +bind_layers(EIR_Hdr, EIR_CompleteList32BitServiceUUIDs, type=0x05) bind_layers(EIR_Hdr, EIR_IncompleteList128BitServiceUUIDs, type=0x06) bind_layers(EIR_Hdr, EIR_CompleteList128BitServiceUUIDs, type=0x07) bind_layers(EIR_Hdr, EIR_ShortenedLocalName, type=0x08) bind_layers(EIR_Hdr, EIR_CompleteLocalName, type=0x09) bind_layers(EIR_Hdr, EIR_Device_ID, type=0x10) bind_layers(EIR_Hdr, EIR_TX_Power_Level, type=0x0a) +bind_layers(EIR_Hdr, EIR_ClassOfDevice, type=0x0d) +bind_layers(EIR_Hdr, EIR_SecureSimplePairingHashC192, type=0x0e) +bind_layers(EIR_Hdr, EIR_SecureSimplePairingRandomizerR192, type=0x0f) +bind_layers(EIR_Hdr, EIR_SecurityManagerOOBFlags, type=0x11) +bind_layers(EIR_Hdr, EIR_PeripheralConnectionIntervalRange, type=0x12) +bind_layers(EIR_Hdr, EIR_ServiceSolicitation16BitUUID, type=0x14) +bind_layers(EIR_Hdr, EIR_ServiceSolicitation128BitUUID, type=0x15) bind_layers(EIR_Hdr, EIR_ServiceData16BitUUID, type=0x16) +bind_layers(EIR_Hdr, EIR_PublicTargetAddress, type=0x17) +bind_layers(EIR_Hdr, EIR_Appearance, type=0x19) +bind_layers(EIR_Hdr, EIR_AdvertisingInterval, type=0x1a) +bind_layers(EIR_Hdr, EIR_LEBluetoothDeviceAddress, type=0x1b) +bind_layers(EIR_Hdr, EIR_ServiceData32BitUUID, type=0x20) +bind_layers(EIR_Hdr, EIR_ServiceData128BitUUID, type=0x21) +bind_layers(EIR_Hdr, EIR_URI, type=0x24) bind_layers(EIR_Hdr, EIR_Manufacturer_Specific_Data, type=0xff) bind_layers(EIR_Hdr, EIR_Raw) @@ -1312,10 +2793,25 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(L2CAP_CmdHdr, L2CAP_ConfResp, code=5) bind_layers(L2CAP_CmdHdr, L2CAP_DisconnReq, code=6) bind_layers(L2CAP_CmdHdr, L2CAP_DisconnResp, code=7) +bind_layers(L2CAP_CmdHdr, L2CAP_EchoReq, code=8) +bind_layers(L2CAP_CmdHdr, L2CAP_EchoResp, code=9) bind_layers(L2CAP_CmdHdr, L2CAP_InfoReq, code=10) bind_layers(L2CAP_CmdHdr, L2CAP_InfoResp, code=11) +bind_layers(L2CAP_CmdHdr, L2CAP_Create_Channel_Request, code=12) +bind_layers(L2CAP_CmdHdr, L2CAP_Create_Channel_Response, code=13) +bind_layers(L2CAP_CmdHdr, L2CAP_Move_Channel_Request, code=14) +bind_layers(L2CAP_CmdHdr, L2CAP_Move_Channel_Response, code=15) +bind_layers(L2CAP_CmdHdr, L2CAP_Move_Channel_Confirmation_Request, code=16) +bind_layers(L2CAP_CmdHdr, L2CAP_Move_Channel_Confirmation_Response, code=17) bind_layers(L2CAP_CmdHdr, L2CAP_Connection_Parameter_Update_Request, code=18) bind_layers(L2CAP_CmdHdr, L2CAP_Connection_Parameter_Update_Response, code=19) +bind_layers(L2CAP_CmdHdr, L2CAP_LE_Credit_Based_Connection_Request, code=20) +bind_layers(L2CAP_CmdHdr, L2CAP_LE_Credit_Based_Connection_Response, code=21) +bind_layers(L2CAP_CmdHdr, L2CAP_Flow_Control_Credit_Ind, code=22) +bind_layers(L2CAP_CmdHdr, L2CAP_Credit_Based_Connection_Request, code=23) +bind_layers(L2CAP_CmdHdr, L2CAP_Credit_Based_Connection_Response, code=24) +bind_layers(L2CAP_CmdHdr, L2CAP_Credit_Based_Reconfigure_Request, code=25) +bind_layers(L2CAP_CmdHdr, L2CAP_Credit_Based_Reconfigure_Response, code=26) bind_layers(L2CAP_Hdr, ATT_Hdr, cid=4) bind_layers(ATT_Hdr, ATT_Error_Response, opcode=0x1) bind_layers(ATT_Hdr, ATT_Exchange_MTU_Request, opcode=0x2) @@ -1345,20 +2841,113 @@ class HCI_LE_Meta_Long_Term_Key_Request(Packet): bind_layers(ATT_Hdr, ATT_Handle_Value_Notification, opcode=0x1b) bind_layers(ATT_Hdr, ATT_Handle_Value_Indication, opcode=0x1d) bind_layers(L2CAP_Hdr, SM_Hdr, cid=6) -bind_layers(SM_Hdr, SM_Pairing_Request, sm_command=1) -bind_layers(SM_Hdr, SM_Pairing_Response, sm_command=2) -bind_layers(SM_Hdr, SM_Confirm, sm_command=3) -bind_layers(SM_Hdr, SM_Random, sm_command=4) -bind_layers(SM_Hdr, SM_Failed, sm_command=5) -bind_layers(SM_Hdr, SM_Encryption_Information, sm_command=6) -bind_layers(SM_Hdr, SM_Master_Identification, sm_command=7) -bind_layers(SM_Hdr, SM_Identity_Information, sm_command=8) -bind_layers(SM_Hdr, SM_Identity_Address_Information, sm_command=9) +bind_layers(SM_Hdr, SM_Pairing_Request, sm_command=0x01) +bind_layers(SM_Hdr, SM_Pairing_Response, sm_command=0x02) +bind_layers(SM_Hdr, SM_Confirm, sm_command=0x03) +bind_layers(SM_Hdr, SM_Random, sm_command=0x04) +bind_layers(SM_Hdr, SM_Failed, sm_command=0x05) +bind_layers(SM_Hdr, SM_Encryption_Information, sm_command=0x06) +bind_layers(SM_Hdr, SM_Master_Identification, sm_command=0x07) +bind_layers(SM_Hdr, SM_Identity_Information, sm_command=0x08) +bind_layers(SM_Hdr, SM_Identity_Address_Information, sm_command=0x09) bind_layers(SM_Hdr, SM_Signing_Information, sm_command=0x0a) +bind_layers(SM_Hdr, SM_Security_Request, sm_command=0x0b) bind_layers(SM_Hdr, SM_Public_Key, sm_command=0x0c) bind_layers(SM_Hdr, SM_DHKey_Check, sm_command=0x0d) +############### +# HCI Monitor # +############### + + +# https://elixir.bootlin.com/linux/v6.4.2/source/include/net/bluetooth/hci_mon.h#L27 +class HCI_Mon_Hdr(Packet): + name = 'Bluetooth Linux Monitor Transport Header' + fields_desc = [ + LEShortEnumField('opcode', None, { + 0: "New index", + 1: "Delete index", + 2: "Command pkt", + 3: "Event pkt", + 4: "ACL TX pkt", + 5: "ACL RX pkt", + 6: "SCO TX pkt", + 7: "SCO RX pkt", + 8: "Open index", + 9: "Close index", + 10: "Index info", + 11: "Vendor diag", + 12: "System note", + 13: "User logging", + 14: "Ctrl open", + 15: "Ctrl close", + 16: "Ctrl command", + 17: "Ctrl event", + 18: "ISO TX pkt", + 19: "ISO RX pkt", + }), + LEShortField('adapter_id', None), + LEShortField('len', None) + ] + + +# https://www.tcpdump.org/linktypes/LINKTYPE_BLUETOOTH_LINUX_MONITOR.html +class HCI_Mon_Pcap_Hdr(HCI_Mon_Hdr): + name = 'Bluetooth Linux Monitor Transport Pcap Header' + fields_desc = [ + ShortField('adapter_id', None), + ShortField('opcode', None) + ] + + +class HCI_Mon_New_Index(Packet): + name = 'Bluetooth Linux Monitor Transport New Index Packet' + fields_desc = [ + ByteEnumField('bus', 0, { + 0x00: "BR/EDR", + 0x01: "AMP" + }), + ByteEnumField('type', 0, { + 0x00: "Virtual", + 0x01: "USB", + 0x02: "PC Card", + 0x03: "UART", + 0x04: "RS232", + 0x05: "PCI", + 0x06: "SDIO" + }), + LEMACField('addr', None), + StrFixedLenField('devname', None, 8) + ] + + +class HCI_Mon_Index_Info(Packet): + name = 'Bluetooth Linux Monitor Transport Index Info Packet' + fields_desc = [ + LEMACField('addr', None), + XLEShortField('manufacturer', None) + ] + + +class HCI_Mon_System_Note(Packet): + name = 'Bluetooth Linux Monitor Transport System Note Packet' + fields_desc = [ + StrNullField('note', None) + ] + + +# https://elixir.bootlin.com/linux/v6.4.2/source/include/net/bluetooth/hci_mon.h#L34 +bind_layers(HCI_Mon_Hdr, HCI_Mon_New_Index, opcode=0) +bind_layers(HCI_Mon_Hdr, HCI_Command_Hdr, opcode=2) +bind_layers(HCI_Mon_Hdr, HCI_Event_Hdr, opcode=3) +bind_layers(HCI_Mon_Hdr, HCI_ACL_Hdr, opcode=5) +bind_layers(HCI_Mon_Hdr, HCI_Mon_Index_Info, opcode=10) +bind_layers(HCI_Mon_Hdr, HCI_Mon_System_Note, opcode=12) + +conf.l2types.register(DLT_BLUETOOTH_LINUX_MONITOR, HCI_Mon_Pcap_Hdr) + + ########### # Helpers # ########### @@ -1367,7 +2956,7 @@ class LowEnergyBeaconHelper: """ Helpers for building packets for Bluetooth Low Energy Beacons. - Implementors provide a :meth:`build_eir` implementation. + Implementers provide a :meth:`build_eir` implementation. This is designed to be used as a mix-in -- see ``scapy.contrib.eddystone`` and ``scapy.contrib.ibeacon`` for examples. @@ -1479,23 +3068,18 @@ class sockaddr_hci(ctypes.Structure): ] -class BluetoothUserSocket(SuperSocket): - desc = "read/write H4 over a Bluetooth user channel" - - def __init__(self, adapter_index=0): +class _BluetoothLibcSocket(SuperSocket): + def __init__(self, socket_domain, socket_type, socket_protocol, sock_address): + # type: (int, int, int, sockaddr_hci) -> 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) @@ -1507,39 +3091,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") + raise BluetoothSocketError( + f"Unable to open socket({socket_domain}, {socket_type}, " + f"{socket_protocol})") - sa = sockaddr_hci() - sa.sin_family = 31 # AF_BLUETOOTH - sa.hci_dev = adapter_index # adapter index - sa.hci_channel = 1 # HCI_USER_CHANNEL - - 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): @@ -1551,8 +3120,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 @@ -1564,6 +3133,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 == 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)) + + +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 6d351c03607..49a5798a48a 100644 --- a/scapy/layers/bluetooth4LE.py +++ b/scapy/layers/bluetooth4LE.py @@ -11,8 +11,11 @@ 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, @@ -164,7 +167,31 @@ def __init__(self, name, default): 'le_ext_adv', 'le_periodic_adv', 'ch_sel_alg', - 'le_pwr_class'] + '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'] ) @@ -256,7 +283,7 @@ class BTLE_ADV(Packet): 2: "ADV_NONCONN_IND", 3: "SCAN_REQ", 4: "SCAN_RSP", - 5: "CONNECT_REQ", + 5: "CONNECT_IND", 6: "ADV_SCAN_IND"}), XByteField("Length", None), ] @@ -395,6 +422,23 @@ class BTLE_CONNECT_REQ(Packet): 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', } @@ -497,7 +541,7 @@ class LL_VERSION_IND(Packet): fields_desc = [ ByteEnumField("version", 8, BTLE_Versions), LEShortEnumField("company", 0, BTLE_Corp_IDs), - XShortField("subversion", 0) + XLEShortField("subversion", 0) ] @@ -620,6 +664,181 @@ class LL_MIN_USED_CHANNELS_IND(Packet): ] +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) @@ -662,6 +881,22 @@ class LL_MIN_USED_CHANNELS_IND(Packet): 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) diff --git a/scapy/layers/can.py b/scapy/layers/can.py index 1110826dea1..0c02c3c4496 100644 --- a/scapy/layers/can.py +++ b/scapy/layers/can.py @@ -12,17 +12,12 @@ import os import gzip import struct -import binascii -from scapy.compat import Tuple, Optional, Type, List, Union, Callable, IO, \ - Any, cast - -import scapy.libs.six as six from scapy.config import conf -from scapy.compat import orb +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 @@ -31,17 +26,33 @@ 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", "CAN_MTU", "CAN_MAX_IDENTIFIER", - "CAN_MAX_DLEN", "CAN_INV_FILTER"] + "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'. @@ -93,6 +104,21 @@ class CAN(Packet): 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): # type: (bytes) -> bytes @@ -146,6 +172,47 @@ def extract_padding(self, p): 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 @@ -427,9 +494,20 @@ class SignalHeader(CAN): '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 @@ -468,10 +546,10 @@ def __init__(self, filename, interface=None): self.filename, self.f = self.open(filename) 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 = cast(List[str], interface) + self.ifilter = interface def __iter__(self): # type: () -> CandumpReader @@ -540,11 +618,16 @@ def read_packet(self, size=CAN_MTU): 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_b, intf, f = line.split() - idn, data = f.split(b'#') + 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_b[1:-1]) # type: Optional[float] else: @@ -559,7 +642,12 @@ def read_packet(self, size=CAN_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: diff --git a/scapy/layers/dcerpc.py b/scapy/layers/dcerpc.py index be54d8f4f03..28dfa6f97a0 100644 --- a/scapy/layers/dcerpc.py +++ b/scapy/layers/dcerpc.py @@ -1,8 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy # See https://scapy.net/ for more information -# Copyright (C) 2016 Gauthier Sebaux -# 2022 Gabriel Potter +# Copyright (C) Gabriel Potter # scapy.contrib.description = DCE/RPC # scapy.contrib.status = loads @@ -16,23 +15,43 @@ And on [MS-RPCE] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/290c38b1-92fe-4229-91e6-4fc376610c15 -""" -from functools import partial -from collections import namedtuple, deque +.. 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 -from scapy.packet import Packet, Raw, bind_bottom_up, bind_layers, bind_top_down +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, @@ -41,17 +60,12 @@ FieldLenField, FieldListField, FlagsField, - IEEEDoubleField, - IEEEFloatField, IntField, LEIntEnumField, LEIntField, LELongField, LEShortEnumField, LEShortField, - LESignedIntField, - LESignedLongField, - LESignedShortField, LenField, MultipleTypeField, PacketField, @@ -79,9 +93,20 @@ XStrFixedLenField, ) from scapy.sessions import DefaultSession +from scapy.supersocket import StreamSocket -from scapy.layers.kerberos import KRB5_GSS_Wrap_RFC1964, KRB5_GSS_Wrap, Kerberos -from scapy.layers.gssapi import GSSAPI_BLOB +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 ( @@ -91,8 +116,18 @@ EPacketListField, ) -import scapy.libs.six as six +# 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 = { @@ -112,6 +147,7 @@ 13: "bind_nak", 14: "alter_context", 15: "alter_context_resp", + 16: "auth3", 17: "shutdown", 18: "co_cancel", 19: "orphaned", @@ -136,9 +172,57 @@ "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 = {} -def _dce_rpc_endianess(pkt): +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 """ @@ -152,7 +236,7 @@ def _dce_rpc_endianess(pkt): class _EField(EField): def __init__(self, fld): - super(_EField, self).__init__(fld, endianness_from=_dce_rpc_endianess) + super(_EField, self).__init__(fld, endianness_from=_dce_rpc_endianness) class DceRpc(Packet): @@ -168,6 +252,12 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): 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) @@ -178,7 +268,7 @@ class _DceRpcPayload(Packet): def endianness(self): if not self.underlayer: return "!" - return _dce_rpc_endianess(self.underlayer) + return _dce_rpc_endianness(self.underlayer) # sect 12.5 @@ -191,7 +281,7 @@ def endianness(self): ] -class DceRpc4(Packet): +class DceRpc4(DceRpc): """ DCE/RPC v4 'connection-less' packet """ @@ -205,9 +295,9 @@ class DceRpc4(Packet): ByteEnumField("ptype", 0, DCE_RPC_TYPE), FlagsField("flags1", 0, 8, _DCE_RPC_4_FLAGS1), FlagsField("flags2", 0, 8, _DCE_RPC_4_FLAGS2), - ] + - _drep + - [ + ] + + _drep + + [ XByteField("serial_hi", 0), _EField(UUIDField("object", None)), _EField(UUIDField("if_id", None)), @@ -226,10 +316,10 @@ class DceRpc4(Packet): ) -# Exceptionally, we define those 2 here. +# Exceptionally, we define those 3 here. -class NetlogonAuthMessage(Packet): +class NL_AUTH_MESSAGE(Packet): # [MS-NRPC] sect 2.2.1.3.1 name = "NL_AUTH_MESSAGE" fields_desc = [ @@ -278,7 +368,7 @@ class NetlogonAuthMessage(Packet): ] -class NetlogonAuthSignature(Packet): +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 = [ @@ -296,61 +386,89 @@ class NetlogonAuthSignature(Packet): { 0xFFFF: "Unencrypted", 0x007A: "RC4", - 0x00A1: "AES-128", + 0x001A: "AES-128", }, ), XLEShortField("Pad", 0xFFFF), - ShortField("Reserved", 0), - XLELongField("SequenceNumber", 0), + ShortField("Flags", 0), + XStrFixedLenField("SequenceNumber", b"", length=8), XStrFixedLenField("Checksum", b"", length=8), - XStrFixedLenField("Confounder", b"", length=8), ConditionalField( - StrFixedLenField("Reserved2", b"", length=24), - lambda pkt: pkt.SignatureAlgorithm == 0x0013, + XStrFixedLenField("Confounder", b"", length=8), + lambda pkt: pkt.SealAlgorithm != 0xFFFF, + ), + MultipleTypeField( + [ + ( + StrFixedLenField("Reserved2", b"", length=24), + lambda pkt: pkt.SignatureAlgorithm == 0x0013, + ), + ], + StrField("Reserved2", b""), ), ] -# sect 13.2.6.1 +# [MS-RPCE] sect 2.2.1.1.7 +# https://learn.microsoft.com/en-us/windows/win32/rpc/authentication-service-constants +# rpcdce.h -_MSRPCE_SECURITY_PROVIDERS = { - # [MS-RPCE] sect 2.2.1.1.7 - 0x00: "None", - 0x09: "SPNEGO", - 0x0A: "NTLM", - 0x0E: "TLS", - 0x10: "Kerberos", - 0x44: "Netlogon", - 0xFF: "NTLM", -} +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 -_MSRPCE_SECURITY_AUTHLEVELS = { - # [MS-RPCE] sect 2.2.1.1.7 - 0x00: "RPC_C_AUTHN_LEVEL_DEFAULT", - 0x01: "RPC_C_AUTHN_LEVEL_NONE", - 0x02: "RPC_C_AUTHN_LEVEL_CONNECT", - 0x03: "RPC_C_AUTHN_LEVEL_CALL", - 0x04: "RPC_C_AUTHN_LEVEL_PKT", - 0x05: "RPC_C_AUTHN_LEVEL_PKT_INTEGRITY", - 0x06: "RPC_C_AUTHN_LEVEL_PRIVACY", -} + +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 + + +# C706 sect 13.2.6.1 class CommonAuthVerifier(Packet): - name = "Common Authentication Verifier (sec_trailer)" + name = "Common Authentication Verifier" fields_desc = [ ByteEnumField( "auth_type", 0, - _MSRPCE_SECURITY_PROVIDERS, + RPC_C_AUTHN, ), - ByteEnumField("auth_level", 0, _MSRPCE_SECURITY_AUTHLEVELS), + 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", @@ -358,17 +476,26 @@ class CommonAuthVerifier(Packet): GSSAPI_BLOB, length_from=lambda pkt: pkt.parent.auth_len, ), - lambda pkt: pkt.auth_type == 0x09, + 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", - NTLM_Header(), - NTLM_Header, + GSSAPI_BLOB_SIGNATURE(), + GSSAPI_BLOB_SIGNATURE, length_from=lambda pkt: pkt.parent.auth_len, ), - lambda pkt: pkt.auth_type in [0x0A, 0xFF], + 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", @@ -376,87 +503,338 @@ class CommonAuthVerifier(Packet): Kerberos, length_from=lambda pkt: pkt.parent.auth_len, ), - lambda pkt: pkt.auth_type == 0x10, + 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", - NetlogonAuthMessage(), - NetlogonAuthMessage, + NL_AUTH_MESSAGE(), + NL_AUTH_MESSAGE, length_from=lambda pkt: pkt.parent.auth_len, ), - lambda pkt: pkt.auth_type == 0x44 and - pkt.parent and - pkt.parent.ptype in [11, 12, 13], + lambda pkt: pkt.auth_type == 0x44 and pkt.parent and + # Bind/Alter + pkt.parent.ptype in [11, 12, 13, 14, 15], ), ( PacketLenField( "auth_value", - NetlogonAuthSignature(), - NetlogonAuthSignature, + NL_AUTH_SIGNATURE(), + NL_AUTH_SIGNATURE, length_from=lambda pkt: pkt.parent.auth_len, ), - lambda pkt: pkt.auth_type == 0x44 and - (not pkt.parent or pkt.parent.ptype not in [11, 12, 13]), + 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.auth_len, + length_from=lambda pkt: pkt.parent and pkt.parent.auth_len or 0, ), ), ] - def is_encrypted(self): - if self.auth_type == 9 and isinstance(self.auth_value, GSSAPI_BLOB): - return isinstance( - self.auth_value.innerContextToken, - (KRB5_GSS_Wrap_RFC1964, KRB5_GSS_Wrap), - ) - return False + 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" -# sect 12.6 + +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="") + "H", data[8:10])[0] - if len(data) == length: - # Start a DCE/RPC session for this TCP stream - dcerpc_session = session.get("dcerpc_session", None) - if not dcerpc_session: - dcerpc_session = session["dcerpc_session"] = DceRpcSession() - pkt = dcerpc_session._process_dcerpc_packet(DceRpc5(data)) - return pkt + 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 -DCE_RPC_INTERFACES_NAMES = {} -DCE_RPC_INTERFACES_NAMES_rev = {} - -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", -} - - class DceRpc5AbstractSyntax(EPacket): name = "Presentation Syntax (p_syntax_id_t)" fields_desc = [ @@ -573,12 +982,11 @@ class DceRpc5AbstractSyntax(EPacket): ( # Those are dynamic DCE_RPC_INTERFACES_NAMES.get, - DCE_RPC_INTERFACES_NAMES_rev.get, + lambda x: DCE_RPC_INTERFACES_NAMES_rev.get(x.lower()), ), ) ), - _EField(ShortField("if_version", 3)), - _EField(ShortField("if_version_minor", 0)), + _EField(IntField("if_version", 3)), ] @@ -592,15 +1000,14 @@ class DceRpc5TransferSyntax(EPacket): DCE_RPC_TRANSFER_SYNTAXES, ) ), - _EField(ShortField("if_version", 3)), - _EField(ShortField("reserved", 0)), + _EField(IntField("if_version", 3)), ] class DceRpc5Context(EPacket): name = "Presentation Context (p_cont_elem_t)" fields_desc = [ - _EField(ShortField("context_id", 0)), + _EField(ShortField("cont_id", 0)), FieldLenField("n_transfer_syn", None, count_of="transfer_syntaxes", fmt="B"), ByteField("reserved", 0), EPacketField("abstract_syntax", None, DceRpc5AbstractSyntax), @@ -609,7 +1016,7 @@ class DceRpc5Context(EPacket): None, DceRpc5TransferSyntax, count_from=lambda pkt: pkt.n_transfer_syn, - endianness_from=_dce_rpc_endianess, + endianness_from=_dce_rpc_endianness, ), ] @@ -626,12 +1033,7 @@ class DceRpc5Result(EPacket): ShortEnumField( "reason", 0, - [ - "reason_not_specified", - "abstract_syntax_not_supported", - "proposed_transfer_syntaxes_not_supported", - "local_limit_exceeded", - ], + _DCE_RPC_REJECTION_REASONS, ) ), EPacketField("transfer_syntax", None, DceRpc5TransferSyntax), @@ -646,59 +1048,6 @@ class DceRpc5PortAny(EPacket): ] -# sec 12.6.4.1 - - -class DceRpc5AlterContext(_DceRpcPayload): - name = "DCE/RPC v5 - AlterContext" - fields_desc = [ - _EField(ShortField("max_xmit_frag", 5840)), - _EField(ShortField("max_recv_frag", 8192)), - _EField(IntField("assoc_group_id", 0)), - # p_result_list_t - _EField(FieldLenField("n_results", None, count_of="results", fmt="B")), - StrFixedLenField("reserved", 0, length=3), - EPacketListField( - "results", - [], - DceRpc5Result, - count_from=lambda pkt: pkt.n_results, - endianness_from=_dce_rpc_endianess, - ), - ] - - -bind_layers(DceRpc5, DceRpc5AlterContext, ptype=14) - - -# sec 12.6.4.2 - - -class DceRpc5AlterContextResp(_DceRpcPayload): - name = "DCE/RPC v5 - AlterContextResp" - 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, - count_from=lambda pkt: pkt.n_results, - endianness_from=_dce_rpc_endianess, - ), - ] - - -bind_layers(DceRpc5, DceRpc5AlterContextResp, ptype=15) - # sec 12.6.4.3 @@ -717,7 +1066,7 @@ class DceRpc5Bind(_DceRpcPayload): "context_elem", [], DceRpc5Context, - endianness_from=_dce_rpc_endianess, + endianness_from=_dce_rpc_endianness, count_from=lambda pkt: pkt.n_context_elem, ), ] @@ -745,7 +1094,7 @@ class DceRpc5BindAck(_DceRpcPayload): "results", [], DceRpc5Result, - endianness_from=_dce_rpc_endianess, + endianness_from=_dce_rpc_endianness, count_from=lambda pkt: pkt.n_results, ), ] @@ -767,21 +1116,77 @@ class DceRpc5Version(EPacket): class DceRpc5BindNak(_DceRpcPayload): name = "DCE/RPC v5 - Bind Nak" fields_desc = [ - _EField(ShortField("provider_reject_reason", 0)), + _EField( + ShortEnumField("provider_reject_reason", 0, _DCE_RPC_REJECTION_REASONS) + ), # p_rt_versions_supported_t - _EField(FieldLenField("n_protocols", None, length_of="protocols", fmt="B")), + _EField(FieldLenField("n_protocols", None, count_of="protocols", fmt="B")), EPacketListField( "protocols", [], DceRpc5Version, count_from=lambda pkt: pkt.n_protocols, - endianness_from=_dce_rpc_endianess, + 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 @@ -791,7 +1196,7 @@ class DceRpc5Fault(_DceRpcPayload): _EField(IntField("alloc_hint", 0)), _EField(ShortField("cont_id", 0)), ByteField("cancel_count", 0), - ByteField("reserved", 0), + FlagsField("reserved", 0, -8, {0x1: "RPC extended error"}), _EField(LEIntEnumField("status", 0, _DCE_RPC_ERROR_CODES)), IntField("reserved2", 0), ] @@ -814,7 +1219,7 @@ class DceRpc5Request(_DceRpcPayload): _EField(UUIDField("object", None)), align=8, ), - lambda pkt: pkt.underlayer.pfc_flags.OBJECT_UUID, + lambda pkt: pkt.underlayer and pkt.underlayer.pfc_flags.PFC_OBJECT_UUID, ), ] @@ -838,33 +1243,97 @@ class DceRpc5Response(_DceRpcPayload): # --- API -DceRpcOp = namedtuple("DceRpcOp", ["request", "response"]) +DceRpcOp = collections.namedtuple("DceRpcOp", ["request", "response"]) DCE_RPC_INTERFACES = {} class DceRpcInterface: - def __init__(self, name, uuid, version, opnums): + def __init__(self, name, uuid, version_tuple, if_version, opnums): self.name = name self.uuid = uuid - self.version, self.minor_version = map(int, version.split(".")) + self.major_version, self.minor_version = version_tuple + self.if_version = if_version self.opnums = opnums def __repr__(self): - return "" % (self.name, self.version) + return "" % ( + self.name, + self.major_version, + self.minor_version, + ) def register_dcerpc_interface(name, uuid, version, opnums): """ Register a DCE/RPC interface """ - if uuid in DCE_RPC_INTERFACES: - raise ValueError("Interface is already registered !") - DCE_RPC_INTERFACES_NAMES[uuid] = "%s (v%s)" % (name.upper(), version) - DCE_RPC_INTERFACES_NAMES_rev[name.upper()] = uuid - DCE_RPC_INTERFACES[uuid] = DceRpcInterface( + 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) + + +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, - version, opnums, ) # bind for build @@ -872,12 +1341,12 @@ def register_dcerpc_interface(name, uuid, version, opnums): bind_top_down(DceRpc5Request, operations.request, opnum=opnum) -def find_dcerpc_interface(name): +def find_com_interface(name) -> ComInterface: """ 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) + return next(x for x in COM_INTERFACES.values() if x.name == name) except StopIteration: raise AttributeError("Unknown interface !") @@ -885,35 +1354,60 @@ def find_dcerpc_interface(name): # --- NDR fields - [C706] chap 14 -def _set_ndr_on(f, ndr64): +def _set_ctx_on(f, obj): if isinstance(f, _NDRPacket): - f.ndr64 = ndr64 + f.ndr64 = obj.ndr64 + f.ndrendian = obj.ndrendian if isinstance(f, list): for x in f: if isinstance(x, _NDRPacket): - x.ndr64 = ndr64 + x.ndr64 = obj.ndr64 + x.ndrendian = obj.ndrendian + + +def _e(ndrendian): + return {"big": ">", "little": "<"}[ndrendian] class _NDRPacket(Packet): - __slots__ = ["ndr64", "defered_pointers", "request_packet"] + __slots__ = ["ndr64", "ndrendian", "deferred_pointers", "request_packet"] def __init__(self, *args, **kwargs): - self.ndr64 = kwargs.pop("ndr64", True) + 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.defered_pointers = [] + self.deferred_pointers = [] super(_NDRPacket, self).__init__(*args, **kwargs) - def dissect(self, s): + def do_dissect(self, s): _up = self.parent or self.underlayer if _up and isinstance(_up, _NDRPacket): self.ndr64 = _up.ndr64 - return super(_NDRPacket, self).dissect(s) + 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_ndr_on(f, self.ndr64) + _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): @@ -921,22 +1415,24 @@ def default_payload_class(self, pkt): def clone_with(self, *args, **kwargs): pkt = super(_NDRPacket, self).clone_with(*args, **kwargs) - # We need to copy defered_pointers to not break pointer deferral + # We need to copy deferred_pointers to not break pointer deferral # on build. - pkt.defered_pointers = self.defered_pointers + 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.defered_pointers = self.defered_pointers + 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).show( - dump, indent, 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: @@ -950,6 +1446,16 @@ def getfield_and_val(self, attr): pass raise + def valueof(self, request): + """ + 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): @@ -978,58 +1484,40 @@ 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("CONFORMANT_COUNT", 0) + conformants = dct.get("DEPORTED_CONFORMANTS", []) if conformants: - if conformants == 1: + amount = len(conformants) + if amount == 1: newcls.fields_desc.insert( 0, - MultipleTypeField( - [ - ( - NDRLongField("max_count", 0), - lambda pkt: pkt and pkt.ndr64, - ) - ], - NDRIntField("max_count", 0), - ), + _VirtualField("max_count", None), ) else: newcls.fields_desc.insert( 0, - MultipleTypeField( - [ - ( - NDRAlign( - FieldListField( - "max_counts", - 0, - LELongField("", 0), - count_from=lambda _: conformants, - ), - align=(8, 8), - ), - lambda pkt: pkt and pkt.ndr64, - ) - ], - NDRAlign( - FieldListField( - "max_counts", - 0, - LEIntField("", 0), - count_from=lambda _: conformants, - ), - align=(4, 4), - ), + FieldListField( + "max_counts", + [], + _VirtualField("", 0), + count_from=lambda _: amount, ), ) return newcls # type: ignore -@six.add_metaclass(_NDRPacketMetaclass) -class NDRPacket(_NDRPacket): +class NDRPacket(_NDRPacket, metaclass=_NDRPacketMetaclass): """ A NDR Packet. Handles pointer size & endianness """ @@ -1039,79 +1527,120 @@ class NDRPacket(_NDRPacket): # NDR64 pad structures # [MS-RPCE] 2.2.5.3.4.1 ALIGNMENT = (1, 1) - # Conformants max_count can be added to the beginning - CONFORMANT_COUNT = 0 + # [C706] sect 14.3.7 - Conformants max_count can be added to the beginning + DEPORTED_CONFORMANTS = [] # Primitive types -NDRByteField = ByteField -NDRSignedByteField = SignedByteField -class NDRShortField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRShortField, self).__init__(LEShortField(*args, **kwargs), align=(2, 2)) +class _NDRValueOf: + def valueof(self, pkt, x): + return x -class NDRSignedShortField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRSignedShortField, self).__init__( - LESignedShortField(*args, **kwargs), align=(2, 2) - ) +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"] -class NDRIntField(NDRAlign): def __init__(self, *args, **kwargs): - super(NDRIntField, self).__init__(LEIntField(*args, **kwargs), align=(4, 4)) + 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 NDRSignedIntField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRSignedIntField, self).__init__( - LESignedIntField(*args, **kwargs), align=(4, 4) - ) +class NDRByteField(_NDRLenField, ByteField): + pass -class NDRLongField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRLongField, self).__init__(LELongField(*args, **kwargs), align=(8, 8)) +class NDRSignedByteField(_NDRLenField, SignedByteField): + pass -class NDRSignedLongField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRSignedLongField, self).__init__( - LESignedLongField(*args, **kwargs), align=(8, 8) - ) +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) -class NDRIEEEFloatField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRIEEEFloatField, self).__init__( - IEEEFloatField(*args, **kwargs), align=(4, 4) - ) + 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 NDRIEEEDoubleField(NDRAlign): - def __init__(self, *args, **kwargs): - super(NDRIEEEDoubleField, self).__init__( - IEEEDoubleField(*args, **kwargs), align=(8, 8) - ) +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(EnumField): +class _NDREnumField(_NDRValueOf, EnumField): # [MS-RPCE] sect 2.2.5.2 - Enums are 4 octets in NDR64 - FMTS = [") + class NDRConstructedType(object): def __init__(self, fields): @@ -1260,7 +1845,7 @@ def rec_check_deferral(self): # 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.deferred: + if isinstance(f, NDRFullPointerField) and f.EMBEDDED: self.handles_deferred = True if isinstance(f, NDRConstructedType): f.rec_check_deferral() @@ -1273,68 +1858,124 @@ def getfield(self, pkt, s): if isinstance(fval, _NDRPacket): # If a sub-packet we just dissected has deferred pointers, # pass it to parent packet to propagate. - pkt.defered_pointers.extend(fval.defered_pointers) - del fval.defered_pointers[:] + pkt.deferred_pointers.extend(fval.deferred_pointers) + del fval.deferred_pointers[:] if self.handles_deferred: - # Now read content of the pointers that were deferred - q = deque() - q.extend(pkt.defered_pointers) - del pkt.defered_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.defered_pointers) - del val.defered_pointers[:] + # 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): - # Same logic than above, same comments. - s = super(NDRConstructedType, self).addfield(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): - pkt.defered_pointers.extend(val.defered_pointers) - del val.defered_pointers[:] + # 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: - q = deque() - q.extend(pkt.defered_pointers) - del pkt.defered_pointers[:] - while q: - addfld, fval = q.popleft() - s = addfld(s) - if isinstance(fval, NDRPointer) and isinstance(fval.value, _NDRPacket): - q.extend(fval.value.defered_pointers) - del fval.value.defered_pointers[:] + # 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(PacketField): +class _NDRPacketField(_NDRValueOf, PacketField): def m2i(self, pkt, m): - return self.cls(m, ndr64=pkt.ndr64, _parent=pkt) + return self.cls(m, ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, _parent=pkt) -# class _NDRPacketPadField(PadField): -# def padlen(self, flen, pkt): -# if pkt.ndr64: -# return -flen % self._align[1] -# else: -# return 0 +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): - fld = _NDRPacketField(name, default, pkt_cls=pkt_cls, **kwargs) + self.DEPORTED_CONFORMANTS = pkt_cls.DEPORTED_CONFORMANTS + self.fld = _NDRPacketField(name, default, pkt_cls=pkt_cls, **kwargs) NDRAlign.__init__( self, - # There is supposed to be padding after a struct in NDR64? - # _NDRPacketPadField(fld, align=pkt_cls.ALIGNMENT), - fld, + _NDRPacketPadField(self.fld, align=pkt_cls.ALIGNMENT), 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 @@ -1351,28 +1992,39 @@ class _NDRPacketListField(NDRConstructedType, PacketListField): def __init__(self, name, default, pkt_cls, **kwargs): self.ptr_pack = kwargs.pop("ptr_pack", False) - PacketListField.__init__(self, name, default, pkt_cls=pkt_cls, **kwargs) if self.ptr_pack: - self.fld = NDRFullPointerField( - NDRPacketField("", None, pkt_cls), deferred=True - ) + 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): """ @@ -1382,9 +2034,20 @@ class NDRFieldListField(NDRConstructedType, FieldListField): islist = 1 def __init__(self, *args, **kwargs): + kwargs.pop("ptr_pack", None) # TODO: unimplemented + 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 = [ @@ -1405,35 +2068,61 @@ class NDRVaryingArray(_NDRPacket): ] -class _NDRVarField(object): +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 + super(_NDRVarField, self).__init__(*args, **kwargs) + def getfield(self, pkt, s): - fmt = ["", 0x10: "<"}.get(pkt.underlayer.Endianness, "<") + + class NDRSerialization1Header(Packet): fields_desc = [ ByteField("Version", 1), - ByteEnumField("Endianness", 0, {0x00: "Big-endian", 0x10: "Little-endian"}), + 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 = [ - LEIntField("ObjectBufferLength", 0), - LEIntField("Filler", 0), + EField( + LEIntField("ObjectBufferLength", 0), endianness_from=_get_ndrtype1_endian + ), + XLEIntField("Filler", 0), ] -def ndr_deserialize1(b, cls, ndr64=False): +def ndr_deserialize1(b, cls, ptr_pack=False): """ - Deserialize Type Serialization Version 1 according to [MS-RPCE] sect 2.2.6 + 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 ( - NDRSerialization1Header(b[:8]) / - NDRSerialization1PrivateHeader(b[8:16]) / - NDRPointer( - ndr64=ndr64, - referent_id=struct.unpack(" 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 @@ -1856,34 +2757,79 @@ class DceRpcSession(DefaultSession): """ def __init__(self, *args, **kwargs): - self.rpc_bind_interface = None + 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.auth_context_id = kwargs.pop("auth_context_id", 0) + self.sent_cont_ids = [] + self.cont_id = 0 # Currently selected context 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 _process_dcerpc_packet(self, pkt): + 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: + 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] + self.rpc_bind_interface = DCE_RPC_INTERFACES[(if_uuid, if_version)] + self.rpc_bind_is_com = False except KeyError: - log_runtime.warning( - "Unknown RPC interface %s. Try loading the IDL" % if_uuid - ) - elif DceRpc5BindAck in pkt: + 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 res in pkt[DceRpc5BindAck].results: + 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 = [] + + # 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 - self.map_callid_opnum[pkt.call_id] = opnum, pkt[DceRpc5Request].payload + if self.rpc_bind_is_com: + self.map_callid_opnum[pkt.call_id] = ( + opnum, + pkt[DceRpc5Request].payload.payload, + ) + else: + self.map_callid_opnum[pkt.call_id] = opnum, pkt[DceRpc5Request].payload elif DceRpc5Response in pkt: # response => get opnum from table try: @@ -1891,11 +2837,225 @@ def _process_dcerpc_packet(self, pkt): del self.map_callid_opnum[pkt.call_id] 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 = 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 - if pkt.auth_verifier and pkt.auth_verifier.is_encrypted(): - return pkt + 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 and conf.raw_layer in pkt: + if opnum is not None and self.rpc_bind_interface: # use opnum to parse the payload is_response = DceRpc5Response in pkt try: @@ -1905,19 +3065,241 @@ def _process_dcerpc_packet(self, pkt): "Unknown opnum %s for interface %s" % (opnum, self.rpc_bind_interface) ) - return - # Dissect payload using class - payload = cls(pkt[conf.raw_layer].load, ndr64=self.ndr64, **opts) - pkt[conf.raw_layer].underlayer.remove_payload() - pkt = pkt / payload + 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 on_packet_received(self, pkt): - if DceRpc5 in pkt: - return super(DceRpcSession, self).on_packet_received( - self._process_dcerpc_packet(pkt) - ) - return super(DceRpcSession, self).on_packet_received(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), + auth_context_id=kwargs.pop("auth_context_id", None), + support_header_signing=kwargs.pop("support_header_signing", True), + ) + super(DceRpcSocket, self).__init__(*args, **kwargs) + + def send(self, x, **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, **kwargs) + else: + super(DceRpcSocket, self).send(pkt, **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 @@ -1948,7 +3330,7 @@ def dispatch_hook(cls, _pkt, _underlayer=None, *args, **kargs): 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)") + log_runtime.warning("DCE/RPC payload class not found or undefined (using Raw)") return Raw @classmethod diff --git a/scapy/layers/dhcp.py b/scapy/layers/dhcp.py index 2e2f832e462..39250cf74a8 100644 --- a/scapy/layers/dhcp.py +++ b/scapy/layers/dhcp.py @@ -12,8 +12,6 @@ - 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: @@ -36,6 +34,7 @@ FlagsField, IntField, IPField, + MACField, ShortField, StrEnumField, StrField, @@ -45,7 +44,7 @@ from scapy.layers.inet import UDP, IP 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, str2mac +from scapy.utils import atol, itom, ltoa, sane, str2mac, mac2str from scapy.volatile import ( RandBin, RandByte, @@ -54,19 +53,33 @@ RandInt, RandNum, RandNumExpo, + VolatileValue, ) -from scapy.arch import get_if_raw_hwaddr -from scapy.sendrecv import srp1, sendp +from scapy.arch import get_if_hwaddr +from scapy.sendrecv import srp1 from scapy.error import warning -import scapy.libs.six as six 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]) @@ -118,12 +131,12 @@ def answers(self, other): return self.xid == other.xid -class _DHCPParamReqFieldListField(FieldListField): +class _DHCPByteFieldListField(FieldListField): def randval(self): - class _RandReqFieldList(RandField): + class _RandByteFieldList(RandField): def _fix(self): return [RandByte()] * int(RandByte()) - return _RandReqFieldList() + return _RandByteFieldList() class RandClasslessStaticRoutesField(RandField): @@ -190,15 +203,7 @@ def i2m(self, pkt, x): return struct.pack('b', prefix) + dest + router def getfield(self, pkt, s): - if not s: - return None - prefix = orb(s[0]) - # 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 s, [] - route_len = 5 + (prefix + 7) // 8 return s[route_len:], self.m2i(pkt, s[:route_len]) @@ -288,7 +293,7 @@ def randval(self): 52: ByteField("dhcp-option-overload", 100), 53: ByteEnumField("message-type", 1, DHCPTypes), 54: IPField("server_id", "0.0.0.0"), - 55: _DHCPParamReqFieldListField( + 55: _DHCPByteFieldListField( "param_req_list", [], ByteField("opcode", 0)), 56: "error_message", @@ -314,6 +319,7 @@ def randval(self): 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"), @@ -329,9 +335,10 @@ def randval(self): 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"), @@ -346,6 +353,9 @@ def randval(self): 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", @@ -362,7 +372,7 @@ def randval(self): DHCPRevOptions = {} -for k, v in six.iteritems(DHCPOptions): +for k, v in DHCPOptions.items(): if isinstance(v, str): n = v v = None @@ -382,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") @@ -399,6 +409,9 @@ def _fix(self): op.append((o.name, r)) return op + def __iter__(self): + return iter(self._fix()) + class DHCPOptionsField(StrField): """ @@ -451,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: @@ -550,10 +573,10 @@ def dhcp_request(hw=None, if hw is None: if iface is None: iface = conf.iface - _, hw = get_if_raw_hwaddr(iface) + hw = get_if_hwaddr(iface) dhcp_options = [ ('message-type', req_type), - ('client_id', b'\x01' + hw), + ('client_id', b'\x01' + mac2str(hw)), ] if requested_addr is not None: dhcp_options.append(('requested_addr', requested_addr)) @@ -586,10 +609,36 @@ def dhcp_request(hw=None, 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)) @@ -597,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 @@ -608,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): @@ -617,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): @@ -646,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 4230cbdc97a..ca24bb17b25 100644 --- a/scapy/layers/dhcp6.py +++ b/scapy/layers/dhcp6.py @@ -10,13 +10,12 @@ 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 @@ -33,9 +32,9 @@ IPv6 from scapy.packet import Packet, bind_bottom_up from scapy.pton_ntop import inet_pton +from scapy.sendrecv import send from scapy.themes import Color from scapy.utils6 import in6_addrtovendor, in6_islladdr -import scapy.libs.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 @@ -126,7 +129,9 @@ def _dhcp6_dispatcher(x, *args, **kargs): 66: "OPTION_RELAY_SUPPLIED_OPTIONS", # RFC6422 68: "OPTION_VSS", # RFC6607 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", @@ -175,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 @@ -183,11 +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 7.3 RFC 8415 : DHCP6 Messages types +# also RFC 9686 dhcp6types = {1: "SOLICIT", 2: "ADVERTISE", 3: "REQUEST", @@ -200,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", + } ##################################################################### @@ -416,7 +428,7 @@ class _OptReqListField(StrLenField): islist = 1 def i2h(self, pkt, x): - if x is None: + if not x: return [] return x @@ -961,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), @@ -1028,6 +1094,14 @@ 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), @@ -1038,6 +1112,12 @@ class DHCP6OptMudUrl(_DHCP6OptGuessPayload): # RFC8520 )] +class DHCP6OptAddrRegEnable(_DHCP6OptGuessPayload): # RFC 9686 sect 4.1 + name = "DHCP6 Address Registration Option" + fields_desc = [ShortEnumField("optcode", 148, dhcp6opts), + ShortField("optlen", 0)] + + ##################################################################### # DHCPv6 messages # ##################################################################### @@ -1372,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}) @@ -1379,6 +1477,7 @@ def answers(self, other): class DHCPv6_am(AnsweringMachine): function_name = "dhcp6d" filter = "udp and port 546 and port 547" + send_function = staticmethod(send) def usage(self): msg = """ @@ -1522,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) @@ -1717,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) @@ -1777,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) @@ -1866,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 index 2c7718cef59..75e9be9330b 100644 --- a/scapy/layers/dns.py +++ b/scapy/layers/dns.py @@ -4,37 +4,73 @@ # Copyright (C) Philippe Biondi """ -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.arch import get_if_addr, get_if_addr6 -from scapy.ansmachine import AnsweringMachine, AnsweringMachineUtils -from scapy.base_classes import Net +from scapy.arch import ( + get_if_addr, + get_if_addr6, + read_nameservers, +) +from scapy.ansmachine import AnsweringMachine +from scapy.base_classes import Net, ScopedIP from scapy.config import conf -from scapy.compat import orb, raw, chb, bytes_encode, plain_str +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, NoPayload, Raw -from scapy.fields import BitEnumField, BitField, ByteEnumField, ByteField, \ - ConditionalField, Field, FieldLenField, FlagsField, IntField, \ - PacketListField, ShortEnumField, ShortField, StrField, \ - StrLenField, MultipleTypeField, UTCTimeField, I -from scapy.sendrecv import sr1 +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 -import scapy.libs.six as six +from scapy.layers.inet6 import IPv6 - -from scapy.compat import ( +from typing import ( Any, + List, Optional, Tuple, Type, @@ -44,7 +80,7 @@ # https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4 dnstypes = { - 0: "ANY", + 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", @@ -68,88 +104,107 @@ dnsclasses = {1: 'IN', 2: 'CS', 3: 'CH', 4: 'HS', 255: 'ANY'} -def dns_get_str(s, pointer=0, pkt=None, _fullpacket=False): +# 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, 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: 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: 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, len(processed_pointers) != 0 + # name, remaining + return name or b".", bytes_left def _is_ptr(x): - return b"." not in x and ( - (x and orb(x[-1]) == 0) or - (len(x) >= 2 and (orb(x[-2]) & 0xc0) == 0xc0) + """ + 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) ) @@ -196,22 +251,21 @@ def dns_compress(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] @@ -269,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): @@ -284,11 +338,22 @@ class DNSStrField(StrLenField): It will also handle DNS decompression. (may be StrLenField if a length_from is passed), """ - __slots__ = ["compressed"] + 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 @@ -299,114 +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 = super(DNSStrField, self).getfield(pkt, s) # Decode the compressed DNS message - decoded, _, left, self.compressed = 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, 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", "rr"] - holds_packets = 1 - - def __init__(self, name, countfld, default, passon=1): - StrField.__init__(self, name, None) - self.countfld = countfld - # Notes: - # - self.rr: used by DNSRRCountField() to compute the records count - # - self.default: used to set the default record - self.rr = self.default = default - 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) - - # Reset rdlen if DNS compression was used - for fname in rr.fieldtype.keys(): - rdata_obj = rr.fieldtype[fname] - if fname == "rdata" and isinstance(rdata_obj, MultipleTypeField): - rdata_obj = rdata_obj._find_fld_pkt_val(rr, rr.type)[0] - if isinstance(rdata_obj, DNSStrField) and rdata_obj.compressed: - del rr.rdlen - break - 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): - log_runtime.info("DNS 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) @@ -414,13 +388,18 @@ 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): log_runtime.info( "DNS RR TXT prematured end of character-string " @@ -441,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. @@ -453,110 +435,28 @@ def i2m(self, pkt, s): return ret_s -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 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", DNSQR()), - DNSRRField("an", "ancount", None), - DNSRRField("ns", "nscount", None), - DNSRRField("ar", "arcount", None, 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!" - 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) +# RFC 2671 - Extension Mechanisms for DNS (EDNS0) - return s +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"} -# RFC 2671 - Extension Mechanisms for DNS (EDNS0) +class _EDNS0Dummy(Packet): + name = "Dummy class that implements extract_padding()" -edns0types = {0: "Reserved", 1: "LLQ", 2: "UL", 3: "NSID", 4: "Reserved", - 5: "PING", 8: "edns-client-subnet"} + def extract_padding(self, p): + # type: (bytes) -> Tuple[bytes, Optional[bytes]] + return "", p -class EDNS0TLV(Packet): +class EDNS0TLV(_EDNS0Dummy): name = "DNS EDNS0 TLV" fields_desc = [ShortEnumField("optcode", 0, edns0types), FieldLenField("optlen", None, "optdata", fmt="H"), StrLenField("optdata", "", length_from=lambda pkt: pkt.optlen)] - def extract_padding(self, p): - # type: (bytes) -> Tuple[bytes, Optional[bytes]] - return "", p - @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): # type: (Optional[bytes], *Any, **Any) -> Type[Packet] @@ -565,16 +465,14 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): if len(_pkt) < 2: return Raw edns0type = struct.unpack("!H", _pkt[:2])[0] - if edns0type == 8: - return EDNS0ClientSubnet - return EDNS0TLV + 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 @@ -585,6 +483,60 @@ class DNSRROPT(InheritOriginDNSStrPacket): 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) >= 18), + ConditionalField( + StrLenField("password", "", + length_from=lambda pkt: pkt.optlen - 18), + lambda pkt: (pkt.optlen or 0) >= 22)] + + 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 + + +# 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)] + + +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): @@ -610,7 +562,7 @@ 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 orb(packed_subnet[i]) != 0: + if packed_subnet[i] != 0: i += 1 break return packed_subnet[:i] @@ -642,7 +594,7 @@ class ClientSubnetv6(ClientSubnetv4): af_default = b"\x20" # 2000:: -class EDNS0ClientSubnet(Packet): +class EDNS0ClientSubnet(_EDNS0Dummy): name = "DNS EDNS0 Client Subnet" fields_desc = [ShortEnumField("optcode", 8, edns0types), FieldLenField("optlen", None, "address", fmt="H", @@ -664,21 +616,76 @@ class EDNS0ClientSubnet(Packet): length_from=lambda p: p.source_plen))] -# RFC 4034 - Resource Records for the DNS Security Extensions +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", +} -# 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"} +# 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, +} -# 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 +# RFC 4034 - Resource Records for the DNS Security Extensions def bitmap2RRlist(bitmap): """ @@ -695,9 +702,9 @@ def bitmap2RRlist(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: log_runtime.info("bitmap length is no valid (%i)", bitmap_len) @@ -709,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 @@ -773,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): @@ -798,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), @@ -832,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), @@ -851,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", []) ] @@ -863,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???????"), @@ -879,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), @@ -905,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), @@ -915,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", []) ] @@ -923,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), @@ -933,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) @@ -940,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), @@ -1011,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 @@ -1022,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( @@ -1049,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), ], @@ -1062,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 + 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): @@ -1114,67 +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 use this automaton for llmnr_spoof + cls = DNS # We also use this automaton for llmnrd / mdnsd def parse_options(self, joker=None, - match=None, joker6=None, from_ip=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): """ - :param joker: default IPv4 for unresolved domains. (Default: None) + Simple DNS answering machine. + + :param joker: default IPv4 for unresolved domains. Set to False to disable, None to mirror the interface's IP. - :param joker6: default IPv6 for unresolved domains (Default: False) - set to False to disable, None to mirror the interface's IPv6. - :param match: a dictionary of {names: (ip, ipv6)} - :param from_ip: an source IP to filter. Can contain a netmask + 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, + ... ) """ - if match is None: - self.match = {} - else: - self.match = match + 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()} + + self.send_error = send_error + self.relay = relay if isinstance(from_ip, str): self.from_ip = Net(from_ip) else: 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): from scapy.layers.inet6 import IPv6 return ( req.haslayer(self.cls) and - req.getlayer(self.cls).qr == 0 and - (not self.from_ip or ( - req[IPv6].src in req if IPv6 in req else req[IP].src - ) in self.from_ip) + 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): - resp = AnsweringMachineUtils.reverse_packet(req) - dns = req.getlayer(self.cls) - if req.qd.qtype == 28: - # AAAA - if self.joker6 is False: - return - rdata = self.match.get( - dns.qd.qname, - self.joker or get_if_addr6(self.optsniff.get("iface", conf.iface)) - ) - if isinstance(rdata, (tuple, list)): - rdata = rdata[1] - resp /= self.cls(id=dns.id, qr=1, qd=dns.qd, - an=DNSRR(rrname=dns.qd.qname, ttl=10, rdata=rdata, - type=28)) + # 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: - if self.joker is False: - return - rdata = self.match.get( - dns.qd.qname, - self.joker or get_if_addr(self.optsniff.get("iface", conf.iface)) + 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, ) - if isinstance(rdata, (tuple, list)): - # Fallback - rdata = rdata[0] - resp /= self.cls(id=dns.id, qr=1, qd=dns.qd, - an=DNSRR(rrname=dns.qd.qname, ttl=10, rdata=rdata)) + 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 bc5703fdea0..862ed6686de 100644 --- a/scapy/layers/dot11.py +++ b/scapy/layers/dot11.py @@ -11,7 +11,6 @@ - RadioTap """ -from __future__ import print_function import re import struct from zlib import crc32 @@ -39,6 +38,7 @@ LEShortEnumField, LEShortField, LESignedIntField, + MayEnd, MultipleTypeField, OUIField, PacketField, @@ -63,8 +63,16 @@ 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 + default_backend = Ciphers = algorithms = decrepit_algorithms = None log_loading.info("Can't import python-cryptography v1.7+. Disabled WEP decryption/encryption. (Dot11)") # noqa: E501 @@ -203,7 +211,7 @@ def answers(self, other): 'SGINsysmDis', 'LDPCextraOFDM', 'Beamformed', 'res1', 'res2'] -_rt_hemuother_per_user_known = { +_rt_hemuother_per_user_known = [ 'user field position', 'STA-ID', 'NSTS', @@ -212,7 +220,7 @@ def answers(self, other): 'MCS', 'DCM', 'Coding', -} +] # Radiotap utils @@ -683,7 +691,7 @@ def i2repr(self, pkt, val): return s -# 802.11-2016 9.2.4.1.1 +# 802.11-2020 9.2.4.1.1 class Dot11(Packet): name = "802.11" fields_desc = [ @@ -702,24 +710,39 @@ class Dot11(Packet): FlagsField("FCfield", 0, 4, ["pw-mgt", "MD", "protected", "order"]), lambda pkt: (pkt.type, pkt.subtype) == (1, 6) + ), + ( + FlagsField("FCfield", 0, 2, + ["security", "AP_PM"]), + lambda pkt: (pkt.type, pkt.subtype) == (3, 1) ) ], FlagsField("FCfield", 0, 8, ["to-DS", "from-DS", "MF", "retry", "pw-mgt", "MD", "protected", "order"]) ), - ShortField("ID", 0), + ConditionalField( + BitField("FCfield_bw", 0, 3), + lambda pkt: (pkt.type, pkt.subtype) == (3, 1) + ), + ConditionalField( + FlagsField("FCfield2", 0, 3, + ["next_tbtt", "comp_ssid", "ano"]), + lambda pkt: (pkt.type, pkt.subtype) == (3, 1) + ), + LEShortField("ID", 0), _Dot11MacField("addr1", ETHER_ANY, 1), ConditionalField( _Dot11MacField("addr2", ETHER_ANY, 2), - lambda pkt: (pkt.type != 1 or - pkt.subtype in [0x8, 0x9, 0xa, 0xb, 0xe, 0xf]), + lambda pkt: (pkt.type not in {1, 3} or + pkt.subtype in [0x4, 0x5, 0x6, 0x8, 0x9, 0xa, 0xb, 0xe, 0xf]), ), ConditionalField( _Dot11MacField("addr3", ETHER_ANY, 3), - lambda pkt: pkt.type in [0, 2], + lambda pkt: (pkt.type in [0, 2] or + ((pkt.type, pkt.subtype) == (1, 6) and pkt.cfe == 6)), ), - ConditionalField(LEShortField("SC", 0), lambda pkt: pkt.type != 1), + ConditionalField(LEShortField("SC", 0), lambda pkt: pkt.type not in {1, 3}), ConditionalField( _Dot11MacField("addr4", ETHER_ANY, 4), lambda pkt: (pkt.type == 2 and @@ -735,7 +758,7 @@ def guess_payload_class(self, payload): 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. @@ -770,6 +793,8 @@ def address_meaning(self, index): 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][ @@ -957,6 +982,7 @@ def network_stats(self): 32: "Power Constraint", 33: "Power Capability", 36: "Supported Channels", + 37: "Channel Switch Announcement", 42: "ERP", 45: "HT Capabilities", 46: "QoS Capability", @@ -964,9 +990,11 @@ def network_stats(self): 50: "Extended Supported Rates", 52: "Neighbor Report", 61: "HT Operation", + 74: "Overlapping BSS Scan Parameters", 107: "Interworking", - 127: "Extendend Capabilities", + 127: "Extended Capabilities", 191: "VHT Capabilities", + 192: "VHT Operation", 221: "Vendor Specific" } @@ -1265,17 +1293,25 @@ class Dot11EltCountry(Dot11Elt): 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 ) ] @@ -1403,22 +1439,25 @@ class Dot11EltVendorSpecific(Dot11Elt): def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt: oui = struct.unpack("!I", b"\x00" + _pkt[2:5])[0] - if oui == 0x0050f2: # Microsoft - 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 + 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" @@ -1431,6 +1470,90 @@ class Dot11EltMicrosoftWPA(Dot11EltVendorSpecific): 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 # @@ -1496,7 +1619,13 @@ class Dot11Auth(_Dot11EltUtils): 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 @@ -1510,6 +1639,248 @@ 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 # ################### @@ -1554,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() @@ -1579,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() @@ -1650,6 +2021,8 @@ class Dot11CCMP(Dot11Encrypted): 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) @@ -1657,12 +2030,15 @@ class Dot11CCMP(Dot11Encrypted): 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,) @@ -1673,6 +2049,11 @@ class Dot11CCMP(Dot11Encrypted): 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) @@ -1734,7 +2115,7 @@ def make_reply(self, p): ip = p.getlayer(IP) tcp = p.getlayer(TCP) pay = raw(tcp.payload) - del p.payload.payload.payload + 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) diff --git a/scapy/layers/eap.py b/scapy/layers/eap.py index bb59a9a81a3..2894570c18a 100644 --- a/scapy/layers/eap.py +++ b/scapy/layers/eap.py @@ -7,16 +7,38 @@ 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.config import conf from scapy.compat import orb, chb @@ -406,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 ############################################################################# @@ -767,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/gssapi.py b/scapy/layers/gssapi.py index cdee4b31bc7..547e09a4734 100644 --- a/scapy/layers/gssapi.py +++ b/scapy/layers/gssapi.py @@ -6,66 +6,59 @@ """ Generic Security Services (GSS) API -Implements parts of -- GSSAPI: RFC2743 -- GSSAPI SPNEGO: RFC4178 > RFC2478 -- GSSAPI SPNEGO NEGOEX: [MS-NEGOEX] +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 struct -from uuid import UUID +import abc -from scapy.asn1.asn1 import ASN1_SEQUENCE, ASN1_Class_UNIVERSAL, ASN1_Codecs +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 from scapy.asn1.mib import conf # loads conf.mib from scapy.asn1fields import ( - ASN1F_CHOICE, - ASN1F_ENUMERATED, - ASN1F_FLAGS, ASN1F_OID, ASN1F_PACKET, ASN1F_SEQUENCE, - ASN1F_SEQUENCE_OF, - ASN1F_STRING, - ASN1F_optional ) from scapy.asn1packet import ASN1_Packet +from scapy.error import log_runtime from scapy.fields import ( - FieldListField, + FieldLenField, LEIntEnumField, - LEIntField, - LELongEnumField, - LELongField, - LEShortField, - MultipleTypeField, PacketField, - PacketListField, - StrFixedLenField, - UUIDEnumField, - UUIDField, - StrField, - XStrFixedLenField, - XStrLenField + StrLenField, ) -from scapy.packet import Packet, bind_layers +from scapy.packet import Packet -# Providers -from scapy.layers.kerberos import ( - Kerberos, - KRB5_GSS, -) -from scapy.layers.ntlm import ( - NEGOEX_EXCHANGE_NTLM, - NTLM_Header, - _NTLMPayloadField, -) - -from scapy.compat import ( - Dict, +# Type hints +from typing import ( + Any, + List, + Optional, Tuple, ) - # https://datatracker.ietf.org/doc/html/rfc1508#page-48 @@ -82,371 +75,632 @@ class BERcodec_GSSAPI_APPLICATION(BERcodec_SEQUENCE): tag = ASN1_Class_GSSAPI.APPLICATION -class ASN1F_SNMP_GSSAPI_APPLICATION(ASN1F_SEQUENCE): +class ASN1F_GSSAPI_APPLICATION(ASN1F_SEQUENCE): ASN1_tag = ASN1_Class_GSSAPI.APPLICATION -# SPNEGO negTokenInit -# https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.1 +# GSS API Blob +# https://datatracker.ietf.org/doc/html/rfc4121 -class SPNEGO_MechType(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_OID("oid", None) - +# Filled by providers +_GSSAPI_OIDS = {} +_GSSAPI_SIGNATURE_OIDS = {} -class SPNEGO_MechTypes(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE_OF("mechTypes", None, SPNEGO_MechType) +# section 4.1 -class SPNEGO_MechListMIC(ASN1_Packet): +class GSSAPI_BLOB(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE(ASN1F_STRING("value", "")) + 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 ord(_pkt[:1]) & 0xA0 >= 0xA0: + from scapy.layers.spnego import SPNEGO_negToken -_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 -} + # 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 + if _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) + return cls -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) - 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 - return dat, r +# Same but to store the signatures (e.g. DCE/RPC) -class SPNEGO_Token(ASN1_Packet): +class GSSAPI_BLOB_SIGNATURE(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = _SPNEGO_Token_Field("value", None) + 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 -_ContextFlags = ["delegFlag", - "mutualFlag", - "replayFlag", - "sequenceFlag", - "superseded", - "anonFlag", - "confFlag", - "integFlag"] + 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 SPNEGO_negTokenInit(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - 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) - ), - ASN1F_optional( - ASN1F_PACKET("mechListMIC", None, - SPNEGO_MechListMIC, - implicit_tag=0xa3) - ) - ) - ) +class _GSSAPI_Field(PacketField): + """ + PacketField that contains a GSSAPI_BLOB_SIGNATURE, but one that can + have a payload when not encrypted. + """ -# SPNEGO negTokenTarg -# https://datatracker.ietf.org/doc/html/rfc4178#section-4.2.2 + __slots__ = ["pay_cls"] -class SPNEGO_negTokenResp(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE( - ASN1F_optional( - ASN1F_ENUMERATED("negResult", 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, - implicit_tag=0xa3) - ) + def __init__(self, name, pay_cls): + self.pay_cls = pay_cls + super().__init__( + name, + None, + GSSAPI_BLOB_SIGNATURE, ) - ) - -class SPNEGO_negToken(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE("token", SPNEGO_negTokenInit(), - ASN1F_PACKET("negTokenInit", - SPNEGO_negTokenInit(), - SPNEGO_negTokenInit, - implicit_tag=0xa0), - ASN1F_PACKET("negTokenResp", - SPNEGO_negTokenResp(), - SPNEGO_negTokenResp, - implicit_tag=0xa1) - ) - -# 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]')", + 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", } -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: "CHALENGE", - 0x05: "AP_REQUEST", - 0x06: "VERIFY", - 0x07: "ALERT"}), - LEIntField("SequenceNum", 0), - LEIntField("cbHeaderLength", None), - LEIntField("cbMessageLength", None), - UUIDField("ConversationId", None), - ] +# GSS Structures - 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(" "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.getSignatureHash() + 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 + ) + ) -def _checksum_size(pkt): - if pkt.ChecksumType == 1: - return 4 - elif pkt.ChecksumType in [2, 4, 6, 7]: - return 16 - elif pkt.ChecksumType in [3, 8, 9]: - return 24 - elif pkt.ChecksumType == 5: - return 8 - elif pkt.ChecksumType in [10, 12, 13, 14, 15, 16]: - return 20 - return 0 +# --- The base GSSAPI SSP base class -class NEGOEX_CHECKSUM(Packet): - fields_desc = [ - LELongField("cbHeaderLength", 20), - LELongEnumField("ChecksumScheme", 1, {1: "CHECKSUM_SCHEME_RFC3961"}), - LELongEnumField("ChecksumType", None, _checksum_types), - XStrLenField("ChecksumValue", b"", length_from=_checksum_size) - ] +class GSS_C_FLAGS(IntFlag): + """ + Authenticator Flags per RFC2744 req_flags + """ -class NEGOEX_EXCHANGE_MESSAGE(Packet): - OFFSET = 64 - show_indent = 0 - fields_desc = [ - NEGOEX_MESSAGE_HEADER, - UUIDEnumField("AuthScheme", None, _NEGOEX_AUTH_SCHEMES), - LEIntField("ExchangeBufferOffset", 0), - LEIntField("ExchangeLen", 0), - _NTLMPayloadField( - 'Payload', OFFSET, [ - # The NEGOEX doc mentions the following blob as as an - # "opaque handshake for the client authentication scheme". - # NEGOEX_EXCHANGE_NTLM is a reversed interpretation, and is - # probably not accurate. - MultipleTypeField( - [ - (PacketField("Exchange", None, NEGOEX_EXCHANGE_NTLM), - lambda pkt: pkt.AuthScheme == \ - UUID("5c33530d-eaf9-0d4d-b2ec-4ae3786ec308")), - ], - StrField("Exchange", b"") - ) - ], - length_from=lambda pkt: pkt.cbMessageLength - pkt.cbHeaderLength), - ] + 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 NEGOEX_VERIFY_MESSAGE(Packet): - show_indent = 0 - fields_desc = [ - NEGOEX_MESSAGE_HEADER, - UUIDEnumField("AuthScheme", None, _NEGOEX_AUTH_SCHEMES), - PacketField("Checksum", NEGOEX_CHECKSUM(), - NEGOEX_CHECKSUM) - ] +class GSS_S_FLAGS(IntFlag): + """ + Equivalent to Microsoft's ASC_REQ* Flags in AcceptSecurityContext + """ + GSS_S_ALLOW_MISSING_BINDINGS = 0x10000000 -bind_layers(NEGOEX_NEGO_MESSAGE, NEGOEX_NEGO_MESSAGE) +class SSP: + """ + The general SSP class + """ -_mechDissector["1.3.6.1.4.1.311.2.2.30"] = NEGOEX_NEGO_MESSAGE + auth_type = 0x00 -# GSS API Blob -# https://datatracker.ietf.org/doc/html/rfc2743 + def __init__(self, **kwargs): + if kwargs: + raise ValueError("Unknown SSP parameters: " + ",".join(list(kwargs))) + def __repr__(self): + return "<%s>" % self.__class__.__name__ -_GSSAPI_OIDS = { - "1.3.6.1.5.5.2": SPNEGO_negToken, # SPNEGO: RFC 2478 - "1.2.840.113554.1.2.2": KRB5_GSS, # RFC 1964 -} + class CONTEXT: + """ + A Security context i.e. the 'state' of the secure negotiation + """ -# sect 3.1 + __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, + 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, + 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 + + # Passive + + @abc.abstractmethod + def GSS_Passive(self, Context: CONTEXT, 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, + ): + return self.GSS_GetMICEx( + Context, + [ + self.MIC_MSG( + sign=True, + data=message, + ) + ], + qop_req=qop_req, + ) -class GSSAPI_BLOB(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SNMP_GSSAPI_APPLICATION( - ASN1F_OID("MechType", "1.3.6.1.5.5.2"), - ASN1F_PACKET("innerContextToken", SPNEGO_negToken(), SPNEGO_negToken, - next_cls_cb=lambda pkt: _GSSAPI_OIDS.get( - pkt.MechType.val, conf.raw_layer)) - ) + # sect 2.3.2 + + def GSS_VerifyMIC( + self, + Context: CONTEXT, + message: bytes, + signature, + ): + self.GSS_VerifyMICEx( + Context, + [ + self.MIC_MSG( + sign=True, + data=message, + ) + ], + signature, + ) - @classmethod - def dispatch_hook(cls, _pkt=None, *args, **kargs): - if _pkt and len(_pkt) >= 1: - if ord(_pkt[:1]) & 0xa0 >= 0xa0: - # 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 - if _pkt[:7] == b"NTLMSSP": - # 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) - return cls + # sect 2.3.3 + + def GSS_Wrap( + self, + Context: CONTEXT, + input_message: bytes, + conf_req_flag: bool, + qop_req: int = GSS_C_QOP_DEFAULT, + ): + _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): + 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 canMechListMIC(self, Context: CONTEXT): + """ + Returns whether or not mechListMIC can be computed + """ + return False + + def getMechListMIC(self, Context, input): + """ + Compute mechListMIC + """ + return bytes(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 ca0868f927e..82e82606357 100644 --- a/scapy/layers/hsrp.py +++ b/scapy/layers/hsrp.py @@ -12,11 +12,11 @@ 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): @@ -48,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)] @@ -66,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 cd8f12d7af2..016337738fc 100644 --- a/scapy/layers/http.py +++ b/scapy/layers/http.py @@ -19,7 +19,7 @@ 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 @@ -28,47 +28,76 @@ 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) + +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 +import base64 +import datetime +import gzip import io import os import re import socket +import ssl import struct import subprocess -from scapy.base_classes import Net -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 +from scapy.supersocket import StreamSocket, SSLStreamSocket from scapy.utils import get_temp_file, ContextManagerSubprocess -from scapy.layers.inet import TCP, TCP_client - -from scapy.libs 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 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_zstd_available = False @@ -76,6 +105,7 @@ 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 @@ -91,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", @@ -126,7 +153,7 @@ "Range", "Referer", "TE", - "User-Agent" + "User-Agent", ] COMMON_UNSTANDARD_REQUEST_HEADERS = [ @@ -220,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() @@ -229,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 @@ -261,33 +288,38 @@ def _dissect_headers(obj, s): continue obj.setfieldval(f.name, value) if headers: - headers = dict(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): 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") @@ -298,30 +330,36 @@ 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) + 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 !" + "Can't import brotli. brotli decompression " "will be ignored !" ) elif "zstd" in encodings: if _is_zstd_available: @@ -342,98 +380,106 @@ def post_dissect(self, s): 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: - 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 !" - ) + 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, **kwargs): - ''' Takes an HTTPRequest or HTTPResponse object, and creates its - string representation.''' + """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): @@ -441,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""" @@ -454,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 @@ -550,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: @@ -581,36 +652,49 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): def tcp_reassemble(cls, data, metadata, _): 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) + # Subtract the length of the "HTTP*" layer if http_packet.payload.payload or length == 0: - http_length = len(data) - len(http_packet.payload.payload) + 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) - is_response = isinstance(http_packet.payload, HTTPResponse) + 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, HTTPRequest): + # end with a double CRLF. Same for HEAD responses + 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, @@ -632,7 +716,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): @@ -641,90 +725,277 @@ 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, - raw=False, iptables=False, iface=None, - **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 + """ + + def __init__( + self, + mech=HTTP_AUTH_MECHS.NONE, + verb=True, + sslcontext=None, + ssp=None, + no_check_certificate=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.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: + # 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, + 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 result in the default browser (default False) - :param raw: opens a raw socket instead of going through the OS's TCP - socket. Scapy will then use its own TCP client. - Careful, the OS might cancel the TCP connection with RST. - :param iptables: when raw is enabled, this calls iptables to temporarily - prevent the OS from sending TCP RST to the host IP. - On Linux, you'll almost certainly need this. :param iface: interface to use. Changing this turns on "raw" :param headers: any additional headers passed to the request :returns: the HTTPResponse packet """ - from scapy.sessions import TCPSession - 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) - ans = None - - # Open a socket - if iface is not None: - raw = True - if raw: - # Use TCP_client on a raw socket - iptables_rule = "iptables -%c INPUT -s %s -p tcp --sport 80 -j DROP" - if iptables: - host = str(Net(host)) - assert os.system(iptables_rule % ('A', host)) == 0 - sock = TCP_client.tcplink(HTTP, host, port, debug=verbose, - iface=iface) - else: - # Use a native TCP socket - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - sock = StreamSocket(sock, HTTP) - # Send the request and wait for the answer - try: - ans = sock.sr1( - req, - session=TCPSession(app=True), - timeout=timeout, - verbose=verbose - ) - finally: - sock.close() - if raw and iptables: - host = str(Net(host)) - assert os.system(iptables_rule % ('D', host)) == 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: @@ -752,3 +1023,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 159b17aa90a..6e01c9b253f 100644 --- a/scapy/layers/inet.py +++ b/scapy/layers/inet.py @@ -7,8 +7,6 @@ IPv4 (Internet Protocol v4). """ -from __future__ import absolute_import -from __future__ import print_function import time import struct import re @@ -19,12 +17,20 @@ from scapy.utils import checksum, do_graph, incremental_label, \ linehexdump, strxor, whois, colgen -from scapy.ansmachine import AnsweringMachine, AnsweringMachineUtils -from scapy.base_classes import Gen, Net +from scapy.ansmachine import AnsweringMachine +from scapy.base_classes import Gen, Net, _ScopedIP 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.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.fields import ( @@ -39,9 +45,12 @@ FieldListField, FlagsField, IPField, + IP6Field, IntField, + MayEnd, MultiEnumField, MultipleTypeField, + PacketField, PacketListField, ShortEnumField, ShortField, @@ -49,6 +58,7 @@ StrField, StrFixedLenField, StrLenField, + TrailerField, XByteField, XShortField, ) @@ -62,8 +72,6 @@ import scapy.as_resolvers -import scapy.libs.six as six - #################### # IP Tools class # #################### @@ -352,7 +360,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) @@ -363,7 +371,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 @@ -523,7 +531,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), @@ -536,7 +543,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 @@ -562,12 +569,15 @@ 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) + return conf.route.route(dst, dev=scope) def hashret(self): if ((self.proto == socket.IPPROTO_ICMP) and @@ -621,38 +631,7 @@ def mysummary(self): def fragment(self, fragsize=1480): """Fragment IP datagrams""" - lastfragsz = fragsize - fragsize -= fragsize % 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) - if len(s) <= lastfragsz: - lst.append(p) - continue - - nb = (len(s) - lastfragsz + fragsize - 1) // fragsize + 1 - 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 - fragend = (i + 1) * fragsize - else: - fragend = i * fragsize + lastfragsz - q[fnb].frag += i * fragsize // 8 - r = conf.raw_layer(load=s[i * fragsize:fragend]) - 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_pseudoheader(proto, u, plen): @@ -663,6 +642,7 @@ def in4_pseudoheader(proto, u, plen): :param u: IP layer instance :param plen: the length of the upper layer and payload """ + u = u.copy() if u.len is not None: if u.ihl is None: olen = sum(len(x) for x in u.options) @@ -697,7 +677,7 @@ def in4_chksum(proto, u, p): # type: (int, IP, bytes) -> int """IPv4 Pseudo Header checksum as defined in RFC793 - :param nh: value of upper layer protocol + :param proto: value of upper layer protocol :param u: upper layer instance :param p: the payload of the upper layer provided as a string """ @@ -891,6 +871,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", @@ -950,35 +1109,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 icmp_id_seq_types), # noqa: E501 - ConditionalField(XShortField("seq", 0), lambda pkt:pkt.type in icmp_id_seq_types), # 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 - 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)), - ] + 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 @@ -995,7 +1218,7 @@ def hashret(self): 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 @@ -1008,11 +1231,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" @@ -1043,6 +1273,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): @@ -1117,6 +1353,7 @@ 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) @@ -1127,6 +1364,9 @@ def mysummary(self): def inet_register_l3(l2, l3): + """ + Resolves the default L2 destination address when IP is used. + """ return getmacbyip(l3.dst) @@ -1141,6 +1381,9 @@ def inet_register_l3(l2, l3): @conf.commands.register def fragment(pkt, fragsize=1480): """Fragment a big IP datagram""" + if fragsize < 8: + warning("fragsize cannot be lower than 8") + fragsize = max(fragsize, 8) lastfragsz = fragsize fragsize -= fragsize % 8 lst = [] @@ -1185,86 +1428,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.extend(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: - c_len = len(ip.payload) - else: - c_len = ip.len - (ip.ihl << 2) - txt = conf.raw_layer() - for q in lst[1:]: - if c_len != q.frag << 3: # Wrong fragmentation offset - if c_len > q.frag << 3: - warning("Fragment overlap (%i > %i) %r || %r || %r" % (c_len, q.frag << 3, p, txt, q)) # noqa: E501 - missfrag.extend(lst) - break - if q[IP].len is None or q[IP].ihl is None: - c_len += 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: - c_len += 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 @@ -1366,9 +1638,9 @@ 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 li in list(k): # use list(): k is modified in the loop @@ -1499,12 +1771,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: @@ -1641,7 +1913,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 @@ -1892,19 +2164,26 @@ class TCP_client(Automaton): :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(prn=self._transmit_packet, store=False) + self.rcvbuf = TCPSession() bpf = "host %s and host %s and port %i and port %i" % (self.src, self.dst, self.sport, @@ -1989,7 +2268,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): @@ -2094,7 +2375,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) @@ -2164,6 +2445,68 @@ 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" @@ -2177,14 +2520,22 @@ def is_request(self, req): return False def print_reply(self, req, reply): - print("Replying %s to %s" % (reply.getlayer(IP).dst, req.dst)) + print("Replying %s to %s" % (reply[IP].dst, req[IP].dst)) def make_reply(self, req): - reply = AnsweringMachineUtils.reverse_packet(req) + reply = req.copy() reply[ICMP].type = 0 # echo-reply # Force re-generation of the checksum reply[ICMP].chksum = None - return reply[ICMP].underlayer + 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] diff --git a/scapy/layers/inet6.py b/scapy/layers/inet6.py index 4f3aff74191..b585c92ea70 100644 --- a/scapy/layers/inet6.py +++ b/scapy/layers/inet6.py @@ -12,9 +12,6 @@ """ -from __future__ import absolute_import -from __future__ import print_function - from hashlib import md5 import random import socket @@ -23,22 +20,67 @@ 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 from scapy.config import conf -from scapy.data import DLT_IPV6, DLT_RAW, DLT_RAW_ALT, ETHER_ANY, ETH_P_IPV6, \ - MTU +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, 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.libs.six as six +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 @@ -49,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"): @@ -76,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 @@ -86,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 @@ -97,19 +147,24 @@ 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 @@ -264,15 +319,18 @@ class IPv6(_IPv6GuessPayload, Packet, IPTools): 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) @@ -366,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) @@ -423,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: @@ -432,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: @@ -448,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) @@ -753,6 +816,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), @@ -788,6 +872,7 @@ def extract_padding(self, p): _hbhoptcls = {0x00: Pad1, 0x01: PadN, 0x05: RouterAlert, + 0x63: RplOption, 0xC2: Jumbo, 0xC9: HAO} @@ -962,10 +1047,8 @@ class IPv6ExtHdrSegmentRoutingTLVEgressNode(IPv6ExtHdrSegmentRoutingTLV): class IPv6ExtHdrSegmentRoutingTLVPad1(IPv6ExtHdrSegmentRoutingTLV): name = "IPv6 Option Header Segment Routing - Pad1 TLV" - # RFC8754 sect 2.1.1.1 - fields_desc = [ByteEnumField("type", 0, _segment_routing_header_tlvs), - FieldLenField("len", None, length_of="padding", fmt="B"), - StrLenField("padding", b"\x00", length_from=lambda pkt: pkt.len)] # noqa: E501 + # RFC8754 sect 2.1.1.1, Pad1 is a single byte + fields_desc = [ByteEnumField("type", 0, _segment_routing_header_tlvs)] class IPv6ExtHdrSegmentRoutingTLVPadN(IPv6ExtHdrSegmentRoutingTLV): @@ -1103,13 +1186,17 @@ def defragment6(packets): # 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].copy() @@ -1385,7 +1472,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): @@ -1403,7 +1493,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 @@ -1691,7 +1785,9 @@ def extract_padding(self, s): 24: "ICMPv6NDOptRouteInfo", 25: "ICMPv6NDOptRDNSS", 26: "ICMPv6NDOptEFA", - 31: "ICMPv6NDOptDNSSL" + 31: "ICMPv6NDOptDNSSL", + 37: "ICMPv6NDOptCaptivePortal", + 38: "ICMPv6NDOptPREF64", } icmp6ndraprefs = {0: "Medium (default)", @@ -1705,18 +1801,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 @@ -1726,7 +1845,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%") @@ -1761,44 +1880,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 @@ -1969,7 +2066,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) @@ -1996,6 +2093,11 @@ def __init__(self, name, default, length_from=None, padded=False): # noqa: E501 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 = [] @@ -2046,7 +2148,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. @@ -2506,7 +2643,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: @@ -2524,7 +2661,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 @@ -3323,9 +3460,9 @@ 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 li in list(k): # use list(): k is modified in the loop @@ -4071,6 +4208,7 @@ 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) diff --git a/scapy/layers/ipsec.py b/scapy/layers/ipsec.py index 162d5aa971a..0815cabdbbc 100644 --- a/scapy/layers/ipsec.py +++ b/scapy/layers/ipsec.py @@ -30,7 +30,6 @@ True """ -from __future__ import absolute_import try: from math import gcd except ImportError: @@ -44,11 +43,26 @@ 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.libs.six as six from scapy.layers.inet6 import IPv6, IPv6ExtHdrHopByHop, IPv6ExtHdrDestOpt, \ IPv6ExtHdrRouting @@ -78,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 @@ -111,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}, @@ -125,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) ############################################################################### @@ -166,6 +208,13 @@ def data_for_encryption(self): algorithms, modes, ) + try: + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms + ) + except ImportError: + decrepit_algorithms = algorithms else: log_loading.info("Can't import python-cryptography v1.7+. " "Disabled IPsec encryption/authentication.") @@ -370,7 +419,11 @@ def encrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): cipher = self.cipher(key, tag_length=icv_size) else: cipher = self.cipher(key) - data = cipher.encrypt(mode_iv, data, aad) + 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: cipher = self.new_cipher(key, mode_iv) encryptor = cipher.encryptor() @@ -422,7 +475,11 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): else: cipher = self.cipher(key) try: - data = cipher.decrypt(mode_iv, data + icv, aad) + 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: @@ -442,8 +499,8 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): 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, @@ -485,6 +542,17 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): iv_size=8, icv_size=16, format_mode_iv=_salt_format_mode_iv) + # 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, @@ -504,34 +572,35 @@ def decrypt(self, sa, esp, key, icv_size=None, esn_en=False, esn=0): icv_size=16, format_mode_iv=_salt_format_mode_iv) # noqa: E501 - # XXX: RFC7321 states that DES *MUST NOT* be implemented. - # XXX: Keep for backward compatibility? # 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) CRYPT_ALGOS['DES'] = CryptAlgo('DES', - cipher=algorithms.TripleDES, + cipher=decrepit_algorithms.TripleDES, mode=modes.CBC, key_size=(8,)) CRYPT_ALGOS['3DES'] = CryptAlgo('3DES', - cipher=algorithms.TripleDES, + cipher=decrepit_algorithms.TripleDES, mode=modes.CBC) - try: + 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=algorithms.CAST5, + cipher=decrepit_algorithms.CAST5, mode=modes.CBC) - # XXX: Flagged as weak by 'cryptography'. - # Kept for backward compatibility CRYPT_ALGOS['Blowfish'] = CryptAlgo('Blowfish', - cipher=algorithms.Blowfish, + cipher=decrepit_algorithms.Blowfish, mode=modes.CBC) - except AttributeError: - # Future-proof, if ever removed from cryptography - pass + 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) ############################################################################### @@ -613,16 +682,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 @@ -651,7 +721,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: @@ -660,12 +733,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 @@ -881,10 +953,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 @@ -972,7 +1044,10 @@ def _encrypt_esp(self, pkt, seq_num=None, iv=None, esn_en=None, esn=None): 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() @@ -985,17 +1060,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): @@ -1084,7 +1158,9 @@ 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 @@ -1115,8 +1191,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/isakmp.py b/scapy/layers/isakmp.py index 8726f3ee08a..b909e66a2ce 100644 --- a/scapy/layers/isakmp.py +++ b/scapy/layers/isakmp.py @@ -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 index 69aa768a014..c8a3320d6e7 100644 --- a/scapy/layers/kerberos.py +++ b/scapy/layers/kerberos.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy # See https://scapy.net/ for more information -# Copyright (C) Gabriel Potter +# Copyright (C) Gabriel Potter r""" Kerberos V5 @@ -11,104 +11,187 @@ - Kerberos Network Authentication Service (V5): RFC4120 - Kerberos Version 5 GSS-API: RFC1964, RFC4121 - Kerberos Pre-Authentication: RFC6113 (FAST) - -You will find more complete documentation for this layer over -`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) +- 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 -from datetime import datetime, timedelta +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_GENERAL_STRING, ASN1_GENERALIZED_TIME, ASN1_INTEGER, - ASN1_SEQUENCE, ASN1_STRING, - ASN1_Class_UNIVERSAL, ASN1_Codecs, ) -from scapy.asn1.ber import BERcodec_SEQUENCE 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, ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_STRING_ENCAPS, + ASN1F_STRING_PacketField, ASN1F_STRING, - ASN1F_enum_INTEGER, - ASN1F_optional, ) 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 ( - ByteField, + ConditionalField, + FieldLenField, FlagsField, - LEIntField, + 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_BLOB, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_BINDINGS, + GSS_S_BAD_MECH, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_DEFECTIVE_TOKEN, + GSS_S_FAILURE, + GSS_S_FLAGS, + GssChannelBindings, + SSP, + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, ) -from scapy.packet import Packet, bind_bottom_up, bind_layers -from scapy.supersocket import StreamSocket -from scapy.volatile import GeneralizedTime, RandNum - from scapy.layers.inet import TCP, UDP +from scapy.layers.smb import _NV_VERSION +from scapy.layers.smb2 import STATUS_ERREF +from scapy.layers.tls.cert import Cert, PrivKey +from scapy.layers.x509 import ( + _CMS_ENCAPSULATED, + CMS_ContentInfo, + CMS_IssuerAndSerialNumber, + DHPublicKey, + X509_AlgorithmIdentifier, + X509_DirectoryName, + X509_SubjectPublicKeyInfo, +) -# kerberos APPLICATION - - -class ASN1_Class_KRB(ASN1_Class_UNIVERSAL): - name = "KERBEROS" - APPLICATION = 0x60 - +# Redirect exports from RFC3961 +try: + from scapy.libs.rfc3961 import * # noqa: F401,F403 +except ImportError: + pass -class ASN1_GSSAPI_APPLICATION(ASN1_SEQUENCE): - tag = ASN1_Class_KRB.APPLICATION +# Typing imports +from typing import ( + Optional, +) -class BERcodec_GSSAPI_APPLICATION(BERcodec_SEQUENCE): - tag = ASN1_Class_KRB.APPLICATION +# kerberos APPLICATION -class ASN1F_KRB_APPLICATION(ASN1F_SEQUENCE): - ASN1_tag = ASN1_Class_KRB.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 @@ -119,6 +202,18 @@ class ASN1F_KRB_APPLICATION(ASN1F_SEQUENCE): 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 @@ -126,199 +221,118 @@ class PrincipalName(ASN1_Packet): ASN1F_enum_INTEGER( "nameType", 0, - { - 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", - }, + _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) -KerberosTime = ASN1F_GENERALIZED_TIME -Microseconds = ASN1F_INTEGER + @staticmethod + def fromUPN(upn: str): + """ + Create a PrincipalName from a UPN string. + """ + 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 + ) -class HostAddress(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER( - "addrType", - 0, - { - # RFC4120 sect 7.5.3 - 2: "IPv4", - 3: "Directional", - 5: "ChaosNet", - 6: "XNS", - 7: "ISO", - 12: "DECNET Phase IV", - 16: "AppleTalk DDP", - 20: "NetBios", - 24: "IPv6", - }, - explicit_tag=0xA0, - ), - ASN1F_STRING("address", "", explicit_tag=0xA1), - ) +KerberosTime = ASN1F_GENERALIZED_TIME +Microseconds = ASN1F_INTEGER -HostAddresses = lambda name, **kwargs: ASN1F_SEQUENCE_OF( - name, [], HostAddress, **kwargs -) -Checksum = lambda **kwargs: ASN1F_SEQUENCE( - ASN1F_enum_INTEGER( - "checksumtype", - 0, - { - # RFC3961 sect 8 - 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", - }, - explicit_tag=0xA0, - ), - ASN1F_STRING("checksum", "", explicit_tag=0xA1), - **kwargs -) +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-1 -_AUTHORIZATIONDATA_VALUES = { - # Filled below +_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", } - -class _ASN1FString_PacketField(ASN1F_STRING): - holds_packets = 1 - - def i2m(self, pkt, val): - if isinstance(val, ASN1_Packet): - val = ASN1_STRING(bytes(val)) - return super(_ASN1FString_PacketField, self).i2m(pkt, val) - - def any2i(self, pkt, x): - if hasattr(x, "add_underlayer"): - x.add_underlayer(pkt) - return super(_ASN1FString_PacketField, self).any2i(pkt, x) - - -class _AuthorizationData_value_Field(_ASN1FString_PacketField): - def m2i(self, pkt, s): - val = super(_AuthorizationData_value_Field, self).m2i(pkt, s) - if pkt.adType.val in _PADATA_CLASSES: - cls = _AUTHORIZATIONDATA_VALUES.get(pkt.adType.val, None) - if not val[0].val: - return val - if cls: - return cls(val[0].val, _underlayer=pkt), val[1] - return val - - -class AuthorizationDataItem(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_enum_INTEGER( - "adType", - 0, - { - # 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", - }, - 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 - ) - - -AD_IF_RELEVANT = AuthorizationData -_AUTHORIZATIONDATA_VALUES[1] = AD_IF_RELEVANT - - -class AD_KDCIssued(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - 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 - - -# back to RFC4120 - - -_KRB_E_TYPES = { - 0x1: "DES", - 0x10: "3DES", - 0x11: "AES-128", - 0x12: "AES-256", - 0x17: "RC4", +# 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", 0x1, _KRB_E_TYPES, explicit_tag=0xA0), - ASN1F_optional(UInt32("kvno", 0, explicit_tag=0xA1)), + 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), ) @@ -333,18 +347,41 @@ def get_usage(self): 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_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" ) @@ -363,7 +400,28 @@ def decrypt(self, key, key_usage_number=None, cls=None): key_usage_number, cls = self.get_usage() d = key.decrypt(key_usage_number, self.cipher.val) if cls: - return cls(d) + 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): @@ -378,8 +436,10 @@ def encrypt(self, key, text, confounder=None, key_usage_number=None): """ if key_usage_number is None: key_usage_number = self.get_usage()[0] - self.etype = key.eptype - self.cipher = key.encrypt(key_usage_number, text, confounder=confounder) + self.etype = key.etype + self.cipher = ASN1_STRING( + key.encrypt(key_usage_number, text, confounder=confounder) + ) class EncryptionKey(ASN1_Packet): @@ -392,118 +452,370 @@ class EncryptionKey(ASN1_Packet): def toKey(self): from scapy.libs.rfc3961 import Key - return Key(self.keytype.val, key=self.keyvalue.val) - - -KerberosFlags = ASN1F_FLAGS - - -_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", - 19: "PA-ETYPE-INFO2", - 20: "PA-SVR-REFERRAL-INFO", - 128: "PA-PAC-REQUEST", - 133: "PA-FX-COOKIE", - 134: "PA-AUTHENTICATION-SET", - 135: "PA-AUTH-SET-SELECTED", - 136: "PA-FX-FAST", - 137: "PA-FX-ERROR", - 165: "PA-SUPPORTED-ENCTYPES", - 167: "PA-PAC-OPTIONS", -} - -_PADATA_CLASSES = { - # Filled elsewhere in this file -} - - -# RFC4120 + return Key( + etype=self.keytype.val, + key=self.keyvalue.val, + ) + @classmethod + def fromKey(self, key): + return EncryptionKey( + keytype=key.etype, + keyvalue=key.key, + ) -class _PADATA_value_Field(_ASN1FString_PacketField): - """ - A special field that properly dispatches PA-DATA values according to - padata-type and if the paquet is a request or a response. - """ +class _Checksum_Field(ASN1F_STRING_PacketField): 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): - is_reply = ( - pkt.underlayer.underlayer is not None and - isinstance(pkt.underlayer.underlayer, KRB_ERROR) - ) or isinstance(pkt.underlayer, (KRB_AS_REP, KRB_TGS_REP)) - cls = cls[is_reply] - if not val[0].val: - return val - return cls(val[0].val, _underlayer=pkt), val[1] + 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 PADATA(ASN1_Packet): +class Checksum(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, + 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" + ) -# RFC 4120 sect 5.2.7.2 + def verify(self, key, text, key_usage_number=None): + """ + Decrypt and return the data contained in cipher. + :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) -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)), - ) + def make(self, key, text, key_usage_number=None, cksumtype=None): + """ + Encrypt text and set it into cipher. + :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, + ) + ) -_PADATA_CLASSES[2] = EncryptedData +KerberosFlags = ASN1F_FLAGS -# RFC 4120 sect 5.2.7.4 +_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 ETYPE_INFO_ENTRY(ASN1_Packet): +class HostAddress(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), + ASN1F_enum_INTEGER( + "addrType", + 0, + _ADDR_TYPES, + explicit_tag=0xA0, ), + ASN1F_STRING("address", "", 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) +HostAddresses = lambda name, **kwargs: ASN1F_SEQUENCE_OF( + name, [], HostAddress, **kwargs +) -_PADATA_CLASSES[11] = ETYPE_INFO +_AUTHORIZATIONDATA_VALUES = { + # Filled below +} -# RFC 4120 sect 5.2.7.5 +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 -class ETYPE_INFO_ENTRY2(ASN1_Packet): + +_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("etype", 0x1, _KRB_E_TYPES, explicit_tag=0xA0), - ASN1F_optional( - KerberosString("salt", "", explicit_tag=0xA1), + 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", + 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), @@ -518,6 +830,18 @@ class ETYPE_INFO2(ASN1_Packet): _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 @@ -543,6 +867,7 @@ class PA_AUTHENTICATION_SET(ASN1_Packet): _PADATA_CLASSES[134] = PA_AUTHENTICATION_SET + # [MS-KILE] sect 2.2.3 @@ -556,6 +881,113 @@ class PA_PAC_REQUEST(ASN1_Packet): _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 = [ + LEIntEnumField( + "apOptions", + 0x4000, + { + 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 @@ -571,9 +1003,9 @@ class PA_SUPPORTED_ENCTYPES(Packet): "RC4-HMAC", "AES128-CTS-HMAC-SHA1-96", "AES256-CTS-HMAC-SHA1-96", - ] + - ["bit_%d" % i for i in range(11)] + - [ + ] + + ["bit_%d" % i for i in range(11)] + + [ "FAST-supported", "Compount-identity-supported", "Claims-supported", @@ -593,11 +1025,12 @@ class PA_PAC_OPTIONS(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( KerberosFlags( "options", - 0, + "", [ "Claims", "Branch-Aware", "Forward-to-Full-DC", + "Resource-based-constrained-delegation", # [MS-SFU] 2.2.5 ], explicit_tag=0xA0, ) @@ -606,50 +1039,119 @@ class PA_PAC_OPTIONS(ASN1_Packet): _PADATA_CLASSES[167] = PA_PAC_OPTIONS +# [MS-KILE] sect 2.2.11 -# RFC6113 sect 5.4.1 +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), + ) -class _KrbFastArmor_value_Field(_ASN1FString_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 +_PADATA_CLASSES[161] = KERB_KEY_LIST_REQ -class KrbFastArmor(ASN1_Packet): +# [MS-KILE] sect 2.2.12 + + +class KERB_KEY_LIST_REP(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), + ASN1_root = ASN1F_SEQUENCE_OF( + "keys", + [], + ASN1F_PACKET("", None, EncryptionKey), ) -# RFC6113 sect 5.4.2 +_PADATA_CLASSES[162] = KERB_KEY_LIST_REP +# [MS-KILE] sect 2.2.13 -class KrbFastArmoredReq(ASN1_Packet): + +class KERB_SUPERSEDED_BY_USER(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE( - ASN1F_optional( - ASN1F_PACKET("armor", KrbFastArmor(), KrbFastArmor, explicit_tag=0xA0) - ), - Checksum(explicit_tag=0xA1), - ASN1F_PACKET("encFastReq", None, EncryptedData, explicit_tag=0xA2), - ) + ASN1F_PACKET("name", None, PrincipalName, explicit_tag=0xA0), + Realm("realm", None, explicit_tag=0xA1), ) -class PA_FX_FAST_REQUEST(ASN1_Packet): - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_CHOICE( - "armoredData", +_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), ) @@ -679,11 +1181,11 @@ class PA_FX_FAST_REPLY(ASN1_Packet): class KrbFastFinished(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - Microseconds("timestamp", 0, explicit_tag=0xA0), - KerberosTime("usec", GeneralizedTime(), explicit_tag=0xA1), + 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), - Checksum(explicit_tag=0xA4), + ASN1F_PACKET("ticketChecksum", Checksum(), Checksum, explicit_tag=0xA4), ) @@ -692,7 +1194,7 @@ class KrbFastResponse(ASN1_Packet): ASN1_root = ASN1F_SEQUENCE( ASN1F_SEQUENCE_OF("padata", [PADATA()], PADATA, explicit_tag=0xA0), ASN1F_optional( - ASN1F_PACKET("stengthenKey", None, EncryptionKey, explicit_tag=0xA1) + ASN1F_PACKET("strengthenKey", None, EncryptionKey, explicit_tag=0xA1) ), ASN1F_optional( ASN1F_PACKET( @@ -705,7 +1207,8 @@ class KrbFastResponse(ASN1_Packet): _PADATA_CLASSES[136] = (PA_FX_FAST_REQUEST, PA_FX_FAST_REPLY) -# RFC 4556 + +# RFC 4556 - PKINIT # sect 3.2.1 @@ -715,13 +1218,20 @@ class ExternalPrincipalIdentifier(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( ASN1F_optional( - ASN1F_STRING("subjectName", "", implicit_tag=0xA0), + ASN1F_STRING_ENCAPS( + "subjectName", None, X509_DirectoryName, implicit_tag=0x80 + ), ), ASN1F_optional( - ASN1F_STRING("issuerAndSerialNumber", "", implicit_tag=0xA1), + ASN1F_STRING_ENCAPS( + "issuerAndSerialNumber", + None, + CMS_IssuerAndSerialNumber, + implicit_tag=0x81, + ), ), ASN1F_optional( - ASN1F_STRING("subjectKeyIdentifier", "", implicit_tag=0xA2), + ASN1F_STRING("subjectKeyIdentifier", "", implicit_tag=0x82), ), ) @@ -729,7 +1239,12 @@ class ExternalPrincipalIdentifier(ASN1_Packet): class PA_PK_AS_REQ(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING("signedAuthpack", "", implicit_tag=0xA0), + ASN1F_STRING_ENCAPS( + "signedAuthpack", + CMS_ContentInfo(), + CMS_ContentInfo, + implicit_tag=0x80, + ), ASN1F_optional( ASN1F_SEQUENCE_OF( "trustedCertifiers", @@ -746,16 +1261,115 @@ class PA_PK_AS_REQ(ASN1_Packet): _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, + ), + ) + + +# still RFC 4556 sect 3.2.1 + + +class 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", "", explicit_tag=0xA4), + ), + # [MS-PKCA] sect 2.2.3 + ASN1F_optional( + ASN1F_PACKET("paChecksum2", None, PAChecksum2, explicit_tag=0xA5), + ), + ) + + +# 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 AuthPack(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET( + "pkAuthenticator", + PKAuthenticator(), + PKAuthenticator, + explicit_tag=0xA0, + ), + ASN1F_optional( + ASN1F_PACKET( + "clientPublicValue", + X509_SubjectPublicKeyInfo(), + X509_SubjectPublicKeyInfo, + explicit_tag=0xA1, + ), + ), + ASN1F_optional( + ASN1F_SEQUENCE_OF( + "supportedCMSTypes", + [], + X509_AlgorithmIdentifier, + explicit_tag=0xA2, + ), + ), + ASN1F_optional( + ASN1F_STRING("clientDCNonce", 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"] = AuthPack + # sect 3.2.3 class DHRepInfo(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( - ASN1F_STRING("dhSignedData", "", implicit_tag=0xA0), + 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), + ), ) @@ -776,6 +1390,81 @@ class PA_PK_AS_REP(ASN1_Packet): _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 @@ -788,12 +1477,15 @@ class PA_PK_AS_REP(ASN1_Packet): 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", + 27: "EncAPRepPart", 28: "EncKrbPrivPart", 29: "EnvKrbCredPart", 30: "KRB-ERROR", @@ -804,16 +1496,22 @@ class PA_PK_AS_REP(ASN1_Packet): class KRB_Ticket(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( ASN1F_SEQUENCE( - ASN1F_INTEGER("tktVno", 0, explicit_tag=0xA0), + ASN1F_INTEGER("tktVno", 5, explicit_tag=0xA0), Realm("realm", "", explicit_tag=0xA1), - ASN1F_PACKET("sname", None, PrincipalName, explicit_tag=0xA2), - ASN1F_PACKET("encPart", None, EncryptedData, explicit_tag=0xA3), + ASN1F_PACKET("sname", PrincipalName(), PrincipalName, explicit_tag=0xA2), + ASN1F_PACKET("encPart", EncryptedData(), EncryptedData, explicit_tag=0xA3), ), - implicit_tag=1, + 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 @@ -838,22 +1536,25 @@ class TransitedEncoding(ASN1_Packet): "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_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( ASN1F_SEQUENCE( KerberosFlags( "flags", - 0, + "", _TICKET_FLAGS, explicit_tag=0xA0, ), - ASN1F_PACKET("key", None, EncryptionKey, explicit_tag=0xA1), + ASN1F_PACKET("key", EncryptionKey(), EncryptionKey, explicit_tag=0xA1), Realm("crealm", "", explicit_tag=0xA2), - ASN1F_PACKET("cname", None, PrincipalName, explicit_tag=0xA3), + ASN1F_PACKET("cname", PrincipalName(), PrincipalName, explicit_tag=0xA3), ASN1F_PACKET( "transited", TransitedEncoding(), TransitedEncoding, explicit_tag=0xA4 ), @@ -874,7 +1575,7 @@ class EncTicketPart(ASN1_Packet): ), ), ), - implicit_tag=3, + implicit_tag=ASN1_Class_KRB.EncTicketPart, ) @@ -902,12 +1603,12 @@ class KRB_KDC_REQ_BODY(ASN1_Packet): "opt-hardware-auth", "unused12", "unused13", - "constrained-delegation", - "canonicalize", - "request-anonymous", - ] + - ["unused%d" % i for i in range(17, 26)] + - [ + "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", @@ -959,9 +1660,9 @@ class KrbFastReq(ASN1_Packet): [ "RESERVED", "hide-client-names", - ] + - ["res%d" % i for i in range(2, 16)] + - ["kdc-follow-referrals"], + ] + + ["res%d" % i for i in range(2, 16)] + + ["kdc-follow-referrals"], explicit_tag=0xA0, ), ASN1F_SEQUENCE_OF("padata", [PADATA()], PADATA, explicit_tag=0xA1), @@ -971,18 +1672,19 @@ class KrbFastReq(ASN1_Packet): class KRB_AS_REQ(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( KRB_KDC_REQ, - implicit_tag=10, + implicit_tag=ASN1_Class_KRB.AS_REQ, ) class KRB_TGS_REQ(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( KRB_KDC_REQ, - implicit_tag=12, + implicit_tag=ASN1_Class_KRB.TGS_REQ, ) + msgType = ASN1_INTEGER(12) # sect 5.4.2 @@ -1002,19 +1704,25 @@ class KRB_TGS_REQ(ASN1_Packet): class KRB_AS_REP(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( KRB_KDC_REP, - implicit_tag=11, + implicit_tag=ASN1_Class_KRB.AS_REP, ) class KRB_TGS_REP(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( KRB_KDC_REP, - implicit_tag=13, + 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 @@ -1033,7 +1741,7 @@ class LastReqItem(ASN1_Packet): ), KerberosFlags( "flags", - 0, + "", _TICKET_FLAGS, explicit_tag=0xA4, ), @@ -1051,25 +1759,25 @@ class LastReqItem(ASN1_Packet): HostAddresses("caddr", explicit_tag=0xAB), ), # RFC6806 sect 11 - # ASN1F_optional( - ASN1F_SEQUENCE_OF("encryptedPaData", [], PADATA, explicit_tag=0xAC), - # ), + ASN1F_optional( + ASN1F_SEQUENCE_OF("encryptedPaData", [], PADATA, explicit_tag=0xAC), + ), ) class EncASRepPart(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( EncKDCRepPart, - implicit_tag=25, + implicit_tag=ASN1_Class_KRB.EncASRepPart, ) class EncTGSRepPart(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( EncKDCRepPart, - implicit_tag=26, + implicit_tag=ASN1_Class_KRB.EncTGSRepPart, ) @@ -1078,7 +1786,7 @@ class EncTGSRepPart(ASN1_Packet): class KRB_AP_REQ(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( ASN1F_SEQUENCE( ASN1F_INTEGER("pvno", 5, explicit_tag=0xA0), ASN1F_enum_INTEGER("msgType", 14, KRB_MSG_TYPES, explicit_tag=0xA1), @@ -1095,7 +1803,7 @@ class KRB_AP_REQ(ASN1_Packet): ASN1F_PACKET("ticket", None, KRB_Ticket, explicit_tag=0xA3), ASN1F_PACKET("authenticator", None, EncryptedData, explicit_tag=0xA4), ), - implicit_tag=14, + implicit_tag=ASN1_Class_KRB.AP_REQ, ) @@ -1104,12 +1812,14 @@ class KRB_AP_REQ(ASN1_Packet): class KRB_Authenticator(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + 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(Checksum(explicit_tag=0x3)), + ASN1F_optional( + ASN1F_PACKET("cksum", None, Checksum, explicit_tag=0xA3), + ), Microseconds("cusec", 0, explicit_tag=0xA4), KerberosTime("ctime", GeneralizedTime(), explicit_tag=0xA5), ASN1F_optional( @@ -1124,7 +1834,7 @@ class KRB_Authenticator(ASN1_Packet): ), ), ), - implicit_tag=2, + implicit_tag=ASN1_Class_KRB.Authenticator, ) @@ -1133,81 +1843,248 @@ class KRB_Authenticator(ASN1_Packet): class KRB_AP_REP(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + 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=15, + implicit_tag=ASN1_Class_KRB.AP_REP, ) -# sect 5.9.1 - - -class MethodData(ASN1_Packet): +class EncAPRepPart(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE_OF("seq", [PADATA()], PADATA) + 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, + ) -class _KRBERROR_data_Field(_ASN1FString_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 [24, 25]: - # 24: KDC_ERR_PREAUTH_FAILED - # 25: KDC_ERR_PREAUTH_REQUIRED - return MethodData(val[0].val, _underlayer=pkt), val[1] - return val +# sect 5.7 -class KRB_ERROR(ASN1_Packet): +class KRB_PRIV(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + 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_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("ctime", GeneralizedTime(), explicit_tag=0xA2), + KerberosTime("timestamp", None, explicit_tag=0xA1), ), ASN1F_optional( - Microseconds("cusec", 0, explicit_tag=0xA3), + Microseconds("usec", None, explicit_tag=0xA2), ), - 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", + 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]: + # 14: KDC_ERR_ETYPE_NOSUPP + # 24: KDC_ERR_PREAUTH_FAILED + # 25: KDC_ERR_PREAUTH_REQUIRED + # 36: KRB_AP_ERR_BADMATCH + return MethodData(val[0].val, _underlayer=pkt), val[1] + elif pkt.errorCode.val in [6, 7, 12, 13, 18, 29, 41, 60]: + # 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 + # 41: KRB_AP_ERR_MODIFIED + # 60: KRB_ERR_GENERIC + 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", @@ -1250,35 +2127,228 @@ class KRB_ERROR(ASN1_Packet): 74: "KDC_ERR_REVOCATION_STATUS_UNAVAILABLE", 75: "KDC_ERR_CLIENT_NAME_MISMATCH", 76: "KDC_ERR_KDC_NAME_MISMATCH", + # 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", "", explicit_tag=0xA7)), + 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", None, PrincipalName, explicit_tag=0xAA), + 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=30, + 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 -class KRB_InitialContextToken(ASN1_Packet): - name = "Kerberos v5 InitialContextToken (RFC1964)" - # It's funny how useless this wrapping is +# RFC 4121 - sect 4.1 + + +class KRB_GSSAPI_Token(GSSAPI_BLOB): + name = "Kerberos GSSAPI-Token" ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_KRB_APPLICATION( + ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("MechType", "1.2.840.113554.1.2.2"), ASN1F_PACKET( - "innerContextToken", - KRB5_InitialContextToken_innerContextToken(), - KRB5_InitialContextToken_innerContextToken, + "innerToken", + KRB_InnerToken(), + KRB_InnerToken, implicit_tag=0x0, ), - implicit_tag=0, + implicit_tag=ASN1_Class_KRB.Token, ) # RFC 1964 - sect 1.2.1 -class KRB5_GSS_GetMIC_RFC1964(Packet): - name = "Kerberos v5 GSS_GetMIC (RFC1964)" +class KRB_GSS_MIC_RFC1964(Packet): + name = "Kerberos v5 MIC Token (RFC1964)" fields_desc = [ - StrFixedLenEnumField("TOK_ID", b"\x01\x01", _TOK_IDS, length=2), LEShortEnumField("SGN_ALG", 0, _SGN_ALGS), - LEIntField("reserved", 0xFFFFFFFF), + XLEIntField("Filler", 0xFFFFFFFF), XStrFixedLenField("SND_SEQ", b"", length=8), PadField( # sect 1.2.2.3 XStrFixedLenField("SGN_CKSUM", b"", length=8), @@ -1341,17 +2435,21 @@ class KRB5_GSS_GetMIC_RFC1964(Packet): ), ] + 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 KRB5_GSS_Wrap_RFC1964(Packet): +class KRB_GSS_Wrap_RFC1964(Packet): name = "Kerberos v5 GSS_Wrap (RFC1964)" fields_desc = [ - StrFixedLenEnumField("TOK_ID", b"\x02\x01", _TOK_IDS, length=2), LEShortEnumField("SGN_ALG", 0, _SGN_ALGS), LEShortEnumField("SEAL_ALG", 0, _SEAL_ALGS), - LEShortField("reserved", 0xFFFF), + XLEShortField("Filler", 0xFFFF), XStrFixedLenField("SND_SEQ", b"", length=8), PadField( # sect 1.2.2.3 XStrFixedLenField("SGN_CKSUM", b"", length=8), @@ -1362,14 +2460,22 @@ class KRB5_GSS_Wrap_RFC1964(Packet): 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 KRB5_GSS_Delete_sec_context_RFC1964(Packet): +class KRB_GSS_Delete_sec_context_RFC1964(Packet): name = "Kerberos v5 GSS_Delete_sec_context (RFC1964)" - TOK_ID = b"\x01\x02" - fields_desc = KRB5_GSS_GetMIC_RFC1964.fields_desc + fields_desc = KRB_GSS_MIC_RFC1964.fields_desc + + +_InitialContextTokens[b"\x01\x02"] = KRB_GSS_Delete_sec_context_RFC1964 # RFC 4121 - sect 4.2.2 @@ -1383,61 +2489,76 @@ class KRB5_GSS_Delete_sec_context_RFC1964(Packet): # RFC 4121 - sect 4.2.6.1 -class KRB5_GSS_GetMIC(Packet): - name = "Kerberos v5 GSS_GetMIC" +class KRB_GSS_MIC(Packet): + name = "Kerberos v5 MIC Token" fields_desc = [ - StrFixedLenEnumField("TOK_ID", b"\x04\x04", _TOK_IDS, length=2), - FlagsField("Flags", 8, 0, _KRB5_GSS_Flags), - LEIntField("reserved", 0xFFFFFFFF), - XStrFixedLenField("SND_SEQ", b"", length=8), - PadField( - XStrFixedLenField("SGN_CKSUM", b"", length=8), - align=8, - padwith=b"\x04", - ), + 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 KRB5_GSS_Wrap(Packet): - name = "Kerberos v5 GSS_Wrap" +class KRB_GSS_Wrap(Packet): + name = "Kerberos v5 Wrap Token" fields_desc = [ - StrFixedLenEnumField("TOK_ID", b"\x05\x04", _TOK_IDS, length=2), - FlagsField("Flags", 8, 0, _KRB5_GSS_Flags), - ByteField("reserved", 0xFF), + FlagsField("Flags", 0, 8, _KRB5_GSS_Flags), + XByteField("Filler", 0xFF), ShortField("EC", 0), # Big endian ShortField("RRC", 0), # Big endian - XStrFixedLenField("SND_SEQ", b"", length=8), - PadField( - XStrFixedLenField("SGN_CKSUM", b"", length=8), - align=8, - padwith=b"\x04", + 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 -# Main classes +_InitialContextTokens[b"\x05\x04"] = KRB_GSS_Wrap -class KRB5_GSS(Packet): - @classmethod - def dispatch_hook(cls, _pkt=None, *args, **kargs): - if _pkt and len(_pkt) >= 2: - if _pkt[:2] == b"\x01\x01": - return KRB5_GSS_GetMIC_RFC1964 - elif _pkt[:2] == b"\x02\x01": - return KRB5_GSS_Wrap_RFC1964 - elif _pkt[:2] == b"\x01\x02": - return KRB5_GSS_Delete_sec_context_RFC1964 - elif _pkt[:2] in [b"\x01\x00", "\x02\x00", "\x03\x00"]: - return KRB5_InitialContextToken_innerContextToken - elif _pkt[:2] == b"\x04\x04": - return KRB5_GSS_GetMIC - elif _pkt[:2] == b"\x05\x04": - return KRB5_GSS_Wrap - return KRB5_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 @@ -1450,7 +2571,8 @@ class Kerberos(ASN1_Packet): ASN1_root = ASN1F_CHOICE( "root", None, - KRB_InitialContextToken, # [APPLICATION 0] + # RFC4120 + KRB_GSSAPI_Token, # [APPLICATION 0] KRB_Ticket, # [APPLICATION 1] KRB_Authenticator, # [APPLICATION 2] KRB_AS_REQ, # [APPLICATION 10] @@ -1459,6 +2581,7 @@ class Kerberos(ASN1_Packet): KRB_TGS_REP, # [APPLICATION 13] KRB_AP_REQ, # [APPLICATION 14] KRB_AP_REP, # [APPLICATION 15] + # RFC4120 KRB_ERROR, # [APPLICATION 30] ) @@ -1470,16 +2593,20 @@ def mysummary(self): bind_bottom_up(UDP, Kerberos, dport=88) bind_layers(UDP, Kerberos, sport=88, dport=88) -bind_layers(KRB5_InitialContextToken_innerContextToken, Kerberos) -bind_layers(KRB5_GSS_GetMIC_RFC1964, Kerberos) -bind_layers(KRB5_GSS_Wrap_RFC1964, Kerberos) -bind_layers(KRB5_GSS_Wrap_RFC1964, Kerberos) +_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 @@ -1497,190 +2624,1311 @@ def tcp_reassemble(cls, data, *args, **kwargs): 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 KerberosClient(Automaton): - RES = namedtuple("AS_Result", ["asrep", "sessionkey"]) + """ + 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 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 + + 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 p12: (optional) use a pfx/p12 instead of x509 and x509key. In this case, + 'password' is the password of the p12. + + 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"]) + RES_TGS_MODE = namedtuple("TGS_Result", ["tgsrep", "sessionkey", "kdcrep"]) + + class MODE(IntEnum): + AS_REQ = 0 + TGS_REQ = 1 + GET_SALT = 2 def __init__( - self, ip=None, host=None, user=None, domain=None, key=None, port=88, **kwargs + self, + mode=MODE.AS_REQ, + ip=None, + upn=None, + password=None, + key=None, + realm=None, + x509=None, + x509key=None, + p12=None, + spn=None, + ticket=None, + host=None, + renew=False, + additional_tickets=[], + u2u=False, + for_user=None, + s4u2proxy=False, + dmsa=False, + kdc_proxy=None, + kdc_proxy_no_check_certificate=False, + fast=False, + armor_ticket=None, + armor_ticket_upn=None, + armor_ticket_skey=None, + key_list_req=[], + etypes=None, + port=88, + timeout=5, + **kwargs, ): import scapy.libs.rfc3961 # Trigger error if any # noqa: F401 + from scapy.layers.ldap import dclocator + + if not upn: + raise ValueError("Invalid upn") + if not spn: + raise ValueError("Invalid spn") + 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") + + # PKINIT checks + if p12 is not None: + from cryptography.hazmat.primitives.serialization import pkcs12 + + # password should be None or bytes + if isinstance(password, str): + password = password.encode() + + # Read p12/pfx + with open(p12, "rb") as fd: + x509key, x509, _ = pkcs12.load_key_and_certificates( + fd.read(), + password=password, + ) + x509 = Cert(cryptography_obj=x509) + x509key = PrivKey(cryptography_obj=x509key) + elif x509 and x509key: + x509 = Cert(x509) + x509key = PrivKey(x509key) + + if mode in [self.MODE.AS_REQ, self.MODE.GET_SALT]: + if not host: + raise ValueError("Invalid host") + if (x509 is None) ^ (x509key is None): + raise ValueError("Must provide both 'x509' and 'x509key' !") + 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}" + ) - if not ip: - raise ValueError("Invalid IP") - if not host: - raise ValueError("Invalid host") - if not user: - raise ValueError("Invalid user") - if not domain: - raise ValueError("Invalid domain") + if mode == self.MODE.GET_SALT: + if etypes is not None: + raise ValueError("Cannot specify etypes in GET_SALT mode !") - self.result = None # Result + from scapy.libs.rfc3961 import EncryptionType + + etypes = [ + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA1_96, + ] + elif etypes is None: + from scapy.libs.rfc3961 import EncryptionType + + etypes = [ + EncryptionType.AES256_CTS_HMAC_SHA1_96, + EncryptionType.AES128_CTS_HMAC_SHA1_96, + EncryptionType.RC4_HMAC, + EncryptionType.RC4_HMAC_EXP, + EncryptionType.DES_CBC_MD5, + ] + self.etypes = etypes - sock = socket.socket() - sock.settimeout(5.0) - sock.connect((ip, port)) - sock = StreamSocket(sock, KerberosTCPHeader) + self.mode = mode - self.host = bytes_encode(host).upper() - self.user = bytes_encode(user) - self.domain = bytes_encode(domain).upper() + self.result = None # Result + + self._timeout = timeout + 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.realm = realm.upper() + self.x509 = x509 + self.x509key = x509key + 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 self.pre_auth = False + 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 + + sock = self._connect() super(KerberosClient, self).__init__( - recvsock=lambda **_: sock, ll=lambda **_: sock, **kwargs + 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 ap_req(self): - from scapy.libs.rfc3961 import EncryptionType + 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 + from scapy.libs.rfc3961 import Key, KRB_FX_CF2 + + 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.cname = PrincipalName.fromUPN(self.upn) + 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 + pafactor = PADATA( + padataType=16, padataValue=PA_PK_AS_REQ() # PA-PK-AS-REQ + ) + raise NotImplementedError("PKINIT isn't implemented yet !") + else: + # Key-based factor + + if self.fast: + # Special FAST factor + # RFC6113 sect 5.4.6 + from scapy.libs.rfc3961 import KRB_FX_CF2 + + # Calculate the 'challenge key' + ts_key = KRB_FX_CF2( + self.fast_armorkey, + self.key, + b"clientchallengearmor", + b"challengelongterm", + ) + pafactor = PADATA( + padataType=138, # PA-ENCRYPTED-CHALLENGE + padataValue=EncryptedData(), + ) + else: + # Usual 'timestamp' factor + ts_key = self.key + pafactor = PADATA( + padataType=2, # PA-ENC-TIMESTAMP + 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, + ) - now_time = datetime.utcnow().replace(microsecond=0) + # 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, + ) + ] - apreq = Kerberos( + # 3. Build the request + asreq = Kerberos( root=KRB_AS_REQ( - padata=[ + 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: + from scapy.libs.rfc3961 import ChecksumType, EncryptionType + + # [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 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 on 192.168.122.17, we ask a TGT for user1 - >>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", password="Password1") + >>> # 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, key=hex_bytes("6d0748c546f4e99205 - ...: e78f8da7681d4ec5520ae4815543720c2a647c1ae814c9")) - >>> krb_as_req("user1@DOMAIN.LOCAL", "192.168.122.17", key=key) + >>> 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:: + + >>> krb_as_req("user1@DOMAIN.LOCAL", p12="./store.p12", password="password") """ - m = re.match(r"^([^@\\]+)(@|\\)([^@\\]+)$", upn) - if not m: - raise ValueError("Invalid UPN !") - if m.group(2) == "@": - user = m.group(1) - domain = m.group(3) - else: - user = m.group(3) - domain = m.group(1) - if key is None: + if realm is None: + _, realm = _parse_upn(upn) + if key is None and p12 is None and x509 is None: if password is None: try: from prompt_toolkit import prompt @@ -1688,23 +3936,1357 @@ def krb_as_req(upn, ip, key=None, password=None, **kwargs): password = prompt("Enter password: ", is_password=True) except ImportError: password = input("Enter password: ") - if user.endswith("$"): - # Machine account - salt = ( - domain.upper().encode() + - b"host" + - user.lower().encode() + - b"." + - domain.lower().encode() - ) - else: - salt = domain.upper().encode() + user.encode() - from scapy.libs.rfc3961 import Key, EncryptionType + cli = KerberosClient( + mode=KerberosClient.MODE.AS_REQ, + realm=realm, + ip=ip, + spn=spn or "krbtgt/" + realm, + 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 - key = Key.string_to_key(EncryptionType.AES256, password.encode(), salt) + +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=upn, + 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( - domain=domain, ip=ip, host="WIN1", user=user, key=key, **kwargs + 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, negResult = ssp.GSS_Init_sec_context( + None, + req_flags=0, # No GSS_C_MUTUAL_FLAG + ) + if negResult != 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, negResult = ssp.GSS_Init_sec_context(Context, resp.aprep) + if negResult != 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. + """ + + oid = "1.2.840.113554.1.2.2" + 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 + "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.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, + ): + self.ST = ST + self.UPN = UPN + self.KEY = KEY + self.SPN = SPN + self.TGT = TGT + self.PASSWORD = PASSWORD + self.U2U = U2U + self.DC_IP = DC_IP + self.debug = debug + if SKEY_TYPE is None: + from scapy.libs.rfc3961 import EncryptionType + + SKEY_TYPE = EncryptionType.AES128_CTS_HMAC_SHA1_96 + self.SKEY_TYPE = SKEY_TYPE + super(KerberosSSP, self).__init__(**kwargs) + + 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=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 + # [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 + from scapy.libs.rfc3961 import ( + Cipher, + Hmac_MD5, + _rfc1964pad, + decrepit_algorithms, + ) + + # 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 + from scapy.libs.rfc3961 import ( + Cipher, + Hmac_MD5, + _rfc1964pad, + decrepit_algorithms, + ) + + # 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, + 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) + + from scapy.libs.rfc3961 import Key + + 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 = token.root.innerToken.root + except AttributeError: + try: + # Kerberos + tgt_rep = 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 token !") + additional_tickets = [tgt_rep.ticket] + if self.TGT is not None: + if not self.KEY: + raise ValueError("Cannot use TGT without the KEY") + # Use TGT + res = krb_tgs_req( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + sessionkey=self.KEY, + ticket=self.TGT, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + ) + else: + # Ask for TGT then ST + res = krb_as_and_tgs( + upn=self.UPN, + spn=self.SPN or target_name, + ip=self.DC_IP, + key=self.KEY, + password=self.PASSWORD, + additional_tickets=additional_tickets, + u2u=self.U2U, + debug=self.debug, + ) + if not res: + # Failed to retrieve the ticket + return Context, None, GSS_S_FAILURE + self.ST, self.KEY = res.tgsrep.ticket, res.sessionkey + elif not self.KEY: + raise ValueError("Must provide KEY with ST") + Context.STSessionKey = self.KEY + + # Save ServerHostname + if len(self.ST.sname.nameString) == 2: + Context.ServerHostname = self.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=self.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 and encrypt the full KRB_Authenticator + ap_req.authenticator.encrypt( + Context.STSessionKey, + KRB_Authenticator( + crealm=crealm, + cname=PrincipalName.fromUPN(self.UPN), + # RFC 4121 checksum + cksum=Checksum( + cksumtype="KRB-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), + ), + ), + 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)), # noqa: E501 + ) + ), + ), + # 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(token, KRB_AP_REP): + # Raw AP_REP was passed + ap_rep = token + else: + try: + # GSSAPI / Kerberos + ap_rep = token.root.innerToken.root + except AttributeError: + try: + # Kerberos + ap_rep = token.innerToken.root + except AttributeError: + try: + # Raw kerberos DCE-STYLE + ap_rep = 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, + 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) + + from scapy.libs.rfc3961 import Key + 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.KEY = res.asrep.ticket, res.sessionkey + + # Server receives AP-req, sends AP-rep + if isinstance(token, KRB_AP_REQ): + # Raw AP_REQ was passed + ap_req = token + else: + try: + # GSSAPI/Kerberos + ap_req = token.root.innerToken.root + except AttributeError: + try: + # Kerberos + ap_req = token.innerToken.root + except AttributeError: + try: + # Raw kerberos + ap_req = 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_TOKEN + + # 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 token: + return Context, None, GSS_S_DEFECTIVE_TOKEN + # Server receives AP-req, sends AP-rep + if isinstance(token, KRB_AP_REP): + # Raw AP_REP was passed + ap_rep = token + else: + try: + # GSSAPI/Kerberos + ap_rep = token.root.innerToken.root + except AttributeError: + try: + # Raw Kerberos + ap_rep = 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, + 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, 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 + return Context, status + elif Context.state == self.STATE.CLI_SENT_APREQ: + Context, _, status = self.GSS_Init_sec_context( + Context, token, req_flags=req_flags + ) + if status == GSS_S_COMPLETE: + Context.state = self.STATE.SRV_SENT_APREP + else: + Context.state == self.STATE.FAILED + return Context, status + + # Unknown state. Don't crash though. + return Context, GSS_S_FAILURE + + 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 + + def canMechListMIC(self, Context: CONTEXT): + return bool(Context.KrbSessionKey) diff --git a/scapy/layers/l2.py b/scapy/layers/l2.py index ed96e978c1f..05bb6ba0c2b 100644 --- a/scapy/layers/l2.py +++ b/scapy/layers/l2.py @@ -7,24 +7,25 @@ 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_LINUX_SLL2, \ - DLT_LOOP, DLT_NULL, ETHER_ANY, ETHER_BROADCAST, ETHER_TYPES, ETH_P_ARP, \ - ETH_P_MACSEC -from scapy.error import warning, ScapyNoDstMacException, log_runtime + 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, @@ -47,12 +48,13 @@ SourceIPField, StrFixedLenField, StrLenField, + ThreeBytesField, XByteField, XIntField, XShortEnumField, XShortField, ) -from scapy.libs.six import viewitems +from scapy.interfaces import _GlobInterfaceType, resolve_iface from scapy.packet import bind_layers, Packet from scapy.plist import ( PacketList, @@ -60,13 +62,28 @@ SndRcvList, _PacketList, ) -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.compat import ( +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, @@ -75,6 +92,8 @@ cast, ) from scapy.interfaces import NetworkInterface + + if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 @@ -98,7 +117,7 @@ def register_l3(self, l2, l3, resolve_method): self.resolvers[l2, l3] = resolve_method def resolve(self, l2inst, l3inst): - # type: (Ether, Packet) -> Optional[str] + # type: (Packet, Packet) -> Optional[str] k = l2inst.__class__, l3inst.__class__ if k in self.resolvers: return self.resolvers[k](l2inst, l3inst) @@ -118,19 +137,40 @@ def __repr__(self): @conf.commands.register def getmacbyip(ip, chainCC=0): # type: (str, int) -> Optional[str] - """Return MAC address corresponding to a given IP address""" + """ + 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) + + # 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 + # Check the cache mac = _arp_cache.get(ip) if mac: return mac @@ -161,7 +201,13 @@ def __init__(self, name): MACField.__init__(self, name, None) def i2h(self, pkt, x): - # type: (Optional[Ether], Optional[str]) -> str + # 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) @@ -172,12 +218,10 @@ 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 super(DestMACField, self).i2h(pkt, x) - - def i2m(self, pkt, x): - # type: (Optional[Ether], Optional[str]) -> bytes - 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): @@ -192,20 +236,15 @@ 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 super(SourceMACField, self).i2h(pkt, x) def i2m(self, pkt, x): - # type: (Optional[Ether], Optional[Any]) -> bytes - 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 @@ -234,7 +273,8 @@ def i2m(self, pkt, x): 21: "ATM", } -ETHER_TYPES[0x88a8] = '802_AD' +ETHER_TYPES[0x88a8] = '802_1AD' +ETHER_TYPES[0x88e7] = '802_1AH' ETHER_TYPES[ETH_P_MACSEC] = '802_1AE' @@ -281,7 +321,7 @@ def extract_padding(self, s): return s[:tmp_len], s[tmp_len:] def answers(self, other): - # type: (Ether) -> int + # type: (Packet) -> int if isinstance(other, Dot3): return self.payload.answers(other.payload) return 0 @@ -306,8 +346,10 @@ class LLC(Packet): ByteField("ctrl", 0)] -def l2_register_l3(l2, l3): - # type: (Packet, Packet) -> Optional[str] +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) @@ -368,9 +410,12 @@ 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 @@ -455,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 @@ -524,12 +569,15 @@ def route(self): 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_inner, IP6Field): - return conf.route6.route(dst) + return conf.route6.route(dst, dev=scope) elif isinstance(fld_inner, IPField): - return conf.route.route(dst) + return conf.route.route(dst, dev=scope) else: return None, None, None @@ -546,14 +594,28 @@ def mysummary(self): return self.sprintf("ARP %op% %psrc% > %pdst%") -def l2_register_l3_arp(l2, l3): - # type: (Type[Packet], Type[Packet]) -> Optional[str] - # TODO: support IPv6? - if l3.plen == 4: +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( - "Unable to guess L2 MAC address from an ARP packet with a " - "non-IPv4 pdst. Provide it manually !" + "You should be providing the Ethernet destination mac when sending this " + "kind of ARP packets." ) return None @@ -664,40 +726,82 @@ 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(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, GRErouting, {"routing_present": 1}) @@ -707,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) @@ -719,8 +824,8 @@ class Dot1AD(Dot1Q): 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) @@ -729,23 +834,189 @@ class Dot1AD(Dot1Q): @conf.commands.register -def arpcachepoison(target, victim, interval=60): - # type: (str, str, int) -> None - """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, # type: Optional[Union[_PacketList[QueryAnswer], List[QueryAnswer]]] # noqa: E501 @@ -759,35 +1030,71 @@ def show(self, *args, **kwargs): """ 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): - # type: (str, int, int, Optional[int], **Any) -> Tuple[ARPingResult, PacketList] # noqa: E501 - """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 + + 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") / ARP(pdst=net), + 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, - iface_hint=net, - **kargs + threaded=threaded, + iface_hint=hint, + **kargs, ) ans = ARPingResult(ans.res) @@ -818,7 +1125,7 @@ def promiscping(net, timeout=2, fake_bcast="ff:ff:ff:ff:ff:fe", **kargs): 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 @@ -866,7 +1173,7 @@ def parse_options(self, IP_addr=None, ARP_addr=None, from_ip=None): self.ARP_addr = ARP_addr def is_request(self, req): - # type: (Ether) -> bool + # type: (Packet) -> bool if not req.haslayer(ARP): return False arp = req[ARP] @@ -931,7 +1238,7 @@ def arpleak(target, plen=255, hwlen=255, **kargs): """ # We want explicit packets - pkts_iface = {} # type: Dict[str, List[Ether]] + 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 @@ -953,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/ldap.py b/scapy/layers/ldap.py index f9f297d1065..c09e07e64c1 100644 --- a/scapy/layers/ldap.py +++ b/scapy/layers/ldap.py @@ -8,29 +8,106 @@ 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 `_ """ -from scapy.automaton import Automaton, ATMT -from scapy.asn1.asn1 import ASN1_STRING, ASN1_Class_UNIVERSAL, ASN1_Codecs -from scapy.asn1.ber import BERcodec_SEQUENCE +import collections +import re +import socket +import ssl +import string +import struct +import uuid + +from enum import Enum + +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, ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, ASN1F_SET_OF, + ASN1F_STRING_PacketField, ASN1F_STRING, - ASN1F_optional, ) from scapy.asn1packet import ASN1_Packet -from scapy.packet import bind_bottom_up, bind_layers +from scapy.config import conf +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.inet import TCP, UDP -from scapy.layers.ntlm import NTLM_Client +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 ( + ChannelBindingType, + GSSAPI_BLOB, + GSSAPI_BLOB_SIGNATURE, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_COMPLETE, + GssChannelBindings, + SSP, + _GSSAPI_Field, +) +from scapy.layers.netbios import NBTDatagram +from scapy.layers.smb import ( + NETLOGON, + NETLOGON_SAM_LOGON_RESPONSE_EX, +) +from scapy.layers.smb2 import STATUS_ERREF + +# Typing imports +from typing import ( + Any, + Dict, + List, + Union, +) # Elements of protocol # https://datatracker.ietf.org/doc/html/rfc1777#section-4 @@ -48,7 +125,7 @@ class AttributeValueAssertion(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( AttributeType("attributeType", "organizationName"), - AttributeValue("attributeValue", "") + AttributeValue("attributeValue", ""), ) @@ -57,70 +134,109 @@ class LDAPReferral(ASN1_Packet): ASN1_root = LDAPString("uri", "") -LDAPResult = ASN1F_SEQUENCE( - ASN1F_ENUMERATED("resultCode", 0, { - 0: "success", - 1: "operationsError", - 2: "protocolError", - 3: "timeLimitExceeded", - 4: "sizeLimitExceeded", - 5: "compareFalse", - 6: "compareTrue", - 7: "authMethodNotSupported", - 8: "strongAuthRequired", - 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", - }), +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) - ) + ASN1F_optional(ASN1F_SEQUENCE_OF("referral", [], LDAPReferral, implicit_tag=0xA3)), ) -# Bind operation -# https://datatracker.ietf.org/doc/html/rfc1777#section-4.1 +# 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 -class ASN1_Class_LDAP_Authentication(ASN1_Class_UNIVERSAL): - name = "LDAP Authentication" - simple = 0xa0 - krbv42LDAP = 0xa1 - krbv42DSA = 0xa2 - sasl = 0xa3 + +# Bind operation +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.2 -class ASN1_LDAP_Authentication_simple(ASN1_STRING): +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_SEQUENCE): +class BERcodec_LDAP_Authentication_simple(BERcodec_STRING): tag = ASN1_Class_LDAP_Authentication.simple @@ -128,11 +244,12 @@ class ASN1F_LDAP_Authentication_simple(ASN1F_STRING): ASN1_tag = ASN1_Class_LDAP_Authentication.simple -class ASN1_LDAP_Authentication_krbv42LDAP(ASN1_STRING): +# krbv42LDAP +class LDAP_Authentication_krbv42LDAP(ASN1_STRING): tag = ASN1_Class_LDAP_Authentication.krbv42LDAP -class BERcodec_LDAP_Authentication_krbv42LDAP(BERcodec_SEQUENCE): +class BERcodec_LDAP_Authentication_krbv42LDAP(BERcodec_STRING): tag = ASN1_Class_LDAP_Authentication.krbv42LDAP @@ -140,11 +257,12 @@ class ASN1F_LDAP_Authentication_krbv42LDAP(ASN1F_STRING): ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42LDAP -class ASN1_LDAP_Authentication_krbv42DSA(ASN1_STRING): +# krbv42DSA +class LDAP_Authentication_krbv42DSA(ASN1_STRING): tag = ASN1_Class_LDAP_Authentication.krbv42DSA -class BERcodec_LDAP_Authentication_krbv42DSA(BERcodec_SEQUENCE): +class BERcodec_LDAP_Authentication_krbv42DSA(BERcodec_STRING): tag = ASN1_Class_LDAP_Authentication.krbv42DSA @@ -152,85 +270,190 @@ class ASN1F_LDAP_Authentication_krbv42DSA(ASN1F_STRING): ASN1_tag = ASN1_Class_LDAP_Authentication.krbv42DSA -class LDAP_SaslCredentials(ASN1_Packet): +# 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_STRING("credentials", "") + 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", 2), + ASN1F_INTEGER("version", 3), LDAPDN("bind_name", ""), - ASN1F_CHOICE("authentication", None, - ASN1F_LDAP_Authentication_simple, - ASN1F_LDAP_Authentication_krbv42LDAP, - ASN1F_LDAP_Authentication_krbv42DSA, - ASN1F_PACKET( - "sasl", - LDAP_SaslCredentials(), - LDAP_SaslCredentials, - implicit_tag=0xa3), - ) + 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.seq + ( - ASN1F_optional( - ASN1F_STRING("serverSaslCreds", "", - implicit_tag=0x87) - ),))) + *( + 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/rfc1777#section-4.2 +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.3 class LDAP_UnbindRequest(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_NULL("info", 0) + ASN1_root = ASN1F_SEQUENCE( + ASN1F_NULL("info", 0), + implicit_tag=ASN1_Class_LDAP.UnbindRequest, + ) # Search operation -# https://datatracker.ietf.org/doc/html/rfc1777#section-4.3 +# https://datatracker.ietf.org/doc/html/rfc4511#section-4.5 class LDAP_SubstringFilterInitial(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = LDAPString("initial", "") + ASN1_root = LDAPString("val", "") class LDAP_SubstringFilterAny(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = LDAPString("any", "") + ASN1_root = LDAPString("val", "") class LDAP_SubstringFilterFinal(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = LDAPString("final", "") + 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=0x0), - ASN1F_PACKET("any", - LDAP_SubstringFilterAny(), - LDAP_SubstringFilterAny, - implicit_tag=0x1), - ASN1F_PACKET("final", - LDAP_SubstringFilterFinal(), - LDAP_SubstringFilterFinal, - implicit_tag=0x2), + "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, + ), ) @@ -238,7 +461,7 @@ class LDAP_SubstringFilter(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( AttributeType("type", ""), - ASN1F_SEQUENCE_OF("filters", [], LDAP_SubstringFilterStr) + ASN1F_SEQUENCE_OF("filters", [], LDAP_SubstringFilterStr), ) @@ -247,53 +470,272 @@ class LDAP_SubstringFilter(ASN1_Packet): class LDAP_FilterAnd(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("and_", [], _LDAP_Filter) + ASN1_root = ASN1F_SET_OF("vals", [], _LDAP_Filter) class LDAP_FilterOr(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SET_OF("or_", [], _LDAP_Filter) + 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", "") + 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=0x80), - ASN1F_PACKET("or_", None, LDAP_FilterOr, - implicit_tag=0x81), - ASN1F_PACKET("not_", None, - _LDAP_Filter, - implicit_tag=0x82), - ASN1F_PACKET("equalityMatch", - AttributeValueAssertion(), - AttributeValueAssertion, - implicit_tag=0x83), - ASN1F_PACKET("substrings", - LDAP_SubstringFilter(), - LDAP_SubstringFilter, - implicit_tag=0x84), - ASN1F_PACKET("greaterOrEqual", - AttributeValueAssertion(), - AttributeValueAssertion, - implicit_tag=0x85), - ASN1F_PACKET("lessOrEqual", - AttributeValueAssertion(), - AttributeValueAssertion, - implicit_tag=0x86), - ASN1F_PACKET("present", LDAP_FilterPresent(), - LDAP_FilterPresent, - implicit_tag=0x87), - ASN1F_PACKET("approxMatch", None, AttributeValueAssertion, - implicit_tag=0x88), + "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 @@ -304,34 +746,38 @@ 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_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) + ASN1F_PACKET("filter", LDAP_Filter(), LDAP_Filter), + ASN1F_SEQUENCE_OF("attributes", [], LDAP_SearchRequestAttribute), + implicit_tag=ASN1_Class_LDAP.SearchRequest, ) -class LDAP_SearchResponseEntryAttributeValue(ASN1_Packet): +class LDAP_AttributeValue(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = AttributeValue("value", "") -class LDAP_SearchResponseEntryAttribute(ASN1_Packet): +class LDAP_PartialAttribute(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( AttributeType("type", ""), - ASN1F_SET_OF("values", [], - LDAP_SearchResponseEntryAttributeValue) + ASN1F_SET_OF("values", [], LDAP_AttributeValue), ) @@ -339,24 +785,206 @@ class LDAP_SearchResponseEntry(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER ASN1_root = ASN1F_SEQUENCE( LDAPDN("objectName", ""), - ASN1F_SEQUENCE_OF("attributes", - LDAP_SearchResponseEntryAttribute(), - LDAP_SearchResponseEntryAttribute) + 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_SearchResponseResultCode(ASN1_Packet): +class LDAP_ModifyDNResponse(ASN1_Packet): ASN1_codec = ASN1_Codecs.BER - ASN1_root = LDAPResult + 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_INTEGER("messageID", 0) + 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 @@ -365,58 +993,146 @@ class LDAP_Control(ASN1_Packet): ASN1F_optional( ASN1F_BOOLEAN("criticality", False), ), - ASN1F_optional( - ASN1F_STRING("controlValue", "") - ), + 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 +_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(), - ASN1F_PACKET("bindRequest", - LDAP_BindRequest(), - LDAP_BindRequest, - implicit_tag=0x60), - ASN1F_PACKET("bindResponse", - LDAP_BindResponse(), - LDAP_BindResponse, - implicit_tag=0x61), - ASN1F_PACKET("unbindRequest", - LDAP_UnbindRequest(), - LDAP_UnbindRequest, - implicit_tag=0x42), - ASN1F_PACKET("searchRequest", - LDAP_SearchRequest(), - LDAP_SearchRequest, - implicit_tag=0x63), - ASN1F_PACKET("searchResponse", - LDAP_SearchResponseEntry(), - LDAP_SearchResponseEntry, - implicit_tag=0x64), - ASN1F_PACKET("searchResponse", - LDAP_SearchResponseResultCode(), - LDAP_SearchResponseResultCode, - implicit_tag=0x65), - ASN1F_PACKET("abandonRequest", - LDAP_AbandonRequest(), - LDAP_AbandonRequest, - implicit_tag=0x70) - ), + 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", [], LDAP_Control, - implicit_tag=0x0) - ) + 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): - return (self.protocolOp.__class__.__name__.replace("_", " "), [LDAP]) + 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) @@ -437,9 +1153,12 @@ class CLDAP(ASN1_Packet): ASN1F_optional( LDAPDN("user", ""), ), - LDAP.ASN1_root.seq[1] # protocolOp + LDAP.ASN1_root.seq[1], # protocolOp ) + def answers(self, other): + return isinstance(other, CLDAP) and other.messageID == self.messageID + bind_layers(CLDAP, CLDAP) @@ -447,87 +1166,1297 @@ class CLDAP(ASN1_Packet): 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 + -# NTLM Automata +_located_dc = collections.namedtuple("LocatedDC", ["ip", "samlogon"]) +_dclocatorcache = conf.netcache.new_cache("dclocator", 600) -class NTLM_LDAP_Client(NTLM_Client, Automaton): - port = 389 - cls = LDAP +@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): - self.messageID = 1 - self.authenticated = False - super(NTLM_LDAP_Client, self).__init__(*args, **kwargs) - - @ATMT.state(initial=1) - def BEGIN(self): - self.wait_server() - - @ATMT.condition(BEGIN) - def begin(self): - raise self.WAIT_FOR_TOKEN() - - @ATMT.state() - def WAIT_FOR_TOKEN(self): - pass - - @ATMT.condition(WAIT_FOR_TOKEN) - def should_send_bind(self): - ntlm_tuple = self.get_token() - raise self.SENT_BIND().action_parameters(ntlm_tuple) - - @ATMT.action(should_send_bind) - def send_bind(self, ntlm_tuple): - ntlm_token, _, _ = ntlm_tuple + resp = kwargs.pop("resp", None) + if resp: + self.resultCode = resp.protocolOp.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 + + if port is None: + if self.ssl: + port = 636 + else: + port = 389 + sock = socket.socket() + self.timeout = timeout + self.host = host + sock.settimeout(timeout) + 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: + self.sock = SSLStreamSocket(sock, LDAP) + # Compute the channel binding token (CBT) + self.chan_bindings = GssChannelBindings.fromssl( + ChannelBindingType.TLS_SERVER_END_POINT, + sslsock=sock, + ) + 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=LDAP_BindRequest( - version=2, - authentication=LDAP_SaslCredentials( - mechanism="GSS-SPNEGO", - credentials=ntlm_token + 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, ) ) + # Send / Receive + resp = self.sock.sr1( + pkt, + verbose=0, + **kwargs, ) - self.send(pkt) - self.messageID += 1 - - @ATMT.state() - def SENT_BIND(self): - pass - - @ATMT.receive_condition(SENT_BIND) - def receive_bind_response(self, pkt): - if isinstance(pkt.protocolOp, LDAP_BindResponse): - if pkt.protocolOp.resultCode == 0x31: # Invalid credentials - ntlm_tuple = (None, None, None) - elif pkt.protocolOp.resultCode == 0x0: # Auth success - ntlm_tuple = (None, 0, None) - self.authenticated = True - elif pkt.protocolOp.resultCode == 0x35: # UnwillingToPerform - print("Error:") - pkt.show() - raise self.ERRORED() - else: - ntlm_tuple = self._get_token( - pkt.protocolOp.serverSaslCreds.val + # 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, + ) ) - self.received_ntlm_token(ntlm_tuple) - if self.authenticated: - raise self.AUTHENTICATED() else: - raise self.WAIT_FOR_TOKEN() + resp = None + 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=False, + encrypt=False, + 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. + """ + # 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 - @ATMT.state(final=1) - def ERRORED(self): - pass + 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 - @ATMT.state(final=1) - def AUTHENTICATED(self): - pass + if not isinstance(self.ssp, SPNEGOSSP): + 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 !") + 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, + 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, + ) + 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): + if self.verb: + print("%s bind failed !" % self.mech.name) + resp.show() + return + val = resp.protocolOp.serverSaslCredsData + if not val: + status = resp.protocolOp.resultCode + break + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + GSSAPI_BLOB(val), + target_name="ldap/" + self.host, + chan_bindings=self.chan_bindings, + ) + else: + status = GSS_S_COMPLETE + if status != GSS_S_COMPLETE: + raise LDAP_Exception( + "%s bind failed !" % self.mech.name, + resp=resp, + ) + 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, + ) + # 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=200, # paging to 200 per 200 + 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, + ) -class NTLM_LDAPS_Client(NTLM_LDAP_Client): - port = 636 - ssl = True + def close(self): + if self.verb: + print("X Connection closed\n") + self.sock.close() + self.bound = False diff --git a/scapy/layers/llmnr.py b/scapy/layers/llmnr.py index 01e211939ca..1f3282879d5 100644 --- a/scapy/layers/llmnr.py +++ b/scapy/layers/llmnr.py @@ -14,15 +14,22 @@ 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, + DNSCompressedPacket, DNS_am, + DNS, + DNSQR, + DNSRR, ) @@ -30,38 +37,44 @@ _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", None), - DNSRRField("an", "ancount", None), - DNSRRField("ns", "nscount", None), - DNSRRField("ar", "arcount", None, 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): - if self.an: - return "LLMNRResponse '%s' is at '%s'" % ( - self.an.rrname.decode(), - self.an.rdata, - ), [UDP] - if self.qd: - return "LLMNRQuery who has '%s'" % ( - self.qd.qname.decode(), - ), [UDP] + 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): @@ -90,9 +103,24 @@ 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): - function_name = "llmnr_spoof" + """ + 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 diff --git a/scapy/layers/lltd.py b/scapy/layers/lltd.py index 8a374f9aabb..e37aeef4872 100644 --- a/scapy/layers/lltd.py +++ b/scapy/layers/lltd.py @@ -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.libs.six as six from scapy.compat import orb, chb @@ -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/ms_nrtp.py b/scapy/layers/ms_nrtp.py new file mode 100644 index 00000000000..34c672f87be --- /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, + type=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 pre-selected type. + return functools.partial( + NRBFAdditionalInfo, + type=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__ = ["type"] + + fields_desc = [ + NRBFValueWithCode.fields_desc[1], + ] + + def __init__(self, _pkt=None, **kwargs): + self.type = kwargs.pop("type", PrimitiveTypeEnum.Byte) + assert isinstance(self.type, PrimitiveTypeEnum) + super(NRBFMemberPrimitiveUnTyped, self).__init__(_pkt, **kwargs) + + def clone_with(self, *args, **kwargs): + pkt = super(NRBFMemberPrimitiveUnTyped, self).clone_with(*args, **kwargs) + pkt.type = self.type + return pkt + + def copy(self): + pkt = super(NRBFMemberPrimitiveUnTyped, self).copy() + pkt.type = self.type + return pkt + + @property + def PrimitiveType(self): + return self.type + + 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, + type=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_pack=True, + ) + ), + ] + + +# [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.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, + 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 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, + 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 + + 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")) + + # [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, _ = _ParseStringArray(remoteReply.valueof("pdsaOxidBindings")) + entry.bindingInfo = self._ChoseRPCBinding(binds) + + 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. + continue + + # Success + return host, port + raise ValueError("No valid bindings available !") + + 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")) + + 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, _ = _ParseStringArray(resp.valueof("ppdsaOxidBindings")) + entry.bindingInfo = self._ChoseRPCBinding(binds) + + # 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, + 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 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, + 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) + + # 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="> 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 _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=modes.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=modes.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=modes.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=modes.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, + 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, + 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( + ... domainname="DOMAIN", computername="WIN10", + ... 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.ndr64 = False # Netlogon doesn't work with NDR64 + self.SessionKey = None + self.ClientStoredCredential = None + self.supportAES = supportAES + super(NetlogonClient, self).__init__( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=auth_level, + ndr64=self.ndr64, + verb=verb, + **kwargs, + ) + + def connect_and_bind(self, remoteIP): + """ + This calls DCERPC_Client's connect_and_bind to bind the 'logon' interface. + """ + super(NetlogonClient, self).connect_and_bind(remoteIP, self.interface) + + def alter_context(self): + return super(NetlogonClient, self).alter_context(self.interface) + + def create_authenticator(self): + """ + Create a NETLOGON_AUTHENTICATOR + """ + # [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, + ) + + def validate_authenticator(self, auth): + """ + Validate a NETLOGON_AUTHENTICATOR + + :param auth: the NETLOGON_AUTHENTICATOR object + """ + # [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 !") + + def establish_secure_channel( + self, + computername: str, + domainname: str, + HashNt: bytes, + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, + secureChannelType=NETLOGON_SECURE_CHANNEL_TYPE.WorkstationSecureChannel, + ): + """ + Function to establish the Netlogon Secure Channel. + + This uses NetrServerAuthenticate3 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 computername: the netbios computer account name that is used to establish + the secure channel. (e.g. WIN10) + :param domainname: the netbios domain name to connect to (e.g. DOMAIN) + :param HashNt: the HashNT of the computer account. + """ + # Flow documented in 3.1.4 Session-Key Negotiation + # and sect 3.4.5.2 for specific calls + clientChall = os.urandom(8) + + # Step 1: NetrServerReqChallenge + 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 + + # 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) + # Step 2: 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 + ) + netr_server_auth3_response = self.sr1_req( + NetrServerAuthenticate3_Request( + PrimaryName=None, + AccountName=computername + "$", + SecureChannelType=secureChannelType, + ComputerName=computername, + ClientCredential=PNETLOGON_CREDENTIAL( + data=self.ClientStoredCredential, + ), + NegotiateFlags=int(NegotiateFlags), + ndr64=self.ndr64, + ndrendian=self.ndrendian, + ) + ) + if ( + NetrServerAuthenticate3_Response not in netr_server_auth3_response + or 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) + ) + ) + + # Show the error + print( + conf.color_theme.fail( + "! %s" + % STATUS_ERREF.get(netr_server_auth3_response.status, "Failure") + ) + ) + + # If error is unknown, show the packet entirely + if netr_server_auth3_response.status not in STATUS_ERREF: + netr_server_auth3_response.show() + + raise ValueError + # 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, + ) + elif mode == NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos: + NegotiateFlags += "Kerberos" + # TODO + raise NotImplementedError + + # Finally alter context (to use the SSP) + self.alter_context() diff --git a/scapy/layers/mspac.py b/scapy/layers/msrpce/mspac.py similarity index 61% rename from scapy/layers/mspac.py rename to scapy/layers/msrpce/mspac.py index 07bf93c50d2..6185673c804 100644 --- a/scapy/layers/mspac.py +++ b/scapy/layers/msrpce/mspac.py @@ -1,12 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-only # This file is part of Scapy # See https://scapy.net/ for more information -# Copyright (C) Gabriel Potter +# 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 @@ -19,50 +20,55 @@ FieldListField, FlagsField, LEIntEnumField, - LELongField, LEIntField, + LELongField, LEShortField, MultipleTypeField, - PacketLenField, + PacketField, PacketListField, StrField, StrFieldUtf16, StrFixedLenField, StrLenFieldUtf16, UTCTimeField, + UUIDField, XStrField, XStrLenField, ) from scapy.packet import Packet -from scapy.layers.kerberos import _AUTHORIZATIONDATA_VALUES +from scapy.layers.kerberos import ( + _AUTHORIZATIONDATA_VALUES, + _KRB_S_TYPES, +) from scapy.layers.dcerpc import ( - _NDRConfField, NDRByteField, + NDRConfFieldListField, + NDRConfPacketListField, NDRConfStrLenField, - NDRConfVarStrLenField, NDRConfVarStrLenFieldUtf16, - NDRConfPacketListField, - NDRConfFieldListField, NDRConfVarStrNullFieldUtf16, NDRConformantString, - NDRFullPointerField, + NDRFieldListField, + NDRFullEmbPointerField, NDRInt3264EnumField, NDRIntField, NDRLongField, NDRPacket, NDRPacketField, NDRSerialization1Header, + NDRSerializeType1PacketLenField, NDRShortField, NDRSignedLongField, NDRUnionField, + _NDRConfField, ndr_deserialize1, ndr_serialize1, ) from scapy.layers.ntlm import ( _NTLMPayloadField, _NTLMPayloadPacket, - _NTLM_post_build, ) +from scapy.layers.smb2 import WINNT_SID # sect 2.4 @@ -75,17 +81,18 @@ class PAC_INFO_BUFFER(Packet): { 0x00000001: "Logon information", 0x00000002: "Credentials information", - 0x00000006: "Server checksum", - 0x00000007: "KDC checksum", + 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 checksum", + 0x00000010: "Ticket Signature", 0x00000011: "PAC Attributes", 0x00000012: "PAC Requestor", + 0x00000013: "Extended KDC Signature", }, ), LEIntField("cbBufferSize", None), @@ -98,19 +105,24 @@ def default_payload_class(self, payload): _PACTYPES = {} -# sect 2.5 - NDR PACKETS AUTO-GENERATED + +# sect 2.5 - NDR PACKETS class RPC_UNICODE_STRING(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRShortField("Length", 0), - NDRShortField("MaximumLength", 0), - NDRFullPointerField( + 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", "", length_from=lambda pkt: (pkt.Length // 2) + "Buffer", + "", + size_is=lambda pkt: (pkt.MaximumLength // 2), + length_is=lambda pkt: (pkt.Length // 2), ), - deferred=True, ), ] @@ -120,7 +132,7 @@ class FILETIME(NDRPacket): fields_desc = [NDRIntField("dwLowDateTime", 0), NDRIntField("dwHighDateTime", 0)] -class PGROUP_MEMBERSHIP(NDRPacket): +class GROUP_MEMBERSHIP(NDRPacket): ALIGNMENT = (4, 4) fields_desc = [NDRIntField("RelativeId", 0), NDRIntField("Attributes", 0)] @@ -137,12 +149,12 @@ class RPC_SID_IDENTIFIER_AUTHORITY(NDRPacket): fields_desc = [StrFixedLenField("Value", "", length=6)] -class PSID(NDRPacket): +class SID(NDRPacket): ALIGNMENT = (4, 8) - CONFORMANT_COUNT = 1 + DEPORTED_CONFORMANTS = ["SubAuthority"] fields_desc = [ NDRByteField("Revision", 0), - NDRByteField("SubAuthorityCount", 0), + NDRByteField("SubAuthorityCount", None, size_of="SubAuthority"), NDRPacketField( "IdentifierAuthority", RPC_SID_IDENTIFIER_AUTHORITY(), @@ -152,16 +164,19 @@ class PSID(NDRPacket): "SubAuthority", [], NDRIntField("", 0), - count_from=lambda pkt: pkt.SubAuthorityCount, + size_is=lambda pkt: pkt.SubAuthorityCount, conformant_in_struct=True, ), ] + def summary(self): + return WINNT_SID.summary(self) -class PKERB_SID_AND_ATTRIBUTES(NDRPacket): + +class KERB_SID_AND_ATTRIBUTES(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullPointerField(NDRPacketField("Sid", PSID(), PSID), deferred=True), + NDRFullEmbPointerField(NDRPacketField("Sid", SID(), SID)), NDRIntField("Attributes", 0), ] @@ -185,59 +200,48 @@ class KERB_VALIDATION_INFO(NDRPacket): NDRShortField("BadPasswordCount", 0), NDRIntField("UserId", 0), NDRIntField("PrimaryGroupId", 0), - NDRIntField("GroupCount", 0), - NDRFullPointerField( + NDRIntField("GroupCount", None, size_of="GroupIds"), + NDRFullEmbPointerField( NDRConfPacketListField( "GroupIds", - [PGROUP_MEMBERSHIP()], - PGROUP_MEMBERSHIP, - count_from=lambda pkt: pkt.GroupCount, + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.GroupCount, ), - deferred=True, ), 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), - NDRFullPointerField( - NDRPacketField("LogonDomainId", PSID(), PSID), deferred=True - ), - FieldListField("Reserved1", [], NDRIntField("", 0), count_from=lambda _: 2), + NDRFullEmbPointerField(NDRPacketField("LogonDomainId", SID(), SID)), + NDRFieldListField("Reserved1", [], NDRIntField("", 0), length_is=lambda _: 2), NDRIntField("UserAccountControl", 0), - FieldListField("Reserved3", [], NDRIntField("", 0), count_from=lambda _: 7), - NDRIntField("SidCount", 0), - NDRFullPointerField( + NDRFieldListField("Reserved3", [], NDRIntField("", 0), length_is=lambda _: 7), + NDRIntField("SidCount", None, size_of="ExtraSids"), + NDRFullEmbPointerField( NDRConfPacketListField( "ExtraSids", - [PKERB_SID_AND_ATTRIBUTES()], - PKERB_SID_AND_ATTRIBUTES, - count_from=lambda pkt: pkt.SidCount, + [KERB_SID_AND_ATTRIBUTES()], + KERB_SID_AND_ATTRIBUTES, + size_is=lambda pkt: pkt.SidCount, ), - deferred=True, ), - NDRFullPointerField( - NDRPacketField("ResourceGroupDomainSid", PSID(), PSID), deferred=True + NDRFullEmbPointerField( + NDRPacketField("ResourceGroupDomainSid", SID(), SID), ), - NDRIntField("ResourceGroupCount", 0), - NDRFullPointerField( + NDRIntField("ResourceGroupCount", None, size_of="ResourceGroupIds"), + NDRFullEmbPointerField( NDRConfPacketListField( "ResourceGroupIds", - [PGROUP_MEMBERSHIP()], - PGROUP_MEMBERSHIP, - count_from=lambda pkt: pkt.ResourceGroupCount, + [GROUP_MEMBERSHIP()], + GROUP_MEMBERSHIP, + size_is=lambda pkt: pkt.ResourceGroupCount, ), - deferred=True, ), ] -class KERB_VALIDATION_INFO_WRAP(NDRPacket): - # Extra packing class to handle all deferred pointers - # (usually, this would be the packing RPC request/response) - fields_desc = [NDRPacketField("data", None, KERB_VALIDATION_INFO)] - - -_PACTYPES[1] = KERB_VALIDATION_INFO_WRAP +_PACTYPES[1] = KERB_VALIDATION_INFO # sect 2.6 @@ -267,7 +271,9 @@ class PAC_CREDENTIAL_INFO(Packet): 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 ( - _NTLM_post_build( + _pac_post_build( self, pkt, - self.OFFSET, - { - "Upn": 0, - "DnsDomainName": 4, - }, - ) + - pay + offset, + fields, + ) + + pay ) _PACTYPES[0xC] = UPN_DNS_INFO -# sect 2.11 - NDR PACKETS AUTO-GENERATED +# sect 2.11 - NDR PACKETS try: from enum import IntEnum @@ -418,63 +462,58 @@ class CLAIMS_COMPRESSION_FORMAT(IntEnum): COMPRESSION_FORMAT_XPRESS_HUFF = 4 -class u_sub0(NDRPacket): +class CLAIM_ENTRY_sub0(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("ValueCount", 0), - NDRFullPointerField( + NDRIntField("ValueCount", None, size_of="Int64Values"), + NDRFullEmbPointerField( NDRConfFieldListField( "Int64Values", [], NDRSignedLongField, - count_from=lambda pkt: pkt.ValueCount, + size_is=lambda pkt: pkt.ValueCount, ), - deferred=True, ), ] -class u_sub1(NDRPacket): +class CLAIM_ENTRY_sub1(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("ValueCount", 0), - NDRFullPointerField( + NDRIntField("ValueCount", None, size_of="Uint64Values"), + NDRFullEmbPointerField( NDRConfFieldListField( - "Uint64Values", [], NDRLongField, count_from=lambda pkt: pkt.ValueCount + "Uint64Values", [], NDRLongField, size_is=lambda pkt: pkt.ValueCount ), - deferred=True, ), ] -class u_sub2(NDRPacket): +class CLAIM_ENTRY_sub2(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("ValueCount", 0), - NDRFullPointerField( + NDRIntField("ValueCount", None, size_of="StringValues"), + NDRFullEmbPointerField( NDRConfFieldListField( "StringValues", [], - NDRFullPointerField( + NDRFullEmbPointerField( NDRConfVarStrNullFieldUtf16("StringVal", ""), - deferred=True, ), - count_from=lambda pkt: pkt.ValueCount, + size_is=lambda pkt: pkt.ValueCount, ), - deferred=True, ), ] -class u_sub3(NDRPacket): +class CLAIM_ENTRY_sub3(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRIntField("ValueCount", 0), - NDRFullPointerField( + NDRIntField("ValueCount", None, size_of="BooleanValues"), + NDRFullEmbPointerField( NDRConfFieldListField( - "BooleanValues", [], NDRLongField, count_from=lambda pkt: pkt.ValueCount + "BooleanValues", [], NDRLongField, size_is=lambda pkt: pkt.ValueCount ), - deferred=True, ), ] @@ -482,46 +521,46 @@ class u_sub3(NDRPacket): class CLAIM_ENTRY(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullPointerField(NDRConfVarStrNullFieldUtf16("Id", ""), deferred=True), + NDRFullEmbPointerField(NDRConfVarStrNullFieldUtf16("Id", "")), NDRInt3264EnumField("Type", 0, CLAIM_TYPE), NDRUnionField( [ ( - NDRPacketField("Values", u_sub0(), u_sub0), + NDRPacketField("Values", CLAIM_ENTRY_sub0(), CLAIM_ENTRY_sub0), ( ( - lambda pkt: getattr(pkt, "Type", None) == - CLAIM_TYPE.CLAIM_TYPE_INT64 + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_INT64 ), (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_INT64), ), ), ( - NDRPacketField("Values", u_sub1(), u_sub1), + NDRPacketField("Values", CLAIM_ENTRY_sub1(), CLAIM_ENTRY_sub1), ( ( - lambda pkt: getattr(pkt, "Type", None) == - CLAIM_TYPE.CLAIM_TYPE_UINT64 + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_UINT64 ), (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_UINT64), ), ), ( - NDRPacketField("Values", u_sub2(), u_sub2), + NDRPacketField("Values", CLAIM_ENTRY_sub2(), CLAIM_ENTRY_sub2), ( ( - lambda pkt: getattr(pkt, "Type", None) == - CLAIM_TYPE.CLAIM_TYPE_STRING + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_STRING ), (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_STRING), ), ), ( - NDRPacketField("Values", u_sub3(), u_sub3), + NDRPacketField("Values", CLAIM_ENTRY_sub3(), CLAIM_ENTRY_sub3), ( ( - lambda pkt: getattr(pkt, "Type", None) == - CLAIM_TYPE.CLAIM_TYPE_BOOLEAN + lambda pkt: getattr(pkt, "Type", None) + == CLAIM_TYPE.CLAIM_TYPE_BOOLEAN ), (lambda _, val: val.tag == CLAIM_TYPE.CLAIM_TYPE_BOOLEAN), ), @@ -529,7 +568,7 @@ class CLAIM_ENTRY(NDRPacket): ], StrFixedLenField("Values", "", length=0), align=(2, 8), - switch_fmt=(" DceRpcSession: + return self.sock.session + + def connect(self, host, port=None, timeout=5, smb_kwargs={}): + """ + Initiate a connection. + + :param host: the host to connect to + :param port: (optional) the port to connect to + :param timeout: (optional) the connection timeout (default 5) + """ + if port is None: + if self.transport == DCERPC_Transport.NCACN_IP_TCP: # IP/TCP + port = 135 + elif self.transport == DCERPC_Transport.NCACN_NP: # SMB + port = 445 + else: + raise ValueError( + "Can't guess the port for transport: %s" % self.transport + ) + 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 + # We pack the socket into a SMB_RPC_SOCKET + sock = self.smbrpcsock = SMB_RPC_SOCKET.from_tcpsock( + sock, ssp=self.ssp, **smb_kwargs + ) + 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, + auth_context_id=self.auth_context_id, + **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_id += 1 + pkt = ( + DceRpc5( + call_id=self.call_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_id += 1 + pkt = ( + DceRpc5( + call_id=self.call_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, + 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") + + # 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. + """ + # NDR 2.0 + contexts = [ + DceRpc5Context( + cont_id=self.all_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.all_cont_id += 1 + + # NDR64 + if self.ndr64: + contexts.append( + DceRpc5Context( + cont_id=self.all_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.all_cont_id += 1 + + # BindTimeFeatureNegotiationBitmask + contexts.append( + DceRpc5Context( + cont_id=self.all_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.all_cont_id += 1 + + return contexts + + def _bind(self, interface: Union[DceRpcInterface, ComInterface], reqcls, respcls): + """ + 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 + ) + ), + target_name="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.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 "" + ) + ), + ) + if respcls not in resp: + token = None + status = GSS_S_FAILURE + else: + # Call the underlying SSP + self.sspcontext, token, status = self.ssp.GSS_Init_sec_context( + self.sspcontext, + token=resp.auth_verifier.auth_value, + target_name="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.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.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, + token=resp.auth_verifier.auth_value, + target_name="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 any(x.result == 0 for x in resp.results[: int(self.ndr64) + 1]) + ): + self.call_id = 0 # reset call id + port = resp.sec_addr.port_spec.decode() + ndr = self.session.ndr64 and "NDR64" or "NDR32" + self.ndr64 = self.session.ndr64 + self.cont_id = int(self.session.ndr64) # ctx 0 for NDR32, 1 for 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 DceRpc5Fault in resp: + 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]): + """ + Bind the client to an interface + + :param interface: the DceRpcInterface object + """ + return self._bind(interface, DceRpc5Bind, DceRpc5BindAck) + + def alter_context(self, interface: Union[DceRpcInterface, ComInterface]): + """ + Alter context: post-bind context negotiation + + :param interface: the DceRpcInterface object + """ + return self._bind(interface, DceRpc5AlterContext, DceRpc5AlterContextResp) + + def bind_or_alter(self, interface: Union[DceRpcInterface, ComInterface]): + """ + 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 + self.bind(interface) + elif self.session.rpc_bind_interface != interface: + # An interface is already bound + self.alter_context(interface) + + 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, + ip: 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 ip: the ip 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) + """ + if self.transport == DCERPC_Transport.NCACN_IP_TCP: + # IP/TCP + # 1. ask the endpoint mapper (port 135) for the IP:PORT + endpoints = get_endpoint( + ip, + interface, + ndrendian=self.ndrendian, + verb=self.verb, + ) + if endpoints: + ip, port = endpoints[0] + else: + return + # 2. Connect to that IP:PORT + self.connect(ip, port=port, timeout=timeout) + elif self.transport == DCERPC_Transport.NCACN_NP: + # SMB + # 1. ask the endpoint mapper (over SMB) for the namedpipe + endpoints = get_endpoint( + ip, + interface, + transport=self.transport, + ndrendian=self.ndrendian, + verb=self.verb, + smb_kwargs=smb_kwargs, + ) + if endpoints: + pipename = endpoints[0].lstrip("\\pipe\\") + else: + return + # 2. connect to the SMB server + self.connect(ip, port=port, timeout=timeout, smb_kwargs=smb_kwargs) + # 3. open the new named pipe + self.open_smbpipe(pipename) + # 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: + pkt.show() + 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, + ndr64=False, + ndrendian=ndrendian, + verb=verb, + ssp=ssp, + ) # EPM only works with NDR32 + client.connect(ip, smb_kwargs=smb_kwargs) + if transport == DCERPC_Transport.NCACN_NP: # SMB + client.open_smbpipe("epmapper") + client.bind(find_dcerpc_interface("ept")) + endpoints = client.epm_map(interface) + client.close() + return endpoints diff --git a/scapy/layers/msrpce/rpcserver.py b/scapy/layers/msrpce/rpcserver.py new file mode 100644 index 00000000000..5a8435cac17 --- /dev/null +++ b/scapy/layers/msrpce/rpcserver.py @@ -0,0 +1,475 @@ +# 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 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, + DceRpc5PortAny, + DceRpc5Request, + DceRpc5Response, + DceRpc5Result, + DceRpc5TransferSyntax, + DceRpcInterface, + DceRpcSession, + 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, + Optional, +) + + +class _DCERPC_Server_metaclass(type): + 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): + func.dcerpc_command = reqcls + 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): + cls = req[DceRpc5Request].payload.__class__ + if cls in self.dcerpc_commands: + # call handler + return self.dcerpc_commands[cls](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=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__) + ) + ) + # 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 7b3f50a3a7e..0995d67d771 100644 --- a/scapy/layers/netbios.py +++ b/scapy/layers/netbios.py @@ -12,7 +12,8 @@ import struct from scapy.arch import get_if_addr from scapy.base_classes import Net -from scapy.ansmachine import AnsweringMachine, AnsweringMachineUtils +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 @@ -34,7 +35,7 @@ XStrFixedLenField ) from scapy.layers.inet import IP, UDP, TCP -from scapy.layers.l2 import SourceMACField +from scapy.layers.l2 import Ether, SourceMACField class NetBIOS_DS(Packet): @@ -129,7 +130,12 @@ class NBNSHeader(Packet): ShortField("ARCOUNT", 0), ] + def hashret(self): + return b"NBNS" + struct.pack("!B", self.OPCODE) + + # Name Query Request +# RFC1002 sect 4.2.12 class NBNSQueryRequest(Packet): @@ -142,7 +148,7 @@ class NBNSQueryRequest(Packet): def mysummary(self): return "NBNSQueryRequest who has '\\\\%s'" % ( - self.QUESTION_NAME.strip().decode() + self.QUESTION_NAME.decode(errors="backslashreplace") ) @@ -151,6 +157,7 @@ def mysummary(self): # Name Query Response +# RFC1002 sect 4.2.13 class NBNS_ADD_ENTRY(Packet): @@ -178,19 +185,30 @@ class NBNSQueryResponse(Packet): ] def mysummary(self): - if not self.ADDR_ENTRY: + 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.strip().decode(), + 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, +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 Request +# Node Status Request +# RFC1002 sect 4.2.17 class NBNSNodeStatusRequest(NBNSQueryRequest): name = "NBNS status request" @@ -199,15 +217,16 @@ class NBNSNodeStatusRequest(NBNSQueryRequest): def mysummary(self): return "NBNSNodeStatusRequest who has '\\\\%s'" % ( - self.QUESTION_NAME.strip().decode() + self.QUESTION_NAME.decode(errors="backslashreplace") ) 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 +# Node Status Response +# RFC1002 sect 4.2.18 class NBNSNodeStatusResponseService(Packet): name = "NBNS Node Status Response Service" @@ -254,8 +273,9 @@ def answers(self, other): bind_layers(NBNSHeader, NBNSNodeStatusResponse, OPCODE=0x0, NM_FLAGS=0x40, RESPONSE=1, ANCOUNT=1) -# Name Registration Request +# Name Registration Request +# RFC1002 sect 4.2.2 class NBNSRegistrationRequest(Packet): name = "NBNS registration request" @@ -277,13 +297,17 @@ class NBNSRegistrationRequest(Packet): 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" @@ -310,7 +334,7 @@ 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), @@ -319,11 +343,19 @@ class NBTDatagram(Packet): ShortEnumField("SUFFIX2", 0x4141, _NETBIOS_SUFFIXES), 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", @@ -335,16 +367,29 @@ class NBTSession(Packet): def post_build(self, pkt, pay): if self.LENGTH is None: - length = len(pay) & (2**18 - 1) + 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_layers(UDP, NBTDatagram, dport=138) +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) @@ -354,7 +399,7 @@ def post_build(self, pkt, pay): class NBNS_am(AnsweringMachine): - function_name = "nbns_spoof" + function_name = "nbnsd" filter = "udp port 137" sniff_options = {"store": 0} @@ -366,7 +411,7 @@ def parse_options(self, server_name=None, from_ip=None, ip=None): :param from_ip: an IP (can have a netmask) to filter on :param ip: the IP to answer with """ - self.ServerName = server_name + self.ServerName = bytes_encode(server_name or "") self.ip = ip if isinstance(from_ip, str): self.from_ip = Net(from_ip) @@ -378,15 +423,19 @@ def is_request(self, req): return False return NBNSQueryRequest in req and ( not self.ServerName or - req[NBNSQueryRequest].QUESTION_NAME.decode().strip() == - self.ServerName + req[NBNSQueryRequest].QUESTION_NAME.strip() == self.ServerName ) def make_reply(self, req): # type: (Packet) -> Packet - resp = AnsweringMachineUtils.reverse_packet(req) - address = self.ip or get_if_addr( - self.optsniff.get("iface", conf.iface)) + 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, diff --git a/scapy/layers/netflow.py b/scapy/layers/netflow.py index 8f7918a35d0..ef0fe8e9646 100644 --- a/scapy/layers/netflow.py +++ b/scapy/layers/netflow.py @@ -30,9 +30,12 @@ """ +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 @@ -63,11 +66,18 @@ ) 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" @@ -186,916 +196,17 @@ 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", -} +@dataclasses.dataclass +class _N910F: + name: str + length: int = 0 + field: Field = None + kwargs: Dict[str, Any] = dataclasses.field(default_factory=dict) -ScopeFieldTypes = { - 1: "System", - 2: "Interface", - 3: "Line card", - 4: "Cache", - 5: "Template", -} - -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: @@ -1135,118 +246,991 @@ 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), + 2: _N910F("IN_PKTS", length=4), + 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"), + 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"), + 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), + 20: _N910F("MUL_DST_BYTES", length=4), + 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), + 24: _N910F("OUT_PKTS", length=4), + 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), + 41: _N910F("TOTAL_PKTS_EXP", length=4), + 42: _N910F("TOTAL_FLOWS_EXP", length=4), + 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), + 71: _N910F("MPLS_LABEL_2", length=3), + 72: _N910F("MPLS_LABEL_3", length=3), + 73: _N910F("MPLS_LABEL_4", length=3), + 74: _N910F("MPLS_LABEL_5", length=3), + 75: _N910F("MPLS_LABEL_6", length=3), + 76: _N910F("MPLS_LABEL_7", length=3), + 77: _N910F("MPLS_LABEL_8", length=3), + 78: _N910F("MPLS_LABEL_9", length=3), + 79: _N910F("MPLS_LABEL_10", length=3), + 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", } @@ -1259,11 +1243,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 @@ -1298,10 +1294,10 @@ def __init__(self, *args, **kwargs): Packet.__init__(self, *args, **kwargs) if (self.fieldType is not None and self.fieldLength is None and - self.fieldType in NetflowV9TemplateFieldDefaultLengths): - self.fieldLength = NetflowV9TemplateFieldDefaultLengths[ + self.fieldType in NetflowV910TemplateFields): + self.fieldLength = NetflowV910TemplateFields[ self.fieldType - ] + ].length or None def default_payload_class(self, p): return conf.padding_layer @@ -1338,18 +1334,21 @@ def _GenNetflowRecordV9(cls, lengths_list): """ _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, []) - ) + _f_type = None _f_kwargs = {} + if k in NetflowV910TemplateFields: + _f = NetflowV910TemplateFields[k] + _f_type = _f.field + _f_kwargs = _f.kwargs + if _f_type: if issubclass(_f_type, _AdjustableNetflowField): _f_kwargs["length"] = j + print(k, _f_kwargs) _fields_desc.append( _f_type( NetflowV910TemplateFieldTypes.get(k, "unknown_data"), - 0, *_f_args, **_f_kwargs + 0, **_f_kwargs ) ) else: @@ -1581,25 +1580,21 @@ class NetflowSession(IPSession): See help(scapy.layers.netflow) for more infos. """ def __init__(self, *args, **kwargs): - IPSession.__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" @@ -1659,12 +1654,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 index 65fec7d5af7..556faee0ea5 100644 --- a/scapy/layers/ntlm.py +++ b/scapy/layers/ntlm.py @@ -1,77 +1,93 @@ # 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) Gabriel Potter """ NTLM -https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-NLMP/%5bMS-NLMP%5d.pdf +This is documented in [MS-NLMP] + +.. note:: + You will find more complete documentation for this layer over at + `GSSAPI `_ """ -import ssl -import socket +import copy +import time +import os import struct -import threading -from scapy.arch import get_if_addr -from scapy.asn1.asn1 import ASN1_STRING, ASN1_Codecs +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 + ASN1F_SEQUENCE_OF, ) from scapy.asn1packet import ASN1_Packet -from scapy.automaton import Automaton, ObjectPipe from scapy.compat import bytes_base64 +from scapy.error import log_runtime from scapy.fields import ( - Field, ByteEnumField, ByteField, ConditionalField, + Field, FieldLenField, FlagsField, + LEIntEnumField, LEIntField, - _StrField, LEShortEnumField, + LEShortField, + LEThreeBytesField, MultipleTypeField, PacketField, PacketListField, - LEShortField, StrField, StrFieldUtf16, StrFixedLenField, - LEIntEnumField, - LEThreeBytesField, StrLenFieldUtf16, UTCTimeField, XStrField, XStrFixedLenField, XStrLenField, + _StrField, ) from scapy.packet import Packet from scapy.sessions import StringBuffer -from scapy.supersocket import SSLStreamSocket, StreamSocket -from scapy.compat import ( +from scapy.layers.gssapi import ( + 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, + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, +) + +# Typing imports +from typing import ( Any, Callable, - Dict, List, - Tuple, Optional, + Tuple, + Union, ) # Crypto imports -from scapy.layers.tls.crypto.hash import Hash_MD4 - -if conf.crypto_valid: - from cryptography.hazmat.primitives import hashes, hmac -else: - hashes = hmac = None +from scapy.layers.tls.crypto.hash import Hash_MD4, Hash_MD5 +from scapy.layers.tls.crypto.h_mac import Hmac_MD5 ########## # Fields # @@ -81,24 +97,40 @@ 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"] + + __slots__ = [ + "fields", + "fields_map", + "offset", + "length_from", + "force_order", + "offset_name", + ] islist = True - def __init__(self, - name, # type: str - offset, # type: int - fields, # type: List[Field[Any, Any]] - length_from=None # type: Optional[Callable[[Packet], int]] - ): + 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] + [ + (field.name, field.default) + for field in fields + if field.default is not None + ], ) def _on_payload(self, pkt, x, func): @@ -109,13 +141,11 @@ def _on_payload(self, pkt, x, func): 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): + 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 - )) + results.append((field_name, value)) return results def i2h(self, pkt, x): @@ -130,20 +160,38 @@ 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 + "BufferOffset") - if offset is not None: - offset -= self.offset - else: + offset = pkt.getfieldval(field_name + self.offset_name) + if offset is None: + # No offset specified: calc offset = len(buf) - buf.append(field.addfield(pkt, b"", value), offset + 1) - return s + bytes(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]]] @@ -156,20 +204,33 @@ def getfield(self, pkt, s): return s, [] results = [] max_offset = 0 - for field in self.fields: - offset = pkt.getfieldval(field.name + "BufferOffset") - self.offset + 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])) - if max_offset: - ret += remain[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] @@ -177,15 +238,48 @@ def getfield(self, pkt, s): class _NTLMPayloadPacket(Packet): _NTLM_PAYLOAD_FIELD_NAME = "Payload" - def __getattr__(self, attr): + 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).__getattr__(attr) + return super(_NTLMPayloadPacket, self).getfieldval(attr) except AttributeError: try: return next( x[1] - for x in super(_NTLMPayloadPacket, self).__getattr__( + for x in super(_NTLMPayloadPacket, self).getfieldval( self._NTLM_PAYLOAD_FIELD_NAME ) if x[0] == attr @@ -193,6 +287,29 @@ def __getattr__(self, 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: @@ -204,42 +321,72 @@ def setfieldval(self, attr, val): 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 - )) + 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 + self._NTLM_PAYLOAD_FIELD_NAME, Payload ) -def _NTLM_post_build(self, p, pay_offset, fields): - # type: (Packet, bytes, int, Dict[str, Tuple[str, int]]) -> bytes +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["Payload"]: - length = self.get_field( - "Payload").fields_map[field_name].i2len(self, value) + 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] - # Length - if self.getfieldval(field_name + "Len") is None: - p = p[:offset] + \ - struct.pack(" 32) and 40 or 32) + fields_desc = ( + [ + NTLM_Header, + FlagsField("NegotiateFlags", 0, -32, _negotiateFlags), + # DomainNameFields + LEShortField("DomainNameLen", None), + LEShortField("DomainNameMaxLen", None), + LEIntField("DomainNameBufferOffset", None), + # WorkstationFields + LEShortField("WorkstationNameLen", None), + LEShortField("WorkstationNameMaxLen", None), + LEIntField("WorkstationNameBufferOffset", None), + ] + + [ + # VERSION + ConditionalField( + # (not present on some old Windows versions. We use a heuristic) + x, + lambda pkt: ( + ( + 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, + [ + _NTLMStrField("DomainName", b""), + _NTLMStrField("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 + return ( + _NTLM_post_build( + self, + pkt, + self.OFFSET(), + { + "DomainName": 16, + "WorkstationName": 24, + }, + ) + + pay + ) + # Challenge class Single_Host_Data(Packet): fields_desc = [ - LEIntField("Size", 0), + LEIntField("Size", 48), LEIntField("Z4", 0), XStrFixedLenField("CustomData", b"", length=8), XStrFixedLenField("MachineID", b"", length=32), @@ -385,37 +569,59 @@ def default_payload_class(self, payload): class AV_PAIR(Packet): name = "NTLM AV Pair" fields_desc = [ - LEShortEnumField('AvId', 0, { - 0x0000: "MsvAvEOL", - 0x0001: "MsvAvNbComputerName", - 0x0002: "MsvAvNbDomainName", - 0x0003: "MsvAvDnsComputerName", - 0x0004: "MsvAvDnsDomainName", - 0x0005: "MsvAvDnsTreeName", - 0x0006: "MsvAvFlags", - 0x0007: "MsvAvTimestamp", - 0x0008: "MsvAvSingleHost", - 0x0009: "MsvAvTargetName", - 0x000A: "MsvAvChannelBindings", - }), - FieldLenField('AvLen', None, length_of="Value", fmt=" 48) and 56 or 48) + fields_desc = ( + [ + NTLM_Header, + # TargetNameFields + LEShortField("TargetNameLen", None), + LEShortField("TargetNameMaxLen", None), + LEIntField("TargetNameBufferOffset", None), + # + FlagsField("NegotiateFlags", 0, -32, _negotiateFlags), + XStrFixedLenField("ServerChallenge", None, length=8), + XStrFixedLenField("Reserved", None, length=8), + # TargetInfoFields + LEShortField("TargetInfoLen", None), + LEShortField("TargetInfoMaxLen", None), + LEIntField("TargetInfoBufferOffset", None), + ] + + [ + # VERSION + ConditionalField( + # (not present on some old Windows versions. We use a heuristic) + x, + lambda pkt: ((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 + 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), @@ -481,22 +717,29 @@ class NTLM_RESPONSE(Packet): class NTLMv2_CLIENT_CHALLENGE(Packet): fields_desc = [ - ByteField("RespType", 0), - ByteField("HiRespType", 0), + ByteField("RespType", 1), + ByteField("HiRespType", 1), LEShortField("Reserved1", 0), LEIntField("Reserved2", 0), - UTCTimeField("TimeStamp", None, fmt=" 72) and 88 or 72) + ) + fields_desc = ( + [ + NTLM_Header, + # LmChallengeResponseFields + LEShortField("LmChallengeResponseLen", None), + LEShortField("LmChallengeResponseMaxLen", None), + LEIntField("LmChallengeResponseBufferOffset", None), + # NtChallengeResponseFields + LEShortField("NtChallengeResponseLen", None), + LEShortField("NtChallengeResponseMaxLen", None), + LEIntField("NtChallengeResponseBufferOffset", None), + # DomainNameFields + LEShortField("DomainNameLen", None), + LEShortField("DomainNameMaxLen", None), + LEIntField("DomainNameBufferOffset", None), + # UserNameFields + LEShortField("UserNameLen", None), + LEShortField("UserNameMaxLen", None), + LEIntField("UserNameBufferOffset", None), + # WorkstationFields + LEShortField("WorkstationLen", None), + LEShortField("WorkstationMaxLen", None), + LEIntField("WorkstationBufferOffset", None), + # EncryptedRandomSessionKeyFields + LEShortField("EncryptedRandomSessionKeyLen", None), + LEShortField("EncryptedRandomSessionKeyMaxLen", None), + LEIntField("EncryptedRandomSessionKeyBufferOffset", None), + # NegotiateFlags + FlagsField("NegotiateFlags", 0, -32, _negotiateFlags), + # VERSION + ] + + [ + ConditionalField( + # (not present on some old Windows versions. We use a heuristic) + x, + lambda pkt: ((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.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 + 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): @@ -612,232 +903,786 @@ 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)) ) -# Answering machine - - -class _NTLM_Automaton(Automaton): - def __init__(self, sock, **kwargs): - # type: (StreamSocket, Any) -> None - self.token_pipe = ObjectPipe() - self.values = {} - for key, dflt in [("DROP_MIC_v1", False), ("DROP_MIC_v2", False)]: - setattr(self, key, kwargs.pop(key, dflt)) - self.DROP_MIC = self.DROP_MIC_v1 or self.DROP_MIC_v2 - super(_NTLM_Automaton, self).__init__( - recvsock=lambda **kwargs: sock, - ll=lambda **kwargs: sock, - **kwargs + +# 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 + """ - def _get_token(self, token): - if not token: - return None, None, None, None + 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) - from scapy.layers.gssapi import ( - GSSAPI_BLOB, - SPNEGO_negToken, - SPNEGO_Token + +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 - negResult = None - MIC = None - rawToken = False - - if isinstance(token, bytes): - # SMB 1 - non extended - return (token, None, None, True) - if isinstance(token, (NTLM_NEGOTIATE, - NTLM_CHALLENGE, - NTLM_AUTHENTICATE, - NTLM_AUTHENTICATE_V2)): - ntlm = token - rawToken = True - else: - if isinstance(token, GSSAPI_BLOB): - token = token.innerContextToken - if isinstance(token, SPNEGO_negToken): - token = token.token - if hasattr(token, "mechListMIC") and token.mechListMIC: - MIC = token.mechListMIC.value - if hasattr(token, "negResult"): - negResult = token.negResult - try: - ntlm = token.mechToken - except AttributeError: - ntlm = token.responseToken - if isinstance(ntlm, SPNEGO_Token): - ntlm = ntlm.value - if isinstance(ntlm, ASN1_STRING): - ntlm = NTLM_Header(ntlm.val) - if isinstance(ntlm, conf.raw_layer): - ntlm = NTLM_Header(ntlm.load) - if self.DROP_MIC_v1 or self.DROP_MIC_v2: - if isinstance(ntlm, NTLM_AUTHENTICATE): - ntlm.MIC = b"\0" * 16 - ntlm.NtChallengeResponseLen = None - ntlm.NtChallengeResponseMaxLen = None - ntlm.EncryptedRandomSessionKeyBufferOffset = None - if self.DROP_MIC_v2: - ChallengeResponse = next( - v[1] for v in ntlm.Payload - if v[0] == 'NtChallengeResponse' - ) - i = next( - i for i, k in enumerate(ChallengeResponse.AvPairs) - if k.AvId == 0x0006 - ) - ChallengeResponse.AvPairs.insert( - i + 1, - AV_PAIR(AvId="MsvAvFlags", Value=0) - ) - return ntlm, negResult, MIC, rawToken + 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(" None - self.srv_atmt = srv_atmt - def set_srv(self, attr, value): - self.srv_atmt.values[attr] = value +# sect 3.4.5.2 - def get_token(self): - return self.srv_atmt.token_pipe.recv() - def echo(self, pkt): - return self.srv_atmt.send(pkt) +def SIGNKEY(NegFlg, ExportedSessionKey, Mode): + if NegFlg.NEGOTIATE_EXTENDED_SESSIONSECURITY: + if Mode == "Client": + return Hash_MD5().digest( + ExportedSessionKey + + b"session key to client-to-server signing key magic constant\x00" + ) + elif Mode == "Server": + return Hash_MD5().digest( + ExportedSessionKey + + b"session key to server-to-client signing key magic constant\x00" + ) + else: + raise ValueError("Unknown Mode") + else: + return None - def wait_server(self): - kwargs = self.client_pipe.recv() - self.client_pipe.close() - return kwargs +# sect 3.4.5.3 -class NTLM_Server(_NTLM_Automaton): + +def SEALKEY(NegFlg, ExportedSessionKey, Mode): + if NegFlg.NEGOTIATE_EXTENDED_SESSIONSECURITY: + if NegFlg.NEGOTIATE_128: + SealKey = ExportedSessionKey + elif NegFlg.NEGOTIATE_56: + SealKey = ExportedSessionKey[:7] + else: + SealKey = ExportedSessionKey[:5] + if Mode == "Client": + return Hash_MD5().digest( + SealKey + + b"session key to client-to-server sealing key magic constant\x00" + ) + elif Mode == "Server": + return Hash_MD5().digest( + SealKey + + b"session key to server-to-client sealing key magic constant\x00" + ) + else: + raise ValueError("Unknown Mode") + elif NegFlg.NEGOTIATE_LM_KEY: + if NegFlg.NEGOTIATE_56: + return ExportedSessionKey[:6] + b"\xa0" + else: + return ExportedSessionKey[:4] + b"\xe5\x38\xb0" + else: + return ExportedSessionKey + + +# --- SSP + + +class NTLMSSP(SSP): """ - A class to overload to create a server automaton when using - NTLM. - - :param NTLM_VALUES: a dict whose keys are - - "NetbiosDomainName" - - "NetbiosComputerName" - - "DnsDomainName" - - "DnsComputerName" - - "DnsTreeName" - - "Flags" - - "Timestamp" - - :param IDENTITIES: a dict {"username": NTOWFv2("password", "username", "domain")} - (this is the KeyResponseNT). Setting this value enables - signature computation and authenticates inbound users. - :param DOMAIN_AUTH: a tuple ("", "machineName", b"machinePassword") to - use for domain authentication, used to establish the netlogon - session. (UNIMPLEMENTED) + The NTLM SSP + + Common arguments: + + :param auth_level: One of DCE_C_AUTHN_LEVEL + :param USE_MIC: whether to use a MIC or not (default: True) + :param NTLM_VALUES: a dictionary used to override the following values + + In case of a client:: + + - NegotiateFlags + - ProductMajorVersion + - ProductMinorVersion + - ProductBuild + + In case of a server:: + + - NetbiosDomainName + - NetbiosComputerName + - DnsComputerName + - DnsDomainName (defaults to DOMAIN) + - DnsTreeName (defaults to DOMAIN) + - Flags + - Timestamp + + Client-only arguments: + + :param UPN: the UPN to use for NTLM auth. If no domain is specified, will + use the one provided by the server (domain in a domain, local + if without domain) + :param HASHNT: the password to use for NTLM auth + :param PASSWORD: the password to use for NTLM auth + + Server-only arguments: + + :param DOMAIN_FQDN: the domain FQDN (default: domain.local) + :param DOMAIN_NB_NAME: the domain Netbios name (default: strip DOMAIN_FQDN) + :param COMPUTER_NB_NAME: the server Netbios name (default: SRV) + :param COMPUTER_FQDN: the server FQDN + (default: .) + :param IDENTITIES: a dict {"username": } + Setting this value enables signature computation and + authenticates inbound users. """ - port = 445 - cls = conf.raw_layer - - def __init__(self, *args, **kwargs): - self.cli_atmt = None - self.cli_values = dict() - self.ntlm_values = kwargs.pop("NTLM_VALUES", None) - self.ntlm_state = 0 - self.DOMAIN_AUTH = kwargs.pop("DOMAIN_AUTH", None) - self.IDENTITIES = kwargs.pop("IDENTITIES", None) - self.CHECK_LOGIN = bool(self.IDENTITIES) or bool(self.DOMAIN_AUTH) - self.SigningSessionKey = None - self.Challenge = None - super(NTLM_Server, self).__init__(*args, **kwargs) - - def bind(self, cli_atmt): - # type: (NTLM_Client) -> None - self.cli_atmt = cli_atmt - - def get_token(self, negoex=False): - if negoex: - # Special case: negoex - if self.cli_atmt: - return self.cli_atmt.token_pipe.recv() + + oid = "1.3.6.1.4.1.311.2.2.10" + 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", + ] + + 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.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" + + def __init__( + self, + UPN=None, + HASHNT=None, + PASSWORD=None, + USE_MIC=True, + 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.USE_MIC = USE_MIC + self.NTLM_VALUES = NTLM_VALUES + if UPN is not None: + 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 + self.DOMAIN_FQDN = DOMAIN_FQDN or "domain.local" + self.DOMAIN_NB_NAME = ( + DOMAIN_NB_NAME or self.DOMAIN_FQDN.split(".")[0].upper()[:15] + ) + self.COMPUTER_NB_NAME = COMPUTER_NB_NAME or "SRV" + self.COMPUTER_FQDN = COMPUTER_FQDN or ( + self.COMPUTER_NB_NAME.lower() + "." + self.DOMAIN_FQDN + ) + 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_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 canMechListMIC(self, Context): + 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 not Context or not Context.SessionKey: + # Not available yet + 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, + 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( + NegotiateFlags="+".join( + [ + "NEGOTIATE_UNICODE", + "REQUEST_TARGET", + "NEGOTIATE_NTLM", + "NEGOTIATE_ALWAYS_SIGN", + "TARGET_TYPE_DOMAIN", + "NEGOTIATE_EXTENDED_SESSIONSECURITY", + "NEGOTIATE_TARGET_INFO", + "NEGOTIATE_VERSION", + "NEGOTIATE_128", + "NEGOTIATE_56", + ] + + ( + [ + "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 [] + ) + ), + ProductMajorVersion=10, + ProductMinorVersion=0, + ProductBuild=19041, + ) + 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]) + 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 = 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 !" + ) + 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 + # Take a default token + tok = NTLM_AUTHENTICATE_V2( + NegotiateFlags=chall_tok.NegotiateFlags, + ProductMajorVersion=10, + ProductMinorVersion=0, + ProductBuild=19041, + ) + tok.LmChallengeResponse = LMv2_RESPONSE() + from scapy.layers.kerberos import _parse_upn + + try: + tok.UserName, realm = _parse_upn(self.UPN) + except ValueError: + tok.UserName, realm = self.UPN, None + if realm is None: + try: + tok.DomainName = chall_tok.getAv(0x0002).Value + except IndexError: + log_runtime.warning( + "No realm specified in UPN, nor provided by server" + ) + tok.DomainName = self.DOMAIN_NB_NAME.encode() else: - self.token_pipe.clear() - return None, None, None, None - from random import randint - if self.ntlm_state == 0: - # First token asked (after negotiate) - self.ntlm_state = 1 - negResult, MIC, rawToken = None, None, False + tok.DomainName = realm + try: + tok.Workstation = Context.ServerHostname = chall_tok.getAv( + 0x0001 + ).Value # noqa: E501 + except IndexError: + tok.Workstation = "WIN" + cr = tok.NtChallengeResponse = NTLMv2_RESPONSE( + ChallengeFromClient=os.urandom(8), + ) + try: + # the server SHOULD set the timestamp in the CHALLENGE_MESSAGE + cr.TimeStamp = chall_tok.getAv(0x0007).Value + except IndexError: + cr.TimeStamp = int((time.time() + 11644473600) * 1e7) + cr.AvPairs = ( + 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)), + ), + ] + + ( + [ + 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="host/" + tok.Workstation), + 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]) + # Compute the ResponseKeyNT + ResponseKeyNT = NTOWFv2( + None, + tok.UserName, + tok.DomainName, + HashNt=self.HASHNT, + ) + # Compute the NTProofStr + cr.NTProofStr = cr.computeNTProofStr( + ResponseKeyNT, + chall_tok.ServerChallenge, + ) + # 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 + if self.USE_MIC: + tok.compute_mic(ExportedSessionKey, Context.neg_tok, chall_tok) + 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) + Context.state = self.STATE.CLI_SENT_AUTH + return Context, tok, GSS_S_COMPLETE + elif Context.state == self.STATE.CLI_SENT_AUTH: + if token: + # what is that? + status = GSS_S_DEFECTIVE_CREDENTIAL + 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, + 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 (token=negotiate) + nego_tok = 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 # Take a default token + currentTime = (time.time() + 11644473600) * 1e7 tok = NTLM_CHALLENGE( - ServerChallenge=struct.pack(" %s" % - (repr(address), repr(_sock.getsockname()))) - cli_atmt = remoteClientCls(remote_sock, debug=debug, **client_kwargs) - sock_tup = ((srv_atmt, cli_atmt), (sock, remote_sock)) - sniffers.append(sock_tup) - # Bind NTLM functions - srv_atmt.bind(cli_atmt) - cli_atmt.bind(srv_atmt) - # Start automatons - srv_atmt.runbg() - cli_atmt.runbg() - except KeyboardInterrupt: - print("Exiting.") - finally: - for atmts, socks in sniffers: - for atmt in atmts: - try: - atmt.forcestop(wait=False) - except Exception: - pass - for sock in socks: - try: - sock.close() - except Exception: - pass - ssock.close() + NTProofStr = auth_tok.NtChallengeResponse.computeNTProofStr( + ResponseKeyNT, + Context.chall_tok.ServerChallenge, + ) + if NTProofStr == auth_tok.NtChallengeResponse.NTProofStr: + return True + return False -def ntlm_server(serverCls, - server_kwargs=None, - iface=None, - debug=2): +class NTLMSSP_DOMAIN(NTLMSSP): """ - Starts a standalone NTLM server class - """ - assert issubclass( - serverCls, NTLM_Server), "Specify a correct NTLM server class" - - ssock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock.bind( - (get_if_addr(iface or conf.iface), serverCls.port)) - ssock.listen(5) - print(conf.color_theme.green( - "Server %s started. Waiting..." % serverCls.__name__ - )) - sniffers = [] - server_kwargs = server_kwargs or {} - try: - evt = threading.Event() - while not evt.is_set(): - clientsocket, address = ssock.accept() - sock = StreamSocket(clientsocket, serverCls.cls) - srv_atmt = serverCls(sock, debug=debug, **server_kwargs) - sniffers.append((srv_atmt, sock)) - print(conf.color_theme.gold("-> %s connected " % repr(address))) - # Start automatons - srv_atmt.runbg() - except KeyboardInterrupt: - print("Exiting.") - finally: - for atmt, sock in sniffers: - try: - atmt.forcestop(wait=False) - except Exception: - pass - try: - sock.close() - except Exception: - pass - ssock.close() + 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: -# 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 - ) - ) + :param UPN: the UPN of the machine account to login for Netlogon. + :param HASHNT: the HASHNT of the machine account to use for Netlogon. + :param PASSWORD: the PASSWORD of the machine acconut to use for Netlogon. + :param DC_IP: (optional) specify the IP of the DC. + Examples:: -class NEGOEX_EXCHANGE_NTLM(ASN1_Packet): - """ - GSSAPI NegoEX Exchange metadata blob - This was reversed and may be meaningless + >>> mySSP = NTLMSSP_DOMAIN( + ... UPN="Server1@domain.local", + ... HASHNT=bytes.fromhex("8846f7eaee8fb117ad06bdd830b7586c"), + ... ) """ - ASN1_codec = ASN1_Codecs.BER - ASN1_root = ASN1F_SEQUENCE( - ASN1F_SEQUENCE( - ASN1F_SEQUENCE_OF( - "items", [], - NEGOEX_EXCHANGE_NTLM_ITEM - ), - implicit_tag=0xa0 - ), - ) + def __init__(self, UPN, *args, timeout=3, ssp=None, **kwargs): + from scapy.layers.kerberos import KerberosSSP -# Crypto - [MS-NLMP] + # UPN is mandatory + kwargs["UPN"] = UPN + # Either PASSWORD or HASHNT or ssp + if "HASHNT" not in kwargs and "PASSWORD" 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 !") -def HMAC_MD5(key, data): - h = hmac.HMAC(key, hashes.MD5()) - h.update(data) - return h.finalize() + # Call parent + super(NTLMSSP_DOMAIN, self).__init__( + *args, + **kwargs, + ) + # Treat specific parameters + self.DC_IP = kwargs.pop("DC_IP", None) + if self.DC_IP is None: + # Get DC_IP from dclocator + from scapy.layers.ldap import dclocator -def MD4(x): - return Hash_MD4().digest(x) + self.DC_IP = dclocator( + self.DOMAIN_FQDN, + timeout=timeout, + debug=kwargs.get("debug", 0), + ).ip + # If logging in via Kerberos + self.ssp = ssp -def RC4K(key, data): - """Alleged RC4""" - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms - algorithm = algorithms.ARC4(key) - cipher = Cipher(algorithm, mode=None) - encryptor = cipher.encryptor() - return encryptor.update(data) + encryptor.finalize() + 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 ( + NetlogonClient, + NETLOGON_SECURE_CHANNEL_METHOD, + ) + from scapy.layers.msrpce.raw.ms_nrpc import ( + NetrLogonSamLogonWithFlags_Request, + PNETLOGON_NETWORK_INFO, + PNETLOGON_AUTHENTICATOR, + NETLOGON_LOGON_IDENTITY_INFO, + UNICODE_STRING, + STRING, + ) -# sect 3.3.2 + # Create NetlogonClient with PRIVACY + client = NetlogonClient() + client.connect_and_bind(self.DC_IP) + # Establish the Netlogon secure channel (this will bind) + try: + if self.ssp is None: + # Login via classic NetlogonSSP + client.establish_secure_channel( + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticate3, + computername=self.COMPUTER_NB_NAME, + domainname=self.DOMAIN_NB_NAME, + HashNt=self.HASHNT, + ) + else: + # Login via KerberosSSP (Windows 2025) + # TODO + client.establish_secure_channel( + mode=NETLOGON_SECURE_CHANNEL_METHOD.NetrServerAuthenticateKerberos, + ) + 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, + ) -def NTOWFv2(Passwd, User, UserDom): - """Computes the ResponseKeyNT""" - return HMAC_MD5(MD4(Passwd.encode("utf-16le")), - (User.upper() + UserDom).encode("utf-16le")) + # Get response + resp = client.sr1_req(req) + if resp and resp.status == 0: + # Success + # Validate DC authenticator + client.validate_authenticator(resp.ReturnAuthenticator.value) -def NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, NTProofStr): - return HMAC_MD5(ResponseKeyNT, NTProofStr) + # 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 _NTLMv2_ComputeResponse(ResponseKeyNT, -# ServerChallenge, ClientChallenge, Time, -# ServerName): -# """ -# Compute the NTLMv2 response : NtChallengeResponse + SessionBaseKey -# -# Remember ServerName = AvPairs -# """ -# Responserversion = b"\x01" -# HiResponserversion = b"\x01" -# temp = b"".join([Responserversion, HiResponserversion, -# Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4)]) -# NTProofStr = HMAC_MD5(ResponseKeyNT, ServerChallenge + temp) -# NtChallengeResponse = NTProofStr + temp -# SessionBaseKey = NTLMv2_ComputeSessionBaseKey(ResponseKeyNT, NTProofStr) -# return NtChallengeResponse, SessionBaseKey + 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 53743f9acdd..e8739006f14 100644 --- a/scapy/layers/ntp.py +++ b/scapy/layers/ntp.py @@ -8,24 +8,44 @@ 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 lhex from scapy.compat import orb from scapy.config import conf -import scapy.libs.six as six ############################################################################# @@ -78,14 +98,14 @@ def i2repr(self, pkt, val): return "--" val = self.i2h(pkt, val) if val < _NTP_BASETIME: - return val + 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 @@ -125,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", } @@ -210,7 +230,9 @@ def pre_dissect(self, s): return s 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): @@ -428,8 +450,8 @@ class NTPHeader(NTP): 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), @@ -590,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): """ @@ -671,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 @@ -732,95 +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) @@ -1098,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), @@ -1156,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 ) ] @@ -1778,7 +1732,7 @@ class NTPPrivate(NTP): 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/ppi.py b/scapy/layers/ppi.py index 749b467fd1b..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 diff --git a/scapy/layers/ppp.py b/scapy/layers/ppp.py index e9a29a6892a..e0f4c593d0c 100644 --- a/scapy/layers/ppp.py +++ b/scapy/layers/ppp.py @@ -38,7 +38,6 @@ XShortField, XStrLenField, ) -from scapy.libs import six class PPPoE(Packet): @@ -78,10 +77,7 @@ class PPPoED(PPPoE): ShortField("len", None)] def extract_padding(self, s): - if len(s) < 5: - return s, None - length = struct.unpack("!H", s[4:6])[0] - return s[:length], s[length:] + return s[:self.len], s[self.len:] def mysummary(self): return self.sprintf("%code%") @@ -296,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: @@ -308,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) @@ -756,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: @@ -837,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/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 1a0ec16e53f..14a1fd8787e 100644 --- a/scapy/layers/radius.py +++ b/scapy/layers/radius.py @@ -11,18 +11,57 @@ 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.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 _radius_attribute_types = { @@ -254,7 +293,7 @@ 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 @@ -396,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 @@ -526,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): @@ -571,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) @@ -1010,6 +1086,10 @@ class RadiusAttr_NAS_Port_Type(_RadiusAttrIntEnumVal): """RFC 2865""" val = 61 +# +# RADIUS attributes that are complex structures +# + class _EAPPacketField(PacketLenField): """ @@ -1065,6 +1145,75 @@ def post_dissect(self, s): 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): """ Implements the "Vendor-Specific" attribute, as described in RFC 2865. @@ -1081,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, @@ -1090,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), + ) ] @@ -1183,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) @@ -1191,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/sctp.py b/scapy/layers/sctp.py index fb7cd575702..b69d6c193ed 100644 --- a/scapy/layers/sctp.py +++ b/scapy/layers/sctp.py @@ -8,7 +8,6 @@ SCTP (Stream Control Transmission Protocol). """ -from __future__ import absolute_import import struct from scapy.compat import orb, raw @@ -160,8 +159,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 = { @@ -179,8 +183,13 @@ 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 = { @@ -192,6 +201,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", @@ -215,6 +230,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", @@ -285,6 +306,17 @@ def mysummary(self): # 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 @@ -368,6 +400,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), ] @@ -554,6 +642,63 @@ class SCTPChunkData(_SCTPChunkGuessPayload, Packet): ] +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), @@ -701,6 +846,26 @@ 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 diff --git a/scapy/layers/sixlowpan.py b/scapy/layers/sixlowpan.py index 91469092bb4..cdc7d829409 100644 --- a/scapy/layers/sixlowpan.py +++ b/scapy/layers/sixlowpan.py @@ -58,7 +58,7 @@ from scapy.fields import ( BitEnumField, BitField, - BitFixedLenField, + BitLenField, BitScalingField, ByteEnumField, ByteField, @@ -282,7 +282,7 @@ class LoWPAN_HC1(Packet): lambda pkt: pkt.nh == 1 and pkt.hc2 ), # Out of spec - BitFixedLenField("pad", 0, _get_hc1_pad) + BitLenField("pad", 0, _get_hc1_pad) ] def post_dissect(self, data): @@ -518,13 +518,18 @@ def _extract_upperaddress(pkt, source=True): # https://tools.ietf.org/html/rfc2464#section-4 return LINK_LOCAL_PREFIX[:8] + addr[:3] + b"\xff\xfe" + addr[3:] elif type(underlayer) == Dot15d4Data: - addr = underlayer.src_addr if source else underlayer.dest_addr + 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 underlayer.underlayer.fcf_destaddrmode == 3: # Extended/long + if addrmode == 3: # Extended/long tmp_ip = LINK_LOCAL_PREFIX[0:8] + addr # Turn off the bit 7. return tmp_ip[0:8] + struct.pack("B", (orb(tmp_ip[8]) ^ 0x2)) + tmp_ip[9:16] # noqa: E501 - elif underlayer.underlayer.fcf_destaddrmode == 2: # Short + elif addrmode == 2: # Short return ( LINK_LOCAL_PREFIX[0:8] + b"\x00\x00\x00\xff\xfe\x00" + @@ -1095,7 +1100,7 @@ class SixLoWPAN(Packet): @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: fb = ord(_pkt[:1]) if fb == 0x41: diff --git a/scapy/layers/smb.py b/scapy/layers/smb.py index d0a9853b4d9..676021e1d6b 100644 --- a/scapy/layers/smb.py +++ b/scapy/layers/smb.py @@ -2,11 +2,17 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi +# 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) """ @@ -18,13 +24,19 @@ from scapy.fields import ( ByteEnumField, ByteField, + ConditionalField, FieldLenField, + FieldListField, FlagsField, + IPField, LEFieldLenField, LEIntEnumField, LEIntField, + LELongField, + LEShortEnumField, LEShortField, MultipleTypeField, + PacketField, PacketLenField, PacketListField, ReversePadField, @@ -35,16 +47,29 @@ StrNullFieldUtf16, UTCTimeField, UUIDField, + XLEShortField, XStrLenField, ) -from scapy.layers.netbios import NBTSession +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 ( STATUS_ERREF, + SMB2_Compression_Transform_Header, SMB2_Header, + SMB2_Transform_Header, ) @@ -129,42 +154,56 @@ 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", 1), - LEShortField("UID", 0), - LEShortField("MID", 0)] + 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 @@ -201,7 +240,16 @@ def guess_payload_class(self, payload): else: return SMBSession_Setup_AndX_Request elif self.Command == 0x25: - return SMBNetlogon_Protocol_Response_Header + 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): @@ -213,8 +261,10 @@ def answers(self, pkt): class SMB_Dialect(Packet): name = "SMB Dialect" - fields_desc = [ByteField("BufferFormat", 0x02), - StrNullField("DialectString", "NT LM 0.12")] + fields_desc = [ + ByteField("BufferFormat", 0x02), + StrNullField("DialectString", "NT LM 0.12"), + ] def default_payload_class(self, payload): return conf.padding_layer @@ -222,12 +272,16 @@ def default_payload_class(self, payload): 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) - ] + 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) @@ -240,15 +294,14 @@ 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) - ], + [(StrNullFieldUtf16(name, default), _isUTF16)], StrNullField(name, default), ) @@ -293,59 +346,87 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): "LEVEL_II_OPLOCKS", "LOCK_AND_READ", "NT_FIND", - "res", "res", + "res", + "res", "DFS", "INFOLEVEL_PASSTHRU", "LARGE_READX", "LARGE_WRITEX", "LWIO", - "res", "res", "res", "res", "res", "res", + "res", + "res", + "res", + "res", + "res", + "res", "UNIX", "res", "COMPRESSED_DATA", - "res", "res", "res", + "res", + "res", + "res", "DYNAMIC_REAUTH", "PERSISTENT_HANDLES", - "EXTENDED_SECURITY" + "EXTENDED_SECURITY", ] # CIFS sect 2.2.4.52.2 + class SMBNegotiate_Response_NoSecurity(_SMBNegotiate_Response): name = "SMB Negotiate No-Security Response (CIFS)" - fields_desc = [ByteField("WordCount", 0x1), - LEShortField("DialectIndex", 7), - FlagsField("SecurityMode", 0x03, 8, - ["USER_SECURITY", - "ENCRYPT_PASSWORDS", - "SECURITY_SIGNATURES_ENABLED", - "SECURITY_SIGNATURES_REQUIRED"]), - LEShortField("MaxMpxCount", 50), - LEShortField("MaxNumberVC", 1), - LEIntField("MaxBufferSize", 16144), # Windows: 4356 - LEIntField("MaxRawSize", 65536), - LEIntField("SessionKey", 0x0000), - FlagsField("ServerCapabilities", 0xf3f9, -32, - _SMB_ServerCapabilities), - UTCTimeField("ServerTime", None, fmt=" 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="= 4: - if _pkt[:4] == b'\xffSMB': + if _pkt[:4] == b"\xffSMB": return SMB_Header - if _pkt[:4] == b'\xfeSMB': + 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, SMBNegociate_Protocol_Request_Header_Generic) +bind_layers(NBTSession, _SMBGeneric) +bind_layers(NBTDatagram, _SMBGeneric) diff --git a/scapy/layers/smb2.py b/scapy/layers/smb2.py index 154bc5b4dd2..7d17e828375 100644 --- a/scapy/layers/smb2.py +++ b/scapy/layers/smb2.py @@ -1,15 +1,25 @@ # 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) 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 re import struct -from scapy.config import conf +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 ( @@ -18,12 +28,15 @@ ConditionalField, FieldLenField, FieldListField, + FlagValue, FlagsField, - IntEnumField, + IP6Field, + IPField, IntField, LEIntField, LEIntEnumField, LELongField, + LenField, LEShortEnumField, LEShortField, MultipleTypeField, @@ -32,13 +45,15 @@ PacketLenField, PacketListField, ReversePadField, + ScalingField, ShortEnumField, ShortField, StrFieldUtf16, StrFixedLenField, - StrLenFieldUtf16, StrLenField, + StrLenFieldUtf16, StrNullFieldUtf16, + ThreeBytesField, UTCTimeField, UUIDField, XLEIntField, @@ -46,41 +61,146 @@ 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.netbios import NBTSession from scapy.layers.gssapi import GSSAPI_BLOB -from scapy.layers.ntlm import _NTLMPayloadField, _NTLMPayloadPacket +from scapy.layers.netbios import NBTSession +from scapy.layers.ntlm import ( + _NTLMPayloadField, + _NTLMPayloadPacket, + _NTLM_ENUM, + _NTLM_post_build, +) # 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', + 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 3.3.5.15 + [MS-ERREF] STATUS_ERREF = { 0x00000000: "STATUS_SUCCESS", + 0x00000002: "ERROR_FILE_NOT_FOUND", + 0x00000005: "ERROR_ACCESS_DENIED", 0x00000103: "STATUS_PENDING", + 0x0000010B: "STATUS_NOTIFY_CLEANUP", 0x0000010C: "STATUS_NOTIFY_ENUM_DIR", + 0x00000532: "ERROR_PASSWORD_EXPIRED", + 0x00000533: "ERROR_ACCOUNT_DISABLED", + 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", + 0xC000006D: "STATUS_LOGON_FAILURE", + 0xC000006E: "STATUS_ACCOUNT_RESTRICTION", + 0xC0000070: "STATUS_INVALID_WORKSTATION", + 0xC0000071: "STATUS_PASSWORD_EXPIRED", + 0xC0000072: "STATUS_ACCOUNT_DISABLED", 0xC000009A: "STATUS_INSUFFICIENT_RESOURCES", + 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 - 0xC000000D: "STATUS_INVALID_PARAMETER", - 0xC000000F: "STATUS_NO_SUCH_FILE", - 0xC00000BB: "STATUS_NOT_SUPPORTED", + 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", - 0x80000005: "STATUS_BUFFER_OVERFLOW", - 0x80000006: "STATUS_NO_MORE_FILES", + 0xC0000257: "STATUS_PATH_NOT_COVERED", + 0xC000035C: "STATUS_NETWORK_SESSION_EXPIRED", +} + +# 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 @@ -108,28 +228,31 @@ # 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', + 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: "LargeMTU", - 0x00000008: "MultiChannel", - 0x00000010: "PersistentHandles", - 0x00000020: "DirectoryLeasing", - 0x00000040: "Encryption", - + 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", } -# EnumField +# [MS-SMB2] 2.2.3.1.3 SMB2_COMPRESSION_ALGORITHMS = { 0x0000: "None", 0x0001: "LZNT1", @@ -138,6 +261,85 @@ 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 = { @@ -168,72 +370,223 @@ 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 @@ -264,6 +668,73 @@ class FileInternalInformation(Packet): 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 @@ -277,6 +748,10 @@ class FileStandardInformation(Packet): ShortField("Reserved", 0), ] + def default_payload_class(self, s): + return conf.padding_layer + + # [MS-FSCC] 2.4.43 FileStreamInformation @@ -285,96 +760,929 @@ class FileStreamInformation(Packet): LEIntField("Next", 0), FieldLenField("StreamNameLength", None, length_of="StreamName", fmt="Q", int(authority))[2:] + ), + SubAuthority=[int(x) for x in subauthority[1:].split("-")], + ) + + def summary(self): + 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): + 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 + ) + + +# [MS-FSCC] 2.4.2 FileAllInformation + + +class FileAllInformation(Packet): + fields_desc = [ + PacketField("BasicInformation", FileBasicInformation(), FileBasicInformation), + PacketField( + "StandardInformation", FileStandardInformation(), FileStandardInformation + ), + PacketField( + "InternalInformation", FileInternalInformation(), FileInternalInformation + ), + PacketField("EaInformation", FileEaInformation(), FileEaInformation), + PacketField( + "AccessInformation", FileAccessInformation(), FileAccessInformation + ), + PacketField( + "PositionInformation", FilePositionInformation(), FilePositionInformation + ), + PacketField("ModeInformation", FileModeInformation(), FileModeInformation), + PacketField( + "AlignmentInformation", FileAlignmentInformation(), FileAlignmentInformation + ), + PacketField("NameInformation", FILE_NAME_INFORMATION(), FILE_NAME_INFORMATION), + ] + + +# [MS-FSCC] 2.5.1 FileFsAttributeInformation + + +class FileFsAttributeInformation(Packet): + fields_desc = [ + FlagsField( + "FileSystemAttributes", + 0x00C706FF, + -32, + { + 0x02000000: "FILE_SUPPORTS_USN_JOURNAL", + 0x01000000: "FILE_SUPPORTS_OPEN_BY_FILE_ID", + 0x00800000: "FILE_SUPPORTS_EXTENDED_ATTRIBUTES", + 0x00400000: "FILE_SUPPORTS_HARD_LINKS", + 0x00200000: "FILE_SUPPORTS_TRANSACTIONS", + 0x00100000: "FILE_SEQUENTIAL_WRITE_ONCE", + 0x00080000: "FILE_READ_ONLY_VOLUME", + 0x00040000: "FILE_NAMED_STREAMS", + 0x00020000: "FILE_SUPPORTS_ENCRYPTION", + 0x00010000: "FILE_SUPPORTS_OBJECT_IDS", + 0x00008000: "FILE_VOLUME_IS_COMPRESSED", + 0x00000100: "FILE_SUPPORTS_REMOTE_STORAGE", + 0x00000080: "FILE_SUPPORTS_REPARSE_POINTS", + 0x00000040: "FILE_SUPPORTS_SPARSE_FILES", + 0x00000020: "FILE_VOLUME_QUOTAS", + 0x00000010: "FILE_FILE_COMPRESSION", + 0x00000008: "FILE_PERSISTENT_ACLS", + 0x00000004: "FILE_UNICODE_ON_DISK", + 0x00000002: "FILE_CASE_PRESERVED_NAMES", + 0x00000001: "FILE_CASE_SENSITIVE_SEARCH", + 0x04000000: "FILE_SUPPORT_INTEGRITY_STREAMS", + 0x08000000: "FILE_SUPPORTS_BLOCK_REFCOUNTING", + 0x10000000: "FILE_SUPPORTS_SPARSE_VDL", + }, + ), + LEIntField("MaximumComponentNameLength", 255), + FieldLenField( + "FileSystemNameLength", None, length_of="FileSystemName", 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, @@ -674,16 +2261,18 @@ class SMB2_Preauth_Integrity_Capabilities(Packet): 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", 1, - 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, { - "Security": 12, - }) + pay + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "SecurityBlob": 12, + }, + ) + + pay + ) bind_top_down( @@ -921,29 +2596,39 @@ def post_build(self, pkt, pay): 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_DATE", - }), + FlagsField( + "SessionFlags", + 0, + -16, + { + 0x0001: "IS_GUEST", + 0x0002: "IS_NULL", + 0x0004: "ENCRYPT_DATA", + }, + ), XLEShortField("SecurityBufferOffset", None), LEShortField("SecurityLen", None), _NTLMPayloadField( - 'Buffer', OFFSET, [ + "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__("Buffer") + or [(None, None)] + )[0][1] return super(SMB2_Session_Setup_Response, self).__getattr__(attr) def setfieldval(self, attr, val): @@ -955,16 +2640,24 @@ def setfieldval(self, attr, val): def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes - return _SMB2_post_build(self, pkt, self.OFFSET, { - "Security": 4, - }) + pay + 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 + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR ) # sect 2.2.7 @@ -972,6 +2665,7 @@ def post_build(self, pkt, pay): class SMB2_Session_Logoff_Request(_SMB2_Payload): name = "SMB2 LOGOFF Request" + Command = 0x0002 fields_desc = [ XLEShortField("StructureSize", 0x4), ShortField("reserved", 0), @@ -989,6 +2683,7 @@ class SMB2_Session_Logoff_Request(_SMB2_Payload): class SMB2_Session_Logoff_Response(_SMB2_Payload): name = "SMB2 LOGOFF Request" + Command = 0x0002 fields_desc = [ XLEShortField("StructureSize", 0x4), ShortField("reserved", 0), @@ -999,7 +2694,7 @@ class SMB2_Session_Logoff_Response(_SMB2_Payload): SMB2_Header, SMB2_Session_Logoff_Response, Command=0x0002, - Flags=1 # SMB2_FLAGS_SERVER_TO_REDIR + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR ) # sect 2.2.9 @@ -1007,26 +2702,41 @@ class SMB2_Session_Logoff_Response(_SMB2_Payload): 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"]), + FlagsField( + "Flags", + 0, + -16, + ["CLUSTER_RECONNECT", "REDIRECT_TO_OWNER", "EXTENSION_PRESENT"], + ), XLEShortField("PathBufferOffset", None), LEShortField("PathLen", None), _NTLMPayloadField( - 'Buffer', OFFSET, [ + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Path": 4, + }, + ) + + pay + ) bind_top_down( @@ -1038,119 +2748,88 @@ def post_build(self, pkt, pay): # sect 2.2.10 -SMB2_ACCESS_FLAGS = { - # sect 2.2.13.1.2 - 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", -} - - 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"}), + 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), + 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 -) +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 -) +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 -) +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) - ] + fields_desc = [XLELongField("Persistent", 0), XLELongField("Volatile", 0)] def __hash__(self): return self.Persistent + self.Volatile << 64 @@ -1158,6 +2837,7 @@ def __hash__(self): def default_payload_class(self, payload): return conf.padding_layer + # sect 2.2.14.2 @@ -1170,30 +2850,40 @@ class SMB2_CREATE_DURABLE_HANDLE_RESPONSE(Packet): class SMB2_CREATE_QUERY_MAXIMAL_ACCESS_RESPONSE(Packet): fields_desc = [ LEIntEnumField("QueryStatus", 0, STATUS_ERREF), - FlagsField("MaximalAccess", 0, -32, SMB2_ACCESS_FLAGS), + FlagsField("MaximalAccess", 0, -32, SMB2_ACCESS_FLAGS_FILE), ] class SMB2_CREATE_QUERY_ON_DISK_ID(Packet): fields_desc = [ - LELongField("DiskFileId", 0), - LELongField("VolumeId", 0), + XLELongField("DiskFileId", 0), + XLELongField("VolumeId", 0), XStrFixedLenField("Reserved", b"", length=16), ] class SMB2_CREATE_RESPONSE_LEASE(Packet): fields_desc = [ - XStrFixedLenField("LeaseKey", b"", length=16), - 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", - }), + 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), ] @@ -1201,7 +2891,7 @@ class SMB2_CREATE_RESPONSE_LEASE(Packet): class SMB2_CREATE_RESPONSE_LEASE_V2(Packet): fields_desc = [ SMB2_CREATE_RESPONSE_LEASE, - XStrFixedLenField("ParentLeaseKey", b"", length=16), + UUIDField("ParentLeaseKey", None), LEShortField("Epoch", 0), LEShortField("Reserved", 0), ] @@ -1210,11 +2900,17 @@ class SMB2_CREATE_RESPONSE_LEASE_V2(Packet): class SMB2_CREATE_DURABLE_HANDLE_RESPONSE_V2(Packet): fields_desc = [ LEIntField("Timeout", 0), - FlagsField("Flags", 0, -32, { - 0x02: "SMB2_DHANDLE_FLAG_PERSISTENT", - }), + FlagsField( + "Flags", + 0, + -32, + { + 0x02: "SMB2_DHANDLE_FLAG_PERSISTENT", + }, + ), ] + # sect 2.2.13 @@ -1272,9 +2968,14 @@ 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", - }), + FlagsField( + "Flags", + 0, + -32, + { + 0x02: "SMB2_DHANDLE_FLAG_PERSISTENT", + }, + ), ] @@ -1298,7 +2999,7 @@ class SMB2_CREATE_APP_INSTANCE_VERSION(Packet): class SMB2_Create_Context(_NTLMPayloadPacket): name = "SMB2 CREATE CONTEXT" - OFFSET = 14 + OFFSET = 16 _NTLM_PAYLOAD_FIELD_NAME = "Buffer" fields_desc = [ LEIntField("Next", None), @@ -1306,19 +3007,36 @@ class SMB2_Create_Context(_NTLMPayloadPacket): LEShortField("NameLen", None), ShortField("Reserved", 0), XLEShortField("DataBufferOffset", None), - LEShortField("DataLen", None), + LEIntField("DataLen", None), _NTLMPayloadField( - 'Buffer', OFFSET, [ - StrLenField("Name", b"", - length_from=lambda pkt: pkt.NameLen), - PacketLenField("Data", None, conf.raw_layer, - length_from=lambda pkt: pkt.DataLen), - ]), - StrLenField("pad", b"", - length_from=lambda x: ( - x.Next - - max(x.DataBufferOffset + x.DataLen, - x.NameBufferOffset + x.NameLen)) if x.Next else 0) + "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): @@ -1337,8 +3055,8 @@ def post_dissect(self, s): 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 + 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 @@ -1364,10 +3082,28 @@ def default_payload_class(self, _): def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes - return _SMB2_post_build(self, pkt, self.OFFSET, { - "Name": 4, - "Data": 10, - }) + pay + 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 @@ -1377,88 +3113,121 @@ def post_build(self, pkt, pay): 0x01: "SMB2_OPLOCK_LEVEL_II", 0x08: "SMB2_OPLOCK_LEVEL_EXCLUSIVE", 0x09: "SMB2_OPLOCK_LEVEL_BATCH", - 0xff: "SMB2_OPLOCK_LEVEL_LEASE", + 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", - }), + LEIntEnumField( + "ImpersonationLevel", + 0, + { + 0x00000000: "Anonymous", + 0x00000001: "Identification", + 0x00000002: "Impersonation", + 0x00000003: "Delegate", + }, + ), LELongField("SmbCreateFlags", 0), LELongField("Reserved", 0), - FlagsField("DesiredAccess", 0, -32, SMB2_ACCESS_FLAGS), + 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", - }), + 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, [ + "Buffer", + OFFSET, + [ StrFieldUtf16("Name", b""), - _NextPacketListField("CreateContexts", [], SMB2_Create_Context, - length_from=lambda pkt: pkt.CreateContextsLen), - ]) + _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, { - "Name": 44, - "CreateContexts": 48, - }) + pay + 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 -) +bind_top_down(SMB2_Header, SMB2_Create_Request, Command=0x0005) # sect 2.2.14 @@ -1466,54 +3235,69 @@ def post_build(self, pkt, pay): 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", - }), + 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), - ]) + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "CreateContexts": 80, + }, + ) + + pay + ) -bind_top_down( - SMB2_Header, - SMB2_Create_Response, - Command=0x0005, - Flags=1 -) +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"]), + FlagsField("Flags", 0, -16, ["SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB"]), LEIntField("Reserved", 0), - PacketField("FileId", SMB2_FILEID(), SMB2_FILEID) + PacketField("FileId", SMB2_FILEID(), SMB2_FILEID), ] @@ -1528,15 +3312,15 @@ class SMB2_Close_Request(_SMB2_Payload): 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"]), + XLEShortField("StructureSize", 0x3C), + FlagsField("Flags", 0, -16, ["SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB"]), LEIntField("Reserved", 0), ] + FileNetworkOpenInformation.fields_desc[:7] @@ -1553,40 +3337,67 @@ class SMB2_Close_Response(_SMB2_Payload): 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", 0), - FlagsField("Flags", 0, -8, { - 0x01: "SMB2_READFLAG_READ_UNBUFFERED", - 0x02: "SMB2_READFLAG_REQUEST_COMPRESSED", - }), - LEIntField("Length", 0), + 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", - }), + 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) - ]) + "Buffer", + OFFSET, + [ + StrLenField( + "ReadChannelInfo", + b"", + length_from=lambda pkt: pkt.ReadChannelInfoLen, + ) + ], + ), ] def post_build(self, pkt, pay): # type: (bytes, bytes) -> bytes - return _SMB2_post_build(self, pkt, self.OFFSET, { - "ReadChannelInfo": 44, - }) + pay + 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( @@ -1600,6 +3411,7 @@ def post_build(self, pkt, pay): class SMB2_Read_Response(_SMB2_Payload, _NTLMPayloadPacket): name = "SMB2 READ Response" + Command = 0x0008 OFFSET = 16 + 64 _NTLM_PAYLOAD_FIELD_NAME = "Buffer" fields_desc = [ @@ -1607,21 +3419,34 @@ class SMB2_Read_Response(_SMB2_Payload, _NTLMPayloadPacket): LEShortField("DataBufferOffset", None), LEIntField("DataLen", None), LEIntField("DataRemaining", 0), - FlagsField("Flags", 0, -32, { - 0x01: "SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM", - }), + FlagsField( + "Flags", + 0, + -32, + { + 0x01: "SMB2_READFLAG_RESPONSE_RDMA_TRANSFORM", + }, + ), _NTLMPayloadField( - 'Buffer', OFFSET, [ - StrLenField("Data", b"", - length_from=lambda pkt: pkt.DataLen) - ]) + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Data": 2, + }, + ) + + pay + ) bind_top_down( @@ -1637,6 +3462,7 @@ def post_build(self, pkt, pay): class SMB2_Write_Request(_SMB2_Payload, _NTLMPayloadPacket): name = "SMB2 WRITE Request" + Command = 0x0009 OFFSET = 48 + 64 _NTLM_PAYLOAD_FIELD_NAME = "Buffer" fields_desc = [ @@ -1645,34 +3471,56 @@ class SMB2_Write_Request(_SMB2_Payload, _NTLMPayloadPacket): 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", - }), + 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", - }), + 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) - ]) + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Data": 2, + "WriteChannelInfo": 40, + }, + ) + + pay + ) bind_top_down( @@ -1686,21 +3534,54 @@ def post_build(self, pkt, pay): 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), + 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_Write_Response, - Command=0x0009, - Flags=1 + SMB2_Echo_Response, + Command=0x000D, + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR ) # sect 2.2.30 @@ -1726,23 +3607,29 @@ class SMB2_Cancel_Request(_SMB2_Payload): class SMB2_IOCTL_Validate_Negotiate_Info_Request(Packet): name = "SMB2 IOCTL Validate Negotiate Info" fields_desc = ( - SMB2_Negotiate_Protocol_Request.fields_desc[4:6] + # Cap/GUID - SMB2_Negotiate_Protocol_Request.fields_desc[1:3][::-1] + # SecMod/DC - [SMB2_Negotiate_Protocol_Request.fields_desc[9]] # Dialects + 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 = { @@ -1752,52 +3639,68 @@ class SMB2_IOCTL_Request(_SMB2_Payload, _NTLMPayloadPacket): 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", - }), + 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", 0), - FlagsField("Flags", 0, -32, { - 0x00000001: "SMB2_0_IOCTL_IS_FSCTL" - }), + LEIntField("MaxOutputResponse", 65535), + FlagsField("Flags", 0, -32, {0x00000001: "SMB2_0_IOCTL_IS_FSCTL"}), LEIntField("Reserved2", 0), _NTLMPayloadField( - 'Buffer', OFFSET, [ + "Buffer", + OFFSET, + [ _SMB2_IOCTL_Request_PacketLenField( - "Input", None, conf.raw_layer, - length_from=lambda pkt: pkt.InputLen), + "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), + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Input": 24, + "Output": 36, + }, + ) + + pay + ) bind_top_down( @@ -1806,16 +3709,136 @@ def post_build(self, pkt, pay): 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 + 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 + Flags=1, # SMB2_FLAGS_SERVER_TO_REDIR ) # sect 2.2.33 @@ -1868,33 +3914,44 @@ def post_build(self, pkt, pay): 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", - }), + 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", 2048), - _NTLMPayloadField( - 'Buffer', OFFSET, [ - StrFieldUtf16("FileName", b"") - ]) + 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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "FileName": 24, + }, + ) + + pay + ) bind_top_down( @@ -1908,6 +3965,7 @@ def post_build(self, pkt, pay): 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 = [ @@ -1915,18 +3973,28 @@ class SMB2_Query_Directory_Response(_SMB2_Payload, _NTLMPayloadPacket): LEShortField("OutputBufferOffset", None), LEIntField("OutputLen", None), _NTLMPayloadField( - 'Buffer', OFFSET, [ + "Buffer", + OFFSET, + [ # TODO - StrFixedLenField("Output", b"", - length_from=lambda pkt: pkt.OutputLen) - ]) + 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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) bind_top_down( @@ -1941,27 +4009,38 @@ def post_build(self, pkt, pay): 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", - }), + 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" - }), + 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), ] @@ -1977,6 +4056,7 @@ class SMB2_Change_Notify_Request(_SMB2_Payload): 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 = [ @@ -1984,18 +4064,33 @@ class SMB2_Change_Notify_Response(_SMB2_Payload, _NTLMPayloadPacket): LEShortField("OutputBufferOffset", None), LEIntField("OutputLen", None), _NTLMPayloadField( - 'Buffer', OFFSET, [ - # TODO - StrFixedLenField("Output", b"", - length_from=lambda pkt: pkt.OutputLen) - ]) + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) bind_top_down( @@ -2013,10 +4108,13 @@ class FILE_GET_QUOTA_INFORMATION(Packet): 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)) + StrLenField( + "pad", + b"", + length_from=lambda x: ( + (x.NextEntryOffset - x.SidLength) if x.NextEntryOffset else 0 + ), + ), ] @@ -2031,63 +4129,115 @@ class SMB2_Query_Quota_Info(Packet): 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) + ( + 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) - ) + 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, { - 0x01: "SMB2_0_INFO_FILE", - 0x02: "SMB2_0_INFO_FILESYSTEM", - 0x03: "SMB2_0_INFO_SECURITY", - 0x04: "SMB2_0_INFO_QUOTA", - }), + 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, { - 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", - }), - FlagsField("Flags", 0, -32, { - 0x00000001: "SL_RESTART_SCAN", - 0x00000002: "SL_RETURN_SINGLE_ENTRY", - 0x00000004: "SL_INDEX_SPECIFIED", - }), + 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, [ - PacketListField( - "Input", None, SMB2_Query_Quota_Info, - length_from=lambda pkt: pkt.InputLen), - ]) + "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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Input": 4, + }, + ) + + pay + ) bind_top_down( @@ -2097,26 +4247,38 @@ def post_build(self, pkt, pay): ) -class SMB2_Query_Info_Response(_SMB2_Payload): +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, [ + "Buffer", + OFFSET, + [ # TODO - StrFixedLenField("Output", b"", - length_from=lambda pkt: pkt.OutputLen) - ]) + 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 + return ( + _SMB2_post_build( + self, + pkt, + self.OFFSET, + { + "Output": 2, + }, + ) + + pay + ) bind_top_down( @@ -2127,6 +4289,158 @@ def post_build(self, pkt, pay): ) +# 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 @@ -2135,13 +4449,574 @@ class SMB2_Compression_Transform_Header(Packet): fields_desc = [ StrFixedLenField("Start", b"\xfcSMB", 4), LEIntField("OriginalCompressedSegmentSize", 0x0), + LEShortEnumField("CompressionAlgorithm", 0, SMB2_COMPRESSION_ALGORITHMS), LEShortEnumField( - "CompressionAlgorithm", 0, - SMB2_COMPRESSION_ALGORITHMS + "Flags", + 0x0, + { + 0x0000: "SMB2_COMPRESSION_FLAG_NONE", + 0x0001: "SMB2_COMPRESSION_FLAG_CHAINED", + }, ), - ShortEnumField("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 index 3bf8a917ac4..360acbce824 100644 --- a/scapy/layers/smbclient.py +++ b/scapy/layers/smbclient.py @@ -5,67 +5,238 @@ """ SMB 1 / 2 Client Automaton + + +.. note:: + You will find more complete documentation for this layer over at + `SMB `_ """ -from scapy.automaton import ATMT, Automaton -from scapy.layers.ntlm import ( - NTLM_AUTHENTICATE, - NTLM_AUTHENTICATE_V2, - NTLM_NEGOTIATE, - NTLM_Client, +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.packet import Raw from scapy.volatile import RandUUID -from scapy.layers.netbios import NBTSession +from scapy.layers.dcerpc import NDRUnion, find_dcerpc_interface from scapy.layers.gssapi import ( - GSSAPI_BLOB, - SPNEGO_MechListMIC, - SPNEGO_MechType, - SPNEGO_Token, - SPNEGO_negToken, - SPNEGO_negTokenInit, - SPNEGO_negTokenResp, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_C_FLAGS, +) +from scapy.layers.msrpce.raw.ms_srvs import ( + LPSHARE_ENUM_STRUCT, + NetrShareEnum_Request, + NetrShareEnum_Response, + SHARE_INFO_1_CONTAINER, +) +from scapy.layers.ntlm import ( + NTLMSSP, ) from scapy.layers.smb import ( - SMB_Header, - SMB_Dialect, SMBNegotiate_Request, - SMBNegotiate_Response_Security, 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_Dialect, + SMB_Header, ) from scapy.layers.smb2 import ( + DirectTCP, + FileAllInformation, + FileIdBothDirectoryInformation, + SECURITY_DESCRIPTOR, + 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.smbserver import NTLM_SMB_Server +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. + """ -class NTLM_SMB_Client(NTLM_Client, Automaton): port = 445 - cls = NBTSession - kwargs_cls = { - NTLM_SMB_Server: {"CLIENT_PROVIDES_NEGOEX": True, "ECHO": True} - } + cls = DirectTCP - def __init__(self, *args, **kwargs): + def __init__(self, sock, ssp=None, *args, **kwargs): + # Various SMB client arguments self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) - self.ALLOW_SMB2 = kwargs.pop("ALLOW_SMB2", True) - self.REAL_HOSTNAME = kwargs.pop("REAL_HOSTNAME", None) - self.RETURN_SOCKET = kwargs.pop("RETURN_SOCKET", None) - self.RUN_SCRIPT = kwargs.pop("RUN_SCRIPT", None) - self.SMB2 = False - super(NTLM_SMB_Client, self).__init__(*args, **kwargs) + 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 + 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 + # [MS-SMB2] 3.2.4.1.2 + pkt.CreditRequest = pkt.CreditCharge + 1 # this code is a bit lazy + # 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): @@ -73,14 +244,8 @@ def BEGIN(self): @ATMT.condition(BEGIN) def continue_smb2(self): - kwargs = self.wait_server() - self.CONTINUE_SMB2 = kwargs.pop("CONTINUE_SMB2", False) - self.SMB2_INIT_PARAMS = kwargs.pop("SMB2_INIT_PARAMS", {}) - if self.CONTINUE_SMB2: - self.SMB2 = True - self.smb_header = NBTSession() / SMB2_Header( - PID=0xfeff - ) + if self.SMB2: # Directly started in SMB2 + self.smb_header = DirectTCP() / SMB2_Header(PID=0xFEFF) raise self.SMB2_NEGOTIATE() @ATMT.condition(BEGIN, prio=1) @@ -89,7 +254,8 @@ def send_negotiate(self): @ATMT.action(send_negotiate) def on_negotiate(self): - self.smb_header = NBTSession() / SMB_Header( + # [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" @@ -97,80 +263,37 @@ def on_negotiate(self): TID=0xFFFF, PIDLow=0xFEFF, UID=0, - MID=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 self.ALLOW_SMB2 else []) + 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" + 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.receive_condition(SENT_NEGOTIATE) - def receive_negotiate_response(self, pkt): - if SMBNegotiate_Response_Security in pkt or\ - SMBNegotiate_Response_Extended_Security in pkt or\ - SMB2_Negotiate_Protocol_Response in pkt: - self.set_srv( - "ServerTime", - pkt.ServerTime - ) - self.set_srv( - "SecurityMode", - pkt.SecurityMode - ) - if SMB2_Negotiate_Protocol_Response in pkt: - # SMB2 - self.SMB2 = True # We are using SMB2 to talk to the server - self.smb_header = NBTSession() / SMB2_Header( - PID=0xfeff - ) - else: - # SMB1 - self.set_srv( - "ServerTimeZone", - pkt.ServerTimeZone - ) - if SMBNegotiate_Response_Extended_Security in pkt or\ - SMB2_Negotiate_Protocol_Response in pkt: - # Extended SMB1 / SMB2 - negoex_tuple = self._get_token( - pkt.SecurityBlob - ) - self.set_srv( - "GUID", - pkt.GUID - ) - self.received_ntlm_token(negoex_tuple) - if SMB2_Negotiate_Protocol_Response in pkt and \ - pkt.DialectRevision in [0x02ff, 0x03ff]: - # There will be a second negotiate protocol request - self.smb_header.MID += 1 - raise self.SMB2_NEGOTIATE() - else: - raise self.NEGOTIATED() - elif SMBNegotiate_Response_Security in pkt: - # Non-extended SMB1 - self.set_srv("Challenge", pkt.Challenge) - self.set_srv("DomainName", pkt.DomainName) - self.set_srv("ServerName", pkt.ServerName) - self.received_ntlm_token((None, None, None, None)) - raise self.NEGOTIATED() - @ATMT.state() def SMB2_NEGOTIATE(self): pass @@ -181,234 +304,1580 @@ def send_negotiate_smb2(self): @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( - # Only ask for SMB 2.0.2 because it has the lowest security - Dialects=[0x0202], - Capabilities=( - "DFS+Leasing+LargeMTU+MultiChannel+" - "PersistentHandles+DirectoryLeasing+Encryption" + Dialects=self.DIALECTS, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" ), - SecurityMode=0, - ClientGUID=self.SMB2_INIT_PARAMS.get("ClientGUID", RandUUID()), ) + 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): - pass + 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, + 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 + # [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) - def should_send_setup_andx_request(self): - ntlm_tuple = self.get_token() - raise self.SENT_SETUP_ANDX_REQUEST().action_parameters(ntlm_tuple) + @ATMT.condition(NEGOTIATED, prio=1) + def should_send_session_setup_request(self, ssp_tuple): + _, _, negResult = ssp_tuple + if negResult not in [GSS_S_COMPLETE, GSS_S_CONTINUE_NEEDED]: + raise ValueError("Internal error: the SSP completed with an error.") + raise self.SENT_SESSION_REQUEST().action_parameters(ssp_tuple) @ATMT.state() - def SENT_SETUP_ANDX_REQUEST(self): + def SENT_SESSION_REQUEST(self): pass - @ATMT.action(should_send_setup_andx_request) - def send_setup_andx_request(self, ntlm_tuple): - ntlm_token, negResult, MIC, rawToken = ntlm_tuple - self.smb_header.MID = self.get("MID") - self.smb_header.TID = self.get("TID") - if self.SMB2: - self.smb_header.AsyncId = self.get("AsyncId") - self.smb_header.SessionId = self.get("SessionId") - else: - self.smb_header.UID = self.get("UID", 0) + @ATMT.action(should_send_session_setup_request) + def send_setup_session_request(self, ssp_tuple): + self.session.sspcontext, token, negResult = ssp_tuple + if self.SMB2 and negResult == 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=0, + SecurityMode=( + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" + ), ) - pkt.CreditsRequested = 33 else: # SMB1 extended - pkt = self.smb_header.copy() / \ - SMBSession_Setup_AndX_Request_Extended_Security( + pkt = ( + self.smb_header.copy() + / SMBSession_Setup_AndX_Request_Extended_Security( ServerCapabilities=( "UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS+" "DYNAMIC_REAUTH+EXTENDED_SECURITY" ), - VCNumber=self.get("VCNumber"), NativeOS=b"", - NativeLanMan=b"" - ) - pkt.SecuritySignature = self.get("SecuritySignature") - if isinstance(ntlm_token, NTLM_NEGOTIATE): - if rawToken: - pkt.SecurityBlob = ntlm_token - else: - pkt.SecurityBlob = GSSAPI_BLOB( - innerContextToken=SPNEGO_negToken( - token=SPNEGO_negTokenInit( - mechTypes=[ - # NTLMSSP - SPNEGO_MechType(oid="1.3.6.1.4.1.311.2.2.10")], # noqa: E501 - mechToken=SPNEGO_Token( - value=ntlm_token - ) - ) - ) - ) - elif isinstance(ntlm_token, (NTLM_AUTHENTICATE, - NTLM_AUTHENTICATE_V2)): - pkt.SecurityBlob = SPNEGO_negToken( - token=SPNEGO_negTokenResp( - negResult=negResult, + NativeLanMan=b"", ) ) - # Token may be missing (e.g. STATUS_MORE_PROCESSING_REQUIRED) - if ntlm_token: - pkt.SecurityBlob.token.responseToken = SPNEGO_Token( - value=ntlm_token - ) - if MIC and not self.DROP_MIC: # Drop the MIC? - pkt.SecurityBlob.token.mechListMIC = SPNEGO_MechListMIC( - value=MIC - ) + pkt.SecurityBlob = token else: - # Non-extended security + # Non-extended security. pkt = self.smb_header.copy() / SMBSession_Setup_AndX_Request( ServerCapabilities="UNICODE+NT_SMBS+STATUS32+LEVEL_II_OPLOCKS", - VCNumber=self.get("VCNumber"), NativeOS=b"", NativeLanMan=b"", OEMPassword=b"\0" * 24, - UnicodePassword=ntlm_token, - PrimaryDomain=self.get("PrimaryDomain"), - AccountName=self.get("AccountName"), - ) / SMBTree_Connect_AndX( - Flags="EXTENDED_RESPONSE", - Password=b"\0", - ) - pkt.PrimaryDomain = self.get("PrimaryDomain") - pkt.AccountName = self.get("AccountName") - pkt.Path = ( - "\\\\%s\\" % self.REAL_HOSTNAME + - self.get("Path")[2:].split("\\", 1)[1] + UnicodePassword=token, ) - pkt.Service = self.get("Service") self.send(pkt) + if self.SMB2: + # If required, compute sessions + self.session.computeSMBSessionPreauth( + bytes(pkt[SMB2_Header]), # session request + ) - @ATMT.receive_condition(SENT_SETUP_ANDX_REQUEST) - def receive_setup_andx_response(self, pkt): - if SMBSession_Null in pkt or \ - SMBSession_Setup_AndX_Response_Extended_Security in pkt or \ - SMBSession_Setup_AndX_Response in pkt: + @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 - self.set_srv("Status", pkt[SMB_Header].Status) - self.set_srv( - "UID", - pkt[SMB_Header].UID - ) - self.set_srv( - "MID", - pkt[SMB_Header].MID - ) - self.set_srv( - "TID", - pkt[SMB_Header].TID - ) if SMBSession_Null in pkt: # Likely an error - self.received_ntlm_token((None, None, None, None)) raise self.NEGOTIATED() - elif SMBSession_Setup_AndX_Response_Extended_Security in pkt or \ - SMBSession_Setup_AndX_Response in pkt: - self.set_srv( - "NativeOS", - pkt.getfieldval( - "NativeOS") - ) - self.set_srv( - "NativeLanMan", - pkt.getfieldval( - "NativeLanMan") - ) - if SMB2_Session_Setup_Response in pkt: - # SMB2 - self.set_srv("Status", pkt.Status) - self.set_srv("SecuritySignature", pkt.SecuritySignature) - self.set_srv("MID", pkt.MID) - self.set_srv("TID", pkt.TID) - self.set_srv("AsyncId", pkt.AsyncId) - self.set_srv("SessionId", pkt.SessionId) - if SMBSession_Setup_AndX_Response_Extended_Security in pkt or \ - SMB2_Session_Setup_Response in pkt: - # SMB1 extended / SMB2 - _, negResult, _, _ = ntlm_tuple = self._get_token( - pkt.SecurityBlob + # 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 negResult == 0: # Authenticated - self.received_ntlm_token(ntlm_tuple) - raise self.AUTHENTICATED() + 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: - self.received_ntlm_token(ntlm_tuple) - raise self.NEGOTIATED().action_parameters(pkt) + 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): - pass + def AUTHENTICATED(self, ssp_blob=None): + self.session.sspcontext, _, status = self.session.ssp.GSS_Init_sec_context( + self.session.sspcontext, + 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 an error.") + # Authentication was successful + self.session.computeSMBSessionKeys(IsClient=True) - @ATMT.condition(AUTHENTICATED, prio=0) + # DEV: add a condition on AUTHENTICATED with prio=0 + + @ATMT.condition(AUTHENTICATED, prio=1) def authenticated_post_actions(self): - if self.RETURN_SOCKET: - raise self.SOCKET_MODE() - if self.RUN_SCRIPT: - raise self.DO_RUN_SCRIPT() + raise self.SOCKET_BIND() - @ATMT.receive_condition(AUTHENTICATED, prio=1) - def receive_packet(self, pkt): - raise self.AUTHENTICATED().action_parameters(pkt) + # Plain SMB Socket - @ATMT.action(receive_packet) - def pass_packet(self, pkt): - self.echo(pkt) + @ATMT.state() + def SOCKET_BIND(self): + self.smb_sock_ready.set() - @ATMT.state(final=1) - def DO_RUN_SCRIPT(self): - # This is an example script, mostly unimplemented... - # Tree connect - self.smb_header.MID += 1 - self.send( - self.smb_header.copy() / - SMB2_Tree_Connect_Request( - Buffer=[('Path', '\\\\%s\\IPC$' % self.REAL_HOSTNAME)] - ) - ) - # Create srvsvc - self.smb_header.MID += 1 - pkt = self.smb_header.copy() - pkt.Command = "SMB2_CREATE" - pkt /= Raw(load=b'9\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x9f\x01\x12\x00\x00\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00x\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00\x00s\x00r\x00v\x00s\x00v\x00c\x00') # noqa: E501 - self.send(pkt) - # ... run something? - self.end() + @ATMT.condition(SOCKET_BIND) + def start_smb_socket(self): + raise self.SOCKET_MODE_SMB() @ATMT.state() - def SOCKET_MODE(self): + def SOCKET_MODE_SMB(self): pass - @ATMT.receive_condition(SOCKET_MODE) - def incoming_data_received(self, pkt): - raise self.SOCKET_MODE().action_parameters(pkt) + @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) - def receive_data(self, pkt): - self.oi.smbpipe.send(bytes(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, name="smbpipe", as_supersocket="smblink") - def outgoing_data_received(self, fd): - raise self.ESTABLISHED().action_parameters(fd.recv()) + @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) + @ATMT.action(outgoing_data_received_smb) def send_data(self, d): - self.smb_header.MID += 1 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.ins.atmt.session.sspcontext: + self.ins.atmt.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, + ) + 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, + ) + 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): + """ + Internal ObjectPipe function. + """ + # Reminder: this class is an ObjectPipe, it's just a queue. + + # Detect if DCE/RPC is fragmented. Then we must use Read/Write + is_frag = x.pfc_flags & 3 != 3 + + if self.use_ioctl and not is_frag: + # 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) + # 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 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, + port: int = 445, + timeout: int = 2, + 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, "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, + ) + 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 not isinstance(resp, NetrShareEnum_Response): + 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() + 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(spaces=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(spaces=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(spaces=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(spaces=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(spaces=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(spaces=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(spaces=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 + # Close the file + self.smbsock.close_request(fileId) + print("Cancelled.") + + @CLIUtil.addcommand(spaces=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) + print("Owner:", sd.OwnerSid.summary()) + print("Group:", sd.GroupSid.summary()) + if getattr(sd, "DACL", None): + print("DACL:") + for ace in sd.DACL.Aces: + print(" - ", ace.toSDDL()) + if getattr(sd, "SACL", None): + print("SACL:") + for ace in sd.SACL.Aces: + print(" - ", ace.toSDDL()) + + +if __name__ == "__main__": + from scapy.utils import AutoArgparse + + AutoArgparse(smbclient) diff --git a/scapy/layers/smbserver.py b/scapy/layers/smbserver.py index 502b1e97291..ff20151c7bd 100644 --- a/scapy/layers/smbserver.py +++ b/scapy/layers/smbserver.py @@ -4,107 +4,370 @@ # Copyright (C) Gabriel Potter """ -SMB 1 / 2 Server Automaton +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.layers.ntlm import ( - NTLM_CHALLENGE, - NTLM_Server, -) +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.netbios import NBTSession +from scapy.layers.dcerpc import ( + DCERPC_Transport, + NDRUnion, +) from scapy.layers.gssapi import ( - GSSAPI_BLOB, - SPNEGO_MechListMIC, - SPNEGO_MechType, - SPNEGO_Token, - SPNEGO_negToken, - SPNEGO_negTokenInit, - SPNEGO_negTokenResp, + 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 ( - SMB_Header, SMBNegotiate_Request, - SMBNegotiate_Response_Security, 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.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, + SECURITY_DESCRIPTOR, + 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_IOCTL_Request, - SMB2_Error_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 NTLM_SMB_Server(NTLM_Server, Automaton): - port = 445 - cls = NBTSession - def __init__(self, *args, **kwargs): - self.CLIENT_PROVIDES_NEGOEX = kwargs.pop("CLIENT_PROVIDES_NEGOEX", False) - self.ECHO = kwargs.pop("ECHO", False) +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", False) - self.PASS_NEGOEX = kwargs.pop("PASS_NEGOEX", False) + self.GUEST_LOGIN = kwargs.pop("GUEST_LOGIN", None) self.EXTENDED_SECURITY = kwargs.pop("EXTENDED_SECURITY", True) - self.ALLOW_SMB2 = kwargs.pop("ALLOW_SMB2", True) - self.REQUIRE_SIGNATURE = kwargs.pop("REQUIRE_SIGNATURE", False) - self.REAL_HOSTNAME = kwargs.pop( - "REAL_HOSTNAME", None - ) # Compulsory for SMB1 !!! - assert self.ALLOW_SMB2 or self.REAL_HOSTNAME, "SMB1 requires REAL_HOSTNAME !" - # Session information + 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.Dialect = None - self.GUID = False - super(NTLM_SMB_Server, self).__init__(*args, **kwargs) + 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): - if self.Dialect and self.SigningSessionKey: - if isinstance(pkt.payload, SMB2_Header): - # Sign SMB2 ! - smb = pkt[SMB2_Header] - smb.Flags += "SMB2_FLAGS_SIGNED" - smb.sign(self.Dialect, self.SigningSessionKey) - return super(NTLM_SMB_Server, self).send(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 - assert ( - not self.ECHO or self.cli_atmt - ), "Cannot use ECHO without binding to a client !" @ATMT.receive_condition(BEGIN) def received_negotiate(self, pkt): if SMBNegotiate_Request in pkt: - if self.cli_atmt: - self.start_client() 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 - if self.cli_atmt: - self.start_client( - CONTINUE_SMB2=True, SMB2_INIT_PARAMS={"ClientGUID": pkt.ClientGUID} - ) raise self.NEGOTIATED().action_parameters(pkt) @ATMT.action(received_negotiate_smb2_begin) @@ -113,27 +376,26 @@ def on_negotiate_smb2_begin(self, pkt): @ATMT.action(received_negotiate) def on_negotiate(self, pkt): - if self.CLIENT_PROVIDES_NEGOEX: - negoex_token, _, _, _ = self.get_token(negoex=True) - else: - negoex_token = None - if not self.SMB2 and not self.get("GUID", 0): - self.EXTENDED_SECURITY = False + 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.sort() - DialectRevision = DialectRevisions[0] - if DialectRevision >= 0x300: # SMB3 - raise ValueError("SMB client requires SMB3 which is unimplemented.") + 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.ALLOW_SMB2: + 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: @@ -145,28 +407,23 @@ def on_negotiate(self, pkt): pass else: DialectIndex = DialectIndexes.index(b"NT LM 0.12") - else: - # Enforce SMB1 - DialectIndex = DialectIndexes.index(b"NT LM 0.12") if DialectRevision and DialectRevision & 0xFF != 0xFF: # Version isn't SMB X.??? - self.Dialect = DialectRevision + self.session.Dialect = DialectRevision cls = None if self.SMB2: # SMB2 cls = SMB2_Negotiate_Protocol_Response - self.smb_header = NBTSession() / SMB2_Header( - CreditsRequested=1, + self.smb_header = DirectTCP() / SMB2_Header( + Flags="SMB2_FLAGS_SERVER_TO_REDIR", + CreditRequest=1, CreditCharge=1, ) if SMB2_Negotiate_Protocol_Request in pkt: - self.smb_header.MID = pkt.MID - self.smb_header.TID = pkt.TID - self.smb_header.AsyncId = pkt.AsyncId - self.smb_header.SessionId = pkt.SessionId + self.update_smbheader(pkt) else: # SMB1 - self.smb_header = NBTSession() / SMB_Header( + self.smb_header = DirectTCP() / SMB_Header( Flags="REPLY+CASE_INSENSITIVE+CANONICALIZED_PATHS", Flags2=( "LONG_NAMES+EAS+NT_STATUS+SMB_SECURITY_SIGNATURE+" @@ -181,19 +438,104 @@ def on_negotiate(self, pkt): cls = SMBNegotiate_Response_Extended_Security else: cls = SMBNegotiate_Response_Security - if self.SMB2: - # SMB2 + 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=3 - if self.REQUIRE_SIGNATURE - else self.get("SecurityMode", bool(self.IDENTITIES)), - ServerTime=self.get("ServerTime", time.time() + 11644473600), + 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( @@ -204,62 +546,83 @@ def on_negotiate(self, pkt): "LWIO+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX" ), SecurityMode=( - 3 - if self.REQUIRE_SIGNATURE - else self.get("SecurityMode", bool(self.IDENTITIES)) + "SIGNING_ENABLED+SIGNING_REQUIRED" + if self.session.SigningRequired + else "SIGNING_ENABLED" ), - ServerTime=self.get("ServerTime"), - ServerTimeZone=self.get("ServerTimeZone"), + 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 = GSSAPI_BLOB( - innerContextToken=SPNEGO_negToken( - token=SPNEGO_negTokenInit( - mechTypes=[ - # NEGOEX - Optional. See below - # NTLMSSP - SPNEGO_MechType(oid="1.3.6.1.4.1.311.2.2.10") - ], - ) - ) - ) - self.GUID = resp.GUID = self.get("GUID", RandUUID()._fix()) - if self.PASS_NEGOEX: # NEGOEX handling - # 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.innerContextToken.token.mechTypes.insert( - 0, - # NEGOEX - SPNEGO_MechType(oid="1.3.6.1.4.1.311.2.2.30"), - ) - resp.SecurityBlob.innerContextToken.token.mechToken = SPNEGO_Token( - value=negoex_token - ) # noqa: E501 + resp.SecurityBlob = spnego_token else: # Non-extended SMB1 - resp.Challenge = self.get("Challenge") - resp.DomainName = self.get("DomainName") - resp.ServerName = self.get("ServerName") + # 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" + 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 @@ -276,145 +639,131 @@ def on_negotiate_smb2(self, 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 + 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 - ntlm_tuple = self._get_token(pkt.SecurityBlob) + ssp_blob = pkt.SecurityBlob else: # Non-extended - self.set_cli("AccountName", pkt.AccountName) - self.set_cli("PrimaryDomain", pkt.PrimaryDomain) - self.set_cli("Path", pkt.Path) - self.set_cli("Service", pkt.Service) - ntlm_tuple = self._get_token( - pkt[SMBSession_Setup_AndX_Request].UnicodePassword - ) - self.set_cli("VCNumber", pkt.VCNumber) - self.set_cli("SecuritySignature", pkt.SecuritySignature) - self.set_cli("UID", pkt.UID) - self.set_cli("MID", pkt.MID) - self.set_cli("TID", pkt.TID) - self.received_ntlm_token(ntlm_tuple) - raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt) + 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 - ntlm_tuple = self._get_token(pkt.SecurityBlob) - self.set_cli("SecuritySignature", pkt.SecuritySignature) - self.set_cli("MID", pkt.MID) - self.set_cli("TID", pkt.TID) - self.set_cli("AsyncId", pkt.AsyncId) - self.set_cli("SessionId", pkt.SessionId) - self.set_cli("SecurityMode", pkt.SecurityMode) - self.received_ntlm_token(ntlm_tuple) - raise self.RECEIVED_SETUP_ANDX_REQUEST().action_parameters(pkt) + 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): - ntlm_token, negResult, MIC, rawToken = ntlm_tuple = self.get_token() - # rawToken == whether the GSSAPI ASN.1 wrapper is used - # typically, when a SMB session **falls back** to NTLM, no - # wrapper is used - if ( - SMBSession_Setup_AndX_Request_Extended_Security in pkt or - SMBSession_Setup_AndX_Request in pkt or - SMB2_Session_Setup_Request in pkt - ): + 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 - self.smb_header.MID = self.get("MID", self.smb_header.MID + 1) - self.smb_header.TID = self.get("TID", self.smb_header.TID) - if self.smb_header.Flags.SMB2_FLAGS_ASYNC_COMMAND: - self.smb_header.AsyncId = self.get( - "AsyncId", self.smb_header.AsyncId - ) - self.smb_header.SessionId = self.get("SessionId", 0x0001000000000015) + resp = self.smb_header.copy() / SMB2_Session_Setup_Response() + # Set security blob (if any) + resp.SecurityBlob = tok else: # SMB1 - self.smb_header.UID = self.get("UID") - self.smb_header.MID = self.get("MID") - self.smb_header.TID = self.get("TID") - if ntlm_tuple == (None, None, None, None): - # Error + 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: - # SMB2 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 - resp = self.smb_header.copy() / SMBSession_Null() - resp.Status = self.get("Status", 0xC000006D) - 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: - # SMB2 - resp = self.smb_header.copy() / SMB2_Session_Setup_Response() - if self.GUEST_LOGIN: - resp.SessionFlags = "IS_GUEST" - if self.ANONYMOUS_LOGIN: - resp.SessionFlags = "IS_NULL" - else: - # SMB1 extended - resp = ( - self.smb_header.copy() / - SMBSession_Setup_AndX_Response_Extended_Security( - NativeOS=self.get("NativeOS"), - NativeLanMan=self.get("NativeLanMan"), - ) - ) - if self.GUEST_LOGIN: - resp.Action = "SMB_SETUP_GUEST" - if not ntlm_token: - # No token (e.g. accepted) - resp.SecurityBlob = SPNEGO_negToken( - token=SPNEGO_negTokenResp( - negResult=negResult, - ) - ) - if MIC and not self.DROP_MIC: # Drop the MIC? - resp.SecurityBlob.token.mechListMIC = SPNEGO_MechListMIC( - value=MIC - ) # noqa: E501 - if negResult == 0: - self.authenticated = True - elif isinstance(ntlm_token, NTLM_CHALLENGE) and not rawToken: - resp.SecurityBlob = SPNEGO_negToken( - token=SPNEGO_negTokenResp( - negResult=negResult or 1, - supportedMech=SPNEGO_MechType( - # NTLMSSP - oid="1.3.6.1.4.1.311.2.2.10" - ), - responseToken=SPNEGO_Token(value=ntlm_token), - ) + # SMB1 extended + resp = ( + self.smb_header.copy() + / SMBSession_Setup_AndX_Response_Extended_Security( + NativeOS="Windows 4.0", + NativeLanMan="Windows 4.0", ) - else: - # Token is raw or unknown - resp.SecurityBlob = ntlm_token - elif SMBSession_Setup_AndX_Request in pkt: - # Non-extended - resp = self.smb_header.copy() / SMBSession_Setup_AndX_Response( - NativeOS=self.get("NativeOS"), - NativeLanMan=self.get("NativeLanMan"), ) - resp.Status = self.get( - "Status", 0x0 if self.authenticated else 0xC0000016 + 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() @@ -424,16 +773,17 @@ def AUTHENTICATED(self): """Dev: overload this""" pass - @ATMT.condition(AUTHENTICATED, prio=1) - def should_end(self): - if not self.ECHO: - # Close connection - raise self.END() + # DEV: add a condition on AUTHENTICATED with prio=0 - @ATMT.receive_condition(AUTHENTICATED, prio=2) - def receive_packet_echo(self, pkt): - if self.ECHO: - raise self.AUTHENTICATED().action_parameters(pkt) + @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") @@ -441,63 +791,1035 @@ def _ioctl_error(self, Status="STATUS_NOT_SUPPORTED"): pkt.Command = "SMB2_IOCTL" self.send(pkt) - @ATMT.action(receive_packet_echo) - def pass_packet(self, pkt): - # Pre-process some of the data if possible - pkt.show() - if not self.SMB2: - # SMB1 - no signature (disabled by our implementation) - if SMBTree_Connect_AndX in pkt and self.REAL_HOSTNAME: - pkt.LENGTH = None - pkt.ByteCount = None - pkt.Path = ( - "\\\\%s\\" % self.REAL_HOSTNAME + pkt.Path[2:].split("\\", 1)[1] + @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())], ) - else: - self.smb_header.MID += 1 - # SMB2 - if SMB2_IOCTL_Request in pkt and pkt.CtlCode == 0x00140204: - # 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.SigningSessionKey: - # We have the session key ! - pkt = 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.Dialect, - SecurityMode=3 - if self.REQUIRE_SIGNATURE - else self.get( - "SecurityMode", bool(self.IDENTITIES) - ), + ) + 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 b691a07bc27..d6a8bf69e56 100644 --- a/scapy/layers/snmp.py +++ b/scapy/layers/snmp.py @@ -7,12 +7,11 @@ 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): diff --git a/scapy/layers/spnego.py b/scapy/layers/spnego.py new file mode 100644 index 00000000000..3afb73268ed --- /dev/null +++ b/scapy/layers/spnego.py @@ -0,0 +1,1158 @@ +# 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 struct +from uuid import UUID + +from scapy.asn1.asn1 import ( + ASN1_OID, + ASN1_STRING, + ASN1_Codecs, +) +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_PACKET, + ASN1F_SEQUENCE, + ASN1F_SEQUENCE_OF, + ASN1F_STRING, + ASN1F_optional, +) +from scapy.asn1packet import ASN1_Packet +from scapy.base_classes import Net +from scapy.fields import ( + FieldListField, + LEIntEnumField, + LEIntField, + LELongEnumField, + LELongField, + LEShortField, + MultipleTypeField, + PacketField, + PacketListField, + StrField, + StrFixedLenField, + UUIDEnumField, + UUIDField, + XStrFixedLenField, + XStrLenField, +) +from scapy.packet import Packet, bind_layers +from scapy.utils import ( + valid_ip, + valid_ip6, +) + +from scapy.layers.inet6 import Net6 +from scapy.layers.gssapi import ( + GSSAPI_BLOB, + GSSAPI_BLOB_SIGNATURE, + GSS_C_FLAGS, + GSS_C_NO_CHANNEL_BINDINGS, + GSS_S_BAD_MECH, + GSS_S_COMPLETE, + GSS_S_CONTINUE_NEEDED, + GSS_S_FLAGS, + GssChannelBindings, + SSP, + _GSSAPI_OIDS, + _GSSAPI_SIGNATURE_OIDS, +) + +# SSP Providers +from scapy.layers.kerberos import ( + Kerberos, + KerberosSSP, + _parse_upn, +) +from scapy.layers.ntlm import ( + NTLMSSP, + MD4le, + NEGOEX_EXCHANGE_NTLM, + NTLM_Header, + _NTLMPayloadField, + _NTLMPayloadPacket, +) + +# Typing imports +from typing import ( + Dict, + 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("value", "") + + +_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 +} + + +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) + 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 + return dat, 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( + "negResult", + 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 + """ + 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("= 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 2f4d77dad08..a5ccb12af66 100644 --- a/scapy/layers/tftp.py +++ b/scapy/layers/tftp.py @@ -5,20 +5,31 @@ """ 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.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 @@ -134,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 @@ -222,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 @@ -302,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 @@ -379,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 = {} @@ -411,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 diff --git a/scapy/layers/tls/__init__.py b/scapy/layers/tls/__init__.py index ab08adf001e..ecdcac9a096 100644 --- a/scapy/layers/tls/__init__.py +++ b/scapy/layers/tls/__init__.py @@ -54,6 +54,8 @@ - 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): @@ -76,16 +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: - Define several Certificate Transparency objects. - - Add the extended master secret and encrypt-then-mac logic. - - Mostly unused features : DSS, fixed DH, SRP, char2 curves... """ diff --git a/scapy/layers/tls/automaton.py b/scapy/layers/tls/automaton.py index d230863cbc0..d1dbe149d66 100644 --- a/scapy/layers/tls/automaton.py +++ b/scapy/layers/tls/automaton.py @@ -64,6 +64,11 @@ 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) @@ -206,16 +211,23 @@ 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 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. diff --git a/scapy/layers/tls/automaton_cli.py b/scapy/layers/tls/automaton_cli.py index b191262aeed..5e442d3f198 100644 --- a/scapy/layers/tls/automaton_cli.py +++ b/scapy/layers/tls/automaton_cli.py @@ -30,13 +30,12 @@ from scapy.all import * from scapy.layers.http import * - from scapy.layers.tls 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 @@ -49,10 +48,16 @@ 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, TLS_Ext_PSKKeyExchangeModes, \ - TLS_Ext_ServerName, ServerName +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, \ @@ -72,10 +77,14 @@ _tls_cipher_suites_cls from scapy.layers.tls.crypto.groups import _tls_named_groups from scapy.layers.tls.crypto.hkdf import TLS13_HKDF -from scapy.libs import six from scapy.packet import Raw from scapy.compat import bytes_encode +# Typing imports +from typing import ( + Optional, +) + class TLSClientAutomaton(_TLSAutomaton): """ @@ -89,14 +98,18 @@ class TLSClientAutomaton(_TLSAutomaton): :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 handshake, should the server ask for client authentication. - :param client_hello: may hold a TLSClientHello or SSLv2ClientHello to be - sent to the server. This is particularly useful for extensions - tweaking. If not set, a default is populated accordingly. + :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", etc.) It may be overridden by the previous + "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. @@ -110,8 +123,10 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=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, @@ -126,7 +141,8 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, self.local_port = None self.socket = None - if isinstance(client_hello, (TLSClientHello, TLS13ClientHello)): + if isinstance(client_hello, (SSLv2ClientHello, TLSClientHello, + TLS13ClientHello)): self.client_hello = client_hello else: self.client_hello = None @@ -141,20 +157,41 @@ 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 @@ -166,6 +203,7 @@ def parse_args(self, server="127.0.0.1", dport=4433, server_name=None, 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: @@ -179,14 +217,16 @@ 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: @@ -293,17 +333,19 @@ def should_add_ClientHello(self): if self.client_hello: p = self.client_hello else: - p = TLSClientHello() - ext = [] - # Add TLS_Ext_SignatureAlgorithms for TLS 1.2 ClientHello - if self.cur_session.advertised_tls_version == 0x0303: - ext += [TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsa"])] - # Add TLS_Ext_ServerName - if self.server_name: - ext += TLS_Ext_ServerName( - servernames=[ServerName(servername=self.server_name)] - ) - p.ext = ext + 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() @@ -424,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() @@ -450,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() @@ -488,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() @@ -591,7 +633,7 @@ def add_ClientData(self): raise self.ADDED_CLIENTDATA() raise self.WAITING_SERVERDATA() else: - 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() if data == b"quit": @@ -632,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() @@ -639,57 +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): if self.is_atmt_socket: # Socket mode self.oi.tls.send(p.data) else: - print("> Received: %r" % p.data) + self.vprint("Received: %r" % p.data) elif isinstance(p, TLSAlert): - print("> Received: %r" % p) + self.vprint("Received: %r" % p) raise self.CLOSE_NOTIFY() - elif isinstance(p, TLS13NewSessionTicket): - print("> Received: %r " % p) - # 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", p.noncelen)) - f.write(p.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", p.ticket_age_add)) - f.write(struct.pack("!H", p.ticketlen)) - f.write(self.cur_session.client_session_ticket) else: - print("> Received: %r" % p) + self.vprint("Received: %r" % p) self.buffer_in = self.buffer_in[1:] raise self.HANDLED_SERVERDATA() @@ -806,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()) @@ -845,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() @@ -929,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: @@ -972,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:] @@ -1011,9 +1097,6 @@ 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", "x448"] - if conf.crypto_valid_advanced: - supported_groups.append("x25519") self.add_record(is_tls13=False) if self.client_hello: p = self.client_hello @@ -1025,22 +1108,32 @@ def tls13_should_add_ClientHello(self): p = TLS13ClientHello(ciphers=c) ext = [] - ext += TLS_Ext_SupportedVersion_CH(versions=["TLS 1.3"]) + 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=supported_groups) + 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") - # RFC844, section 4.2.11. + # 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 @@ -1079,16 +1172,12 @@ def tls13_should_add_ClientHello(self): ext += TLS_Ext_PreSharedKey_CH(identities=[psk_id], binders=[psk_binder_entry]) else: - ext += TLS_Ext_SupportedGroups(groups=supported_groups) + 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=["sha256+rsaepss", - "sha256+rsa"]) - # Add TLS_Ext_ServerName - if self.server_name: - ext += TLS_Ext_ServerName( - servernames=[ServerName(servername=self.server_name)] + ext += TLS_Ext_SignatureAlgorithms( + sig_algs=self.supported_signature_algorithms, ) p.ext = ext self.add_msg(p) @@ -1143,13 +1232,20 @@ def tls13_should_handle_AlertMessage_(self): self.raise_on_packet(TLSAlert, 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=4) + @ATMT.condition(TLS13_RECEIVED_SERVERFLIGHT1, prio=5) def tls13_missing_ServerHello(self): raise self.MISSING_SERVERHELLO() @@ -1161,77 +1257,28 @@ 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: - raise self.CLOSE_NOTIFY() - - ext = [] - ext += TLS_Ext_SupportedVersion_CH(versions=[_tls_version[selected_version]]) # noqa: E501 - - if s.tls13_psk_secret: - if self.tls13_psk_mode == "psk_dhe_ke": - ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_dhe_ke"), - ext += TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]) # noqa: E501 - ext += TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]) # noqa: E501 - else: - ext += TLS_Ext_PSKKeyExchangeModes(kxmodes="psk_ke") + self.advertised_tls_version = e.version - if s.client_session_ticket: - - # XXX Retrieve parameters from first ClientHello... - 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 - - # 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: - hkdf = TLS13_HKDF("sha256") - hash_len = hkdf.hash.digest_size - psk_id = PSKIdentity(identity='Client_identity') - 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]) + 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() - else: - ext += TLS_Ext_SupportedGroups(groups=[_tls_named_groups[selected_group]]) # noqa: E501 - ext += TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group=selected_group)]) # noqa: E501 - ext += TLS_Ext_SignatureAlgorithms(sig_algs=["sha256+rsaepss"]) + if not self.advertised_tls_version: + self.vprint("No supported_versions found in TLS 1.3 Hello Retry Request!") + raise self.CLOSE_NOTIFY() - 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): @@ -1336,21 +1383,31 @@ def tls13_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 TLS13CertificateRequest not in hs_msg: - raise self.TLS13_ADDED_CLIENTCERTIFICATE() - # return + 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)) + 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): """ @@ -1360,11 +1417,6 @@ def tls13_should_add_ClientCertificateVerify(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 (TLS13CertificateRequest not in hs_msg or - self.mycert is None or - self.mykey is None): - return self.tls13_should_add_ClientFinished() self.add_msg(TLSCertificateVerify()) raise self.TLS13_ADDED_CERTIFICATEVERIFY() @@ -1388,6 +1440,10 @@ 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'.") diff --git a/scapy/layers/tls/automaton_srv.py b/scapy/layers/tls/automaton_srv.py index 4f194e0a3ee..d0ad402142b 100644 --- a/scapy/layers/tls/automaton_srv.py +++ b/scapy/layers/tls/automaton_srv.py @@ -11,13 +11,13 @@ 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 @@ -30,17 +30,28 @@ 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, \ - TLS_Ext_SignatureAlgorithms, TLS_Ext_PSKKeyExchangeModes, \ - TLS_Ext_EarlyDataIndicationTicket -from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_SH, \ - KeyShareEntry, TLS_Ext_KeyShare_HRR, TLS_Ext_PreSharedKey_CH, \ - TLS_Ext_PreSharedKey_SH +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, \ @@ -55,8 +66,17 @@ TLSApplicationData 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, \ - get_usable_ciphersuites +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 @@ -89,7 +109,8 @@ 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, @@ -120,36 +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 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 - for (group_id, ng) in _tls_named_groups.items(): - if ng == curve: - self.curve = group_id + + 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) @@ -212,6 +262,7 @@ def BIND(self): @ATMT.state() def SOCKET_CLOSED(self): + self.socket.close() raise self.WAITING_CLIENT() @ATMT.state() @@ -263,12 +314,22 @@ def RECEIVED_CLIENTFLIGHT1(self): 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): """ @@ -278,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() @@ -305,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() @@ -564,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() @@ -577,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"), @@ -717,6 +792,8 @@ def tls13_should_add_ServerHello(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] group = next(iter(self.cur_session.tls13_client_pubshares)) @@ -814,6 +891,22 @@ def tls13_ADDED_CERTIFICATE(self): @ATMT.condition(tls13_ADDED_CERTIFICATE) def tls13_should_add_CertificateVerifiy(self): 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() @@ -1170,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) @@ -1180,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) @@ -1208,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() @@ -1218,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))) diff --git a/scapy/layers/tls/basefields.py b/scapy/layers/tls/basefields.py index 6c4dc711cbe..2862c896e0a 100644 --- a/scapy/layers/tls/basefields.py +++ b/scapy/layers/tls/basefields.py @@ -11,7 +11,6 @@ import struct from scapy.fields import ByteField, ShortEnumField, ShortField, StrField -import scapy.libs.six as six from scapy.compat import orb _tls_type = {20: "change_cipher_spec", @@ -168,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 7095eab29c4..b38f52ca073 100644 --- a/scapy/layers/tls/cert.py +++ b/scapy/layers/tls/cert.py @@ -10,48 +10,79 @@ Supports both RSA and ECDSA objects. 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 subject public +key info of a 'cert' and then resign it with whatever 'key':: -For instance, here is what you could do in order to modify the serial of -'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") - f = open('cert.der') - c = X509_Cert(f.read()) +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('key.pem') - new_x509_cert = k.resignCert(c) + 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") No need for obnoxious openssl tweaking anymore. :) """ -from __future__ import absolute_import -from __future__ import print_function import base64 import os import time from scapy.config import conf, crypto_validator -import scapy.libs.six as six from scapy.error import warning from scapy.utils import binrepr from scapy.asn1.asn1 import ASN1_BIT_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.x509 import ( + ECDSAPrivateKey_OpenSSL, + ECDSAPrivateKey, + ECDSAPublicKey, + EdDSAPublicKey, + EdDSAPrivateKey, + RSAPrivateKey_OpenSSL, + RSAPrivateKey, + RSAPublicKey, + X509_Cert, + X509_CRL, + X509_SubjectPublicKeyInfo, +) from scapy.layers.tls.crypto.pkcs1 import pkcs_os2ip, _get_hash, \ _EncryptAndVerifyRSA, _DecryptAndSignRSA from scapy.compat import raw, bytes_encode + if conf.crypto_valid: + from cryptography.exceptions import InvalidSignature from cryptography.hazmat.backends import default_backend 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 + + # 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 @@ -69,11 +100,11 @@ def der2pem(der_string, obj="UNKNOWN"): """Convert DER octet string to PEM format (with optional header)""" # Encode a byte string in PEM format. Header advertises type. - pem_string = ("-----BEGIN %s-----\n" % obj).encode() - base64_string = base64.b64encode(der_string) + 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)] # noqa: E501 - pem_string += b'\n'.join(chunks) - pem_string += ("\n-----END %s-----\n" % obj).encode() + pem_string += '\n'.join(chunks) + pem_string += "\n-----END %s-----\n" % obj return pem_string @@ -114,16 +145,9 @@ def split_pem(s): 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): @@ -156,20 +180,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_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 @@ -187,7 +206,15 @@ class _PubKeyFactory(_PKIObjMaker): 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) @@ -209,42 +236,42 @@ 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): @@ -255,6 +282,39 @@ def verifyCert(self, cert): sigVal = raw(cert.signatureValue) return self.verify(raw(tbsCert), 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()) + class PubKeyRSA(PubKey, _EncryptAndVerifyRSA): """ @@ -266,9 +326,18 @@ 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)) @@ -276,6 +345,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 @@ -291,10 +363,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 @@ -324,11 +392,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 @@ -341,6 +410,37 @@ 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 # ################ @@ -351,7 +451,7 @@ class _PrivKeyFactory(_PKIObjMaker): 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); @@ -368,43 +468,51 @@ def __call__(cls, key_path=None): obj.fill_and_store() return obj - obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) - multiPEM = False + # 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() + )) + else: + # Load from file + obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE) + 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 @@ -415,10 +523,11 @@ def __bytes__(self): __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"): @@ -456,8 +565,35 @@ def verifyCert(self, cert): sigVal = raw(cert.signatureValue) return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs') + @property + def pem(self): + return der2pem(self.der, self.marker) -class PrivKeyRSA(PrivKey, _EncryptAndVerifyRSA, _DecryptAndSignRSA): + @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()) + + +class PrivKeyRSA(PrivKey, _DecryptAndSignRSA): """ Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py Use the 'key' attribute to access original object. @@ -473,10 +609,19 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, # 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: @@ -487,14 +632,18 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None, 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 @@ -510,9 +659,14 @@ def import_from_asn1pkt(self, privkey): 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) @@ -527,28 +681,53 @@ class PrivKeyECDSA(PrivKey): 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.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(raw(privkey), None, + backend=default_backend()) # noqa: E501 + 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 # ################ @@ -558,19 +737,29 @@ 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)): +class Cert(metaclass=_CertMaker): """ Wrapper for the X509_Cert from layers/x509.py. Use the 'x509Cert' attribute to access original object. @@ -657,6 +846,37 @@ def encrypt(self, msg, t="pkcs", h="sha256", mgf=None, L=None): 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 getSignatureHash(self): + """ + Return the hash used by the 'signatureAlgorithm' + """ + tbsCert = self.tbsCertificate + sigAlg = tbsCert.signature + h = hash_by_oid[sigAlg.algorithm.val] + return _get_hash(h) + + 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): """ Based on the value of notAfter field, returns the number of @@ -719,15 +939,28 @@ def isRevoked(self, crl_list): return self.serial in (x[0] for x in c.revoked_cert_serials) return False - def export(self, filename, fmt="DER"): + @property + def pem(self): + return der2pem(self.der, self.marker) + + @property + def der(self): + return bytes(self.x509Cert) + + 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) @@ -752,14 +985,14 @@ 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. diff --git a/scapy/layers/tls/crypto/cipher_aead.py b/scapy/layers/tls/crypto/cipher_aead.py index eed8bae0edf..b83ddccbe66 100644 --- a/scapy/layers/tls/crypto/cipher_aead.py +++ b/scapy/layers/tls/crypto/cipher_aead.py @@ -13,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.libs.six as six if conf.crypto_valid: from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes # noqa: E501 @@ -58,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 @@ -145,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"): @@ -185,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) @@ -248,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 @@ -317,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"): @@ -343,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 c4f73ced6a0..4323b97e77b 100644 --- a/scapy/layers/tls/crypto/cipher_block.py +++ b/scapy/layers/tls/crypto/cipher_block.py @@ -12,17 +12,26 @@ from scapy.config import conf from scapy.layers.tls.crypto.common import CipherError -import scapy.libs.six as six if conf.crypto_valid: from cryptography.utils import ( CryptographyDeprecationWarning, ) - from cryptography.hazmat.primitives.ciphers import (Cipher, algorithms, modes, # noqa: E501 - BlockCipherAlgorithm, - CipherAlgorithm) - from cryptography.hazmat.backends.openssl.backend import (backend, - GetCipherByName) + 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 _tls_block_cipher_algs = {} @@ -43,7 +52,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): @@ -55,17 +64,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": @@ -83,7 +97,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() @@ -96,7 +110,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() @@ -134,8 +148,14 @@ class Cipher_CAMELLIA_256_CBC(Cipher_CAMELLIA_128_CBC): _sslv2_block_cipher_algs = {} if conf.crypto_valid: + class Cipher_DES_ECB(_BlockCipher): + pc_cls = decrepit_algorithms.TripleDES + pc_cls_mode = modes.ECB + block_size = 8 + key_len = 8 + class Cipher_DES_CBC(_BlockCipher): - pc_cls = algorithms.TripleDES + pc_cls = decrepit_algorithms.TripleDES pc_cls_mode = modes.CBC block_size = 8 key_len = 8 @@ -153,7 +173,7 @@ 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 @@ -167,13 +187,13 @@ class Cipher_3DES_EDE_CBC(_BlockCipher): category=CryptographyDeprecationWarning) class Cipher_IDEA_CBC(_BlockCipher): - pc_cls = algorithms.IDEA + pc_cls = decrepit_algorithms.IDEA pc_cls_mode = modes.CBC block_size = 8 key_len = 16 class Cipher_SEED_CBC(_BlockCipher): - pc_cls = algorithms.SEED + pc_cls = decrepit_algorithms.SEED pc_cls_mode = modes.CBC block_size = 16 key_len = 16 @@ -192,24 +212,41 @@ class Cipher_SEED_CBC(_BlockCipher): # silently not declared, and the corresponding suites will have 'usable' False. if conf.crypto_valid: - class _ARC2(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 - - _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 @@ -218,10 +255,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 dbe71c480f4..5c95fadd13a 100644 --- a/scapy/layers/tls/crypto/cipher_stream.py +++ b/scapy/layers/tls/crypto/cipher_stream.py @@ -8,14 +8,19 @@ Stream ciphers. """ -from __future__ import absolute_import from scapy.config import conf from scapy.layers.tls.crypto.common import CipherError -import scapy.libs.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 = {} @@ -36,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): @@ -82,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) @@ -105,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/compression.py b/scapy/layers/tls/crypto/compression.py index 91137ba2075..0c92233d851 100644 --- a/scapy/layers/tls/crypto/compression.py +++ b/scapy/layers/tls/crypto/compression.py @@ -8,11 +8,9 @@ TLS compression. """ -from __future__ import absolute_import import zlib from scapy.error import warning -import scapy.libs.six as six _tls_compression_algs = {} @@ -34,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 23b7672b029..7bbb80f26a6 100644 --- a/scapy/layers/tls/crypto/groups.py +++ b/scapy/layers/tls/crypto/groups.py @@ -13,50 +13,20 @@ (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.libs.six as six if conf.crypto_valid: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import dh, ec from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric.dh import DHParameterNumbers if conf.crypto_valid_advanced: from cryptography.hazmat.primitives.asymmetric import x25519 from cryptography.hazmat.primitives.asymmetric import x448 -# 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.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. - _ffdh_groups = {} @@ -72,7 +42,7 @@ def __new__(cls, ffdh_name, bases, dct): return the_class -class _FFDHParams(six.with_metaclass(_FFDHParamsMetaclass)): +class _FFDHParams(metaclass=_FFDHParamsMetaclass): pass @@ -461,7 +431,12 @@ def _tls_named_groups_import(group, pubbytes): import_point = x448.X448PublicKey.from_public_bytes return import_point(pubbytes) else: - curve = ec._CURVE_TYPES[_tls_named_curves[group]]() + 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, @@ -479,7 +454,7 @@ 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) + 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 @@ -518,7 +493,12 @@ def _tls_named_groups_generate(group): "Your cryptography version doesn't support " + group_name ) else: - curve = ec._CURVE_TYPES[_tls_named_curves[group]]() + 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. diff --git a/scapy/layers/tls/crypto/h_mac.py b/scapy/layers/tls/crypto/h_mac.py index 5ee48956628..26c69ebfbe0 100644 --- a/scapy/layers/tls/crypto/h_mac.py +++ b/scapy/layers/tls/crypto/h_mac.py @@ -8,11 +8,9 @@ HMAC classes. """ -from __future__ import absolute_import import hmac from scapy.layers.tls.crypto.hash import _tls_hash_algs -import scapy.libs.six as six from scapy.compat import bytes_encode _SSLv3_PAD1_MD5 = b"\x36" * 48 @@ -53,7 +51,7 @@ class HMACError(Exception): pass -class _GenericHMAC(six.with_metaclass(_GenericHMACMetaclass, object)): +class _GenericHMAC(metaclass=_GenericHMACMetaclass): def __init__(self, key=None): if key is None: self.key = b"" @@ -95,6 +93,10 @@ def digest_sslv3(self, tbd): return b"" +class Hmac_MD4(_GenericHMAC): + pass + + class Hmac_MD5(_GenericHMAC): pass @@ -117,3 +119,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}"](key=key) diff --git a/scapy/layers/tls/crypto/hash.py b/scapy/layers/tls/crypto/hash.py index c6cb68e2427..b1bcdbe2669 100644 --- a/scapy/layers/tls/crypto/hash.py +++ b/scapy/layers/tls/crypto/hash.py @@ -11,8 +11,6 @@ from hashlib import md5, sha1, sha224, sha256, sha384, sha512 from scapy.layers.tls.crypto.md4 import MD4 as md4 -import scapy.libs.six as six - _tls_hash_algs = {} @@ -32,7 +30,7 @@ def __new__(cls, hash_name, bases, dct): return the_class -class _GenericHash(six.with_metaclass(_GenericHashMetaclass, object)): +class _GenericHash(metaclass=_GenericHashMetaclass): def digest(self, tbd): return self.hash_cls(tbd).digest() diff --git a/scapy/layers/tls/crypto/hkdf.py b/scapy/layers/tls/crypto/hkdf.py index 28cb002ce43..649305666a5 100644 --- a/scapy/layers/tls/crypto/hkdf.py +++ b/scapy/layers/tls/crypto/hkdf.py @@ -9,7 +9,7 @@ import struct -from scapy.config import conf +from scapy.config import conf, crypto_validator from scapy.layers.tls.crypto.pkcs1 import _get_hash if conf.crypto_valid: @@ -20,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)) @@ -44,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) @@ -51,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 1f0b01e9599..cd1dbec9c1d 100644 --- a/scapy/layers/tls/crypto/kx_algs.py +++ b/scapy/layers/tls/crypto/kx_algs.py @@ -10,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.libs.six as six _tls_kx_algs = {} @@ -42,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/pkcs1.py b/scapy/layers/tls/crypto/pkcs1.py index 6691a56aa89..18008a5e7d8 100644 --- a/scapy/layers/tls/crypto/pkcs1.py +++ b/scapy/layers/tls/crypto/pkcs1.py @@ -12,9 +12,7 @@ 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.libs.six as six from scapy.config import conf, crypto_validator from scapy.error import warning @@ -170,9 +168,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) @@ -215,9 +211,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 4a4c81c6928..39f35509e54 100644 --- a/scapy/layers/tls/crypto/prf.py +++ b/scapy/layers/tls/crypto/prf.py @@ -8,7 +8,6 @@ TLS Pseudorandom Function. """ -from __future__ import absolute_import from scapy.error import warning from scapy.utils import strxor diff --git a/scapy/layers/tls/crypto/suites.py b/scapy/layers/tls/crypto/suites.py index 3c06fdf2c0f..f7079384d42 100644 --- a/scapy/layers/tls/crypto/suites.py +++ b/scapy/layers/tls/crypto/suites.py @@ -11,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.libs.six as six def get_algs_from_ciphersuite_name(ciphersuite_name): @@ -127,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 diff --git a/scapy/layers/tls/extensions.py b/scapy/layers/tls/extensions.py index 112bdd5670a..552e6d86a1c 100644 --- a/scapy/layers/tls/extensions.py +++ b/scapy/layers/tls/extensions.py @@ -7,14 +7,26 @@ 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 @@ -23,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 @@ -81,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 @@ -197,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, @@ -208,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), @@ -229,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", @@ -239,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", @@ -311,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)] @@ -320,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): @@ -364,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)] @@ -373,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), @@ -384,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)), ] @@ -392,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)), ] @@ -402,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, @@ -413,7 +427,7 @@ 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)] @@ -437,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, @@ -457,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, @@ -468,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", [], @@ -480,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"})] @@ -505,7 +519,7 @@ def i2repr(self, pkt, x): 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)] @@ -522,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 @@ -546,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)] @@ -576,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", [], @@ -594,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)] @@ -605,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)] @@ -623,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", [], @@ -635,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)] @@ -653,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", 0x32, _tls_ext), - ShortField("len", None), + MayEnd(ShortField("len", None)), SigAndHashAlgsLenField("sig_algs_len", None, length_of="sig_algs"), SigAndHashAlgsField("sig_algs", [], @@ -671,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", "", @@ -681,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, @@ -718,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 @@ -831,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 7ef4956ab16..6a5e3834c76 100644 --- a/scapy/layers/tls/handshake.py +++ b/scapy/layers/tls/handshake.py @@ -12,7 +12,6 @@ mechanisms which are addressed with keyexchange.py. """ -from __future__ import absolute_import import math import os import struct @@ -37,7 +36,6 @@ from scapy.compat import hex_bytes, orb, raw from scapy.config import conf -from scapy.libs import six from scapy.packet import Packet, Raw, Padding from scapy.utils import randstring, repr_hex from scapy.layers.x509 import OCSP_Response @@ -46,6 +44,7 @@ _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, @@ -115,9 +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) ############################################################################### @@ -180,7 +185,7 @@ 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 @@ -338,9 +343,10 @@ def tls_session_update(self, msg_str): 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): @@ -398,7 +404,7 @@ def post_build(self, p, pay): # For a resumed PSK, the hash function use # to compute the binder must be the same # as the one used to establish the original - # conntection. For that, we assume that + # 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 @@ -465,10 +471,12 @@ def tls_session_update(self, msg_str): # RFC 8701: GREASE of TLS will send unknown versions # here. We have to ignore them if ver in _tls_version: - self.tls_session.advertised_tls_version = ver + 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 ############################################################################### @@ -519,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:] @@ -667,7 +680,6 @@ def tls_session_update(self, msg_str): 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) @@ -700,12 +712,15 @@ 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) 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 if s.client_session_ticket: @@ -802,7 +817,8 @@ def post_dissection_tls_session_update(self, msg_str): s.wcs = self.tls_session.pwcs s.triggered_pwcs_commit = False else: - s.triggered_prcs_commit = True + s.triggered_pwcs_commit = True + ############################################################################### # Certificate # ############################################################################### @@ -990,11 +1006,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 @@ -1181,6 +1197,11 @@ class TLS13CertificateRequest(_TLSHandshake): 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 # ############################################################################### @@ -1203,11 +1224,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 @@ -1228,6 +1254,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 @@ -1331,7 +1359,14 @@ def tls_session_update(self, msg_str): self.tls_session.session_hash = ( Hash_MD5().digest(to_hash) + Hash_SHA().digest(to_hash) ) - self.tls_session.compute_ms_and_derive_keys() + 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() ############################################################################### @@ -1342,7 +1377,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 @@ -1360,6 +1395,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: @@ -1369,13 +1406,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 @@ -1389,7 +1429,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 @@ -1400,7 +1441,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) @@ -1410,6 +1451,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) @@ -1417,7 +1461,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) @@ -1427,6 +1471,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 @@ -1654,7 +1701,6 @@ def build(self): 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 diff --git a/scapy/layers/tls/handshake_sslv2.py b/scapy/layers/tls/handshake_sslv2.py index 78885d2b953..1917cdf523e 100644 --- a/scapy/layers/tls/handshake_sslv2.py +++ b/scapy/layers/tls/handshake_sslv2.py @@ -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 e1e22a66993..0f939b8358e 100644 --- a/scapy/layers/tls/keyexchange.py +++ b/scapy/layers/tls/keyexchange.py @@ -9,7 +9,6 @@ TLS key exchange logic. """ -from __future__ import absolute_import import math import struct @@ -161,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", 0x0804, _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, @@ -173,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 "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): """ @@ -194,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): @@ -208,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: @@ -339,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) @@ -375,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()) @@ -585,6 +599,7 @@ def fill_missing(self): # 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: self.point = _tls_named_groups_pubbytes( @@ -613,6 +628,7 @@ def register_pubkey(self): 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 = self.named_curve @@ -669,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) @@ -681,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: @@ -748,7 +767,7 @@ def fill_missing(self): 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.lstrip(b"\x00") - if not s.extms or s.session_hash: + 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 @@ -782,7 +801,7 @@ 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.lstrip(b"\x00") - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() def guess_payload_class(self, p): @@ -821,15 +840,15 @@ def fill_missing(self): x = pubkey.public_numbers().x y = pubkey.public_numbers().y self.ecdh_Yc = (b"\x04" + - pkcs_i2osp(x, pubkey.key_size // 8) + - pkcs_i2osp(y, pubkey.key_size // 8)) + 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: s.pre_master_secret = pms - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() def post_build(self, pkt, pay): @@ -855,7 +874,7 @@ def post_dissection(self, m): 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 - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() @@ -919,7 +938,7 @@ def pre_dissect(self, m): warning(err) s.pre_master_secret = pms - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() return pms @@ -935,7 +954,7 @@ def post_build(self, pkt, pay): s = self.tls_session s.pre_master_secret = enc - if not s.extms or s.session_hash: + if not s.extms: s.compute_ms_and_derive_keys() if s.server_tmp_rsa_key is not None: diff --git a/scapy/layers/tls/keyexchange_tls13.py b/scapy/layers/tls/keyexchange_tls13.py index 87f979fd4d3..03692ffdccb 100644 --- a/scapy/layers/tls/keyexchange_tls13.py +++ b/scapy/layers/tls/keyexchange_tls13.py @@ -16,6 +16,7 @@ FieldLenField, IntField, PacketField, + PacketLenField, PacketListField, ShortEnumField, ShortField, @@ -23,8 +24,9 @@ StrLenField, XStrLenField, ) -from scapy.packet import Packet, Padding +from scapy.packet import Packet from scapy.layers.tls.extensions import TLS_Ext_Unknown, _tls_ext +from scapy.layers.tls.cert import PrivKeyECDSA, PrivKeyRSA, PrivKeyEdDSA from scapy.layers.tls.crypto.groups import ( _tls_named_curves, _tls_named_ffdh_groups, @@ -33,10 +35,12 @@ _tls_named_groups_import, _tls_named_groups_pubbytes, ) -import scapy.libs.six as six if conf.crypto_valid: from cryptography.hazmat.primitives.asymmetric import ec +if conf.crypto_valid_advanced: + from cryptography.hazmat.primitives.asymmetric import ed25519 + from cryptography.hazmat.primitives.asymmetric import ed448 class KeyShareEntry(Packet): @@ -168,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): + 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): @@ -191,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): + 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): + 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) @@ -229,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" @@ -258,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 @@ -283,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 463504d28d5..e6c59456913 100644 --- a/scapy/layers/tls/record.py +++ b/scapy/layers/tls/record.py @@ -37,18 +37,9 @@ 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.libs.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 # @@ -156,7 +147,7 @@ def getfield(self, pkt, s): 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: @@ -217,7 +208,7 @@ def addfield(self, pkt, s, val): # 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) and not isinstance(pkt.msg[0], TLSChangeCipherSpec)): @@ -337,8 +328,14 @@ def dispatch_hook(cls, _pkt=None, *args, **kargs): 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 _tls_version_check(s.tls_version, 0x0304): + if s and s.tls_version == 0x0304: _has_cipher = lambda x: ( x and not isinstance(x.cipher, Cipher_NULL) ) @@ -576,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) @@ -735,11 +744,11 @@ def post_build(self, pkt, pay): return hdr + efrag + pay def mysummary(self): - s = super(TLS, self).mysummary() + s, n = super(TLS, self).mysummary() if self.msg: s += " / " s += " / ".join(getattr(x, "_name", x.name) for x in self.msg) - return s + return s, n ############################################################################### # TLS ChangeCipherSpec # diff --git a/scapy/layers/tls/record_sslv2.py b/scapy/layers/tls/record_sslv2.py index abe5004610a..8e99a3522b2 100644 --- a/scapy/layers/tls/record_sslv2.py +++ b/scapy/layers/tls/record_sslv2.py @@ -141,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 @@ -174,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 b505bc8e20f..ff8f0acec4d 100644 --- a/scapy/layers/tls/record_tls13.py +++ b/scapy/layers/tls/record_tls13.py @@ -15,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 @@ -125,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): @@ -172,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 @@ -223,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 887a024a078..de9f342bb46 100644 --- a/scapy/layers/tls/session.py +++ b/scapy/layers/tls/session.py @@ -10,16 +10,16 @@ """ import binascii +import collections import socket import struct from scapy.config import conf from scapy.compat import raw -import scapy.libs.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 DefaultSession +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 @@ -27,7 +27,7 @@ from scapy.layers.tls.crypto.prf import PRF # Typing imports -from scapy.compat import Dict +from typing import Dict def load_nss_keys(filename): @@ -35,14 +35,15 @@ def load_nss_keys(filename): """ Parses a NSS Keys log and returns unpacked keys in a dictionary. """ - keys = {} + # 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 {} - else: + try: with open(filename) as fd: for line in fd: if line.startswith("#"): @@ -54,24 +55,26 @@ def load_nss_keys(filename): try: client_random = binascii.unhexlify(data[1]) - except binascii.Error: + except ValueError: warning("Invalid ClientRandom: %s", data[1]) return {} try: secret = binascii.unhexlify(data[2]) - except binascii.Error: + 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 data[0] in keys: + if client_random in keys[data[0]]: warning("Duplicated entry for %s !", data[0]) - keys[data[0]] = {"ClientRandom": client_random, - "Secret": secret} + 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__() @@ -369,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 @@ -435,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. @@ -477,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 @@ -498,7 +510,9 @@ def __init__(self, self.tls13_handshake_secret = None self.tls13_master_secret = None self.tls13_derived_secrets = {} - self.post_handshake_auth = False + 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 @@ -508,6 +522,9 @@ def __init__(self, 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 @@ -530,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): @@ -542,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 @@ -599,12 +631,16 @@ def compute_master_secret(self): if conf.debug_tls: log_runtime.debug("TLS: master secret: %s", repr_hex(ms)) - def compute_ms_and_derive_keys(self): + def use_nss_master_secret_if_present(self) -> bool: # Load the master secret from an NSS Key dictionary - if self.nss_keys and self.nss_keys.get("CLIENT_RANDOM", False) and \ - self.nss_keys["CLIENT_RANDOM"].get("Secret", False): - self.master_secret = self.nss_keys["CLIENT_RANDOM"]["Secret"] + 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): if not self.master_secret: self.compute_master_secret() @@ -695,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) @@ -732,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. @@ -765,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) @@ -779,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): @@ -861,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. @@ -904,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 # ############################################################################### @@ -947,14 +1044,8 @@ def __init__(self, _pkt="", post_transform=None, _internal=0, self.wcs_snap_init = self.tls_session.wcs.snapshot() if isinstance(_underlayer, TCP): - tcp = _underlayer - self.tls_session.sport = tcp.sport - self.tls_session.dport = tcp.dport - try: - self.tls_session.ipsrc = tcp.underlayer.src - self.tls_session.ipdst = tcp.underlayer.dst - except AttributeError: - pass + # Get information from _underlayer + self.tls_session.set_underlayer(_underlayer) # Load a NSS Key Log file if conf.tls_nss_filename is not None: @@ -1080,25 +1171,54 @@ def show2(self): s.rcs = rcs_snap s.wcs = wcs_snap - def mysummary(self): - return "TLS %s / %s" % (repr(self.tls_session), - getattr(self, "_name", self.name)) + 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 TLSSession + # 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: - return cls(data) - elif len(data) > length: - pkt = cls(data) - if hasattr(pkt.payload, "tcp_reassemble"): - return pkt.payload.tcp_reassemble(data[length:], metadata, session) - else: - return pkt + 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) @@ -1124,11 +1244,12 @@ 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) @@ -1141,16 +1262,16 @@ def find(self, session): 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 li in six.itervalues(self.sessions): + for li in self.sessions.values(): for s in li: src = "%s[%d]" % (s.ipsrc, s.sport) dst = "%s[%d]" % (s.ipdst, s.dport) @@ -1163,8 +1284,13 @@ def __repr__(self): return "\n".join(map(lambda x: fmt % x, res)) -class TLSSession(DefaultSession): +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 @@ -1177,10 +1303,5 @@ def toPacketList(self): return super(TLSSession, self).toPacketList() +# Instantiate the TLS sessions holder conf.tls_sessions = _tls_sessions() -conf.tls_session_enable = False -conf.tls_verbose = False -# Filename containing NSS Keys Log -conf.tls_nss_filename = None -# Dictionary containing parsed NSS Keys -conf.tls_nss_keys = None diff --git a/scapy/layers/tls/tools.py b/scapy/layers/tls/tools.py index 23c1a404e35..66318b92ec6 100644 --- a/scapy/layers/tls/tools.py +++ b/scapy/layers/tls/tools.py @@ -8,7 +8,6 @@ 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/tuntap.py b/scapy/layers/tuntap.py index a52dbd3ebc6..19a3e289f0b 100644 --- a/scapy/layers/tuntap.py +++ b/scapy/layers/tuntap.py @@ -10,28 +10,30 @@ These allow Scapy to act as the remote side of a virtual network interface. """ -from __future__ import absolute_import - -import os import socket import time from fcntl import ioctl -from scapy.compat import raw, bytes_encode +from scapy.compat import bytes_encode, raw from scapy.config import conf -from scapy.consts import BIG_ENDIAN, BSD, LINUX +from scapy.consts import BIG_ENDIAN, BSD, DARWIN, LINUX from scapy.data import ETHER_TYPES, MTU -from scapy.error import warning, log_runtime -from scapy.fields import Field, FlagsField, StrFixedLenField, XShortEnumField +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 IPv46, IPv6 +from scapy.layers.inet6 import IPv6, IPv46 from scapy.layers.l2 import Ether -from scapy.packet import Packet +from scapy.packet import Packet, bind_layers from scapy.supersocket import SimpleSocket -import scapy.libs.six as six - # Linux-specific defines (/usr/include/linux/if_tun.h) LINUX_TUNSETIFF = 0x400454ca LINUX_IFF_TUN = 0x0001 @@ -39,6 +41,11 @@ 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): @@ -64,6 +71,18 @@ class LinuxTunIfReq(Packet): ] +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. @@ -81,6 +100,12 @@ class LinuxTunPacketInfo(TunPacketInfo): ] +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. @@ -114,12 +139,12 @@ class TunTapInterface(SimpleSocket): 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) + 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"): + 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 @@ -155,23 +180,38 @@ def __init__(self, iface=None, mode_tun=None, default_read_size=MTU, 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 not (self.iface.startswith(b"tap") or - self.iface.startswith(b"tun")): + 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") - 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 + "`tap` on BSD and Darwin or `utun` on Darwin") else: raise NotImplementedError("TunTapInterface is not supported on " "this platform!") - sock = open(devname, "r+b", buffering=0) - if LINUX: if self.mode_tun: flags = LINUX_IFF_TUN @@ -210,12 +250,7 @@ def recv_raw(self, x=None): x += self.mtu_overhead - if six.PY2: - # For some mystical reason, using self.ins.read ignores - # buffering=0 on python 2.7 and blocks ?! - dat = os.read(self.ins.fileno(), x) - else: - dat = self.ins.read(x) + 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 @@ -249,3 +284,8 @@ def send(self, x): 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 c7313b862f3..d5a7f39837c 100644 --- a/scapy/layers/usb.py +++ b/scapy/layers/usb.py @@ -10,22 +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.interfaces import NetworkInterface, InterfaceProvider, \ - network_name, IFACES from scapy.packet import Packet, bind_top_down -from scapy.supersocket import SuperSocket -from scapy.utils import PcapReader + # USBpcap @@ -152,134 +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"] - ) - - class UsbpcapInterfaceProvider(InterfaceProvider): - name = "USBPcap" - headers = ("Index", "Name", "Address") - header_sort = 1 - - def load(self): - data = {} - try: - interfaces = get_usbpcap_interfaces() - except OSError: - 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] = NetworkInterface(self, if_data) - return data - - def l2socket(self): - return conf.USBsocket - l2listen = l2socket - - def l3socket(self): - raise ValueError("No L3 available for USBpcap !") - - def _format(self, dev, **kwargs): - """Returns a tuple of the elements used by show()""" - return (str(dev.index), dev.name, dev.network_name) - - IFACES.register_provider(UsbpcapInterfaceProvider) - - 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 - - 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()") - iface = network_name(iface) - 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/x509.py b/scapy/layers/x509.py index b46c9e3cf45..1f1afe4f9c9 100644 --- a/scapy/layers/x509.py +++ b/scapy/layers/x509.py @@ -2,30 +2,56 @@ # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -# Acknowledgment: Maxence Tury +# Acknowledgment: Arnaud Ebalard & Maxence Tury # Cool history about this file: http://natisbad.org/scapy/index.html """ -X.509 certificates. +X.509 certificates and other crypto-related ASN.1 structures """ 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_NULL, + 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_optional, + ASN1F_PACKET, + ASN1F_PRINTABLE_STRING, + ASN1F_SEQUENCE_OF, + ASN1F_SEQUENCE, + ASN1F_SET_OF, + 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 PacketField, MultipleTypeField from scapy.volatile import ZuluTime, GeneralizedTime from scapy.compat import plain_str @@ -91,6 +117,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 # #################################### @@ -160,6 +218,35 @@ class ECDSASignature(ASN1_Packet): ASN1F_INTEGER("s", 0)) +#################################### +# 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 # ###################### @@ -169,12 +256,13 @@ 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) @@ -216,9 +304,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", "") @@ -662,9 +759,16 @@ 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_field("value", None) + ASN1_root = ASN1F_X509_otherName() + type_id = ASN1_OID("1.3.6.1.4.1.311.25.2.1") + value = ASN1_UTF8_STRING("") # oid-info.com shows that some extensions share multiple OIDs. @@ -694,51 +798,35 @@ 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.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 @@ -760,29 +848,14 @@ class X509_AlgorithmIdentifier(ASN1_Packet): 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) + ASN1F_CHOICE( + "parameters", ASN1_NULL(0), + ASN1F_NULL, + ECParameters, + DomainParameters, + ) + ) + ) class ASN1F_X509_SubjectPublicKeyInfo(ASN1F_SEQUENCE): @@ -790,37 +863,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 @@ -1004,20 +1068,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", @@ -1026,37 +1076,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 @@ -1121,20 +1151,6 @@ def get_issuer_str(self): 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): seq = [ASN1F_PACKET("tbsCertList", @@ -1143,43 +1159,188 @@ 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): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_PACKET("issuer", X509_DirectoryName(), X509_DirectoryName), + ASN1F_INTEGER("serialNumber", 0) + ) + + +# RFC3852 sect 5.3 + + +class CMS_Attribute(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + ASN1F_OID("attrType", "0"), + ASN1F_SET_OF("attrValues", [], ASN1F_field("attr", None)) + ) + + +class CMS_SignerInfo(ASN1_Packet): + ASN1_codec = ASN1_Codecs.BER + ASN1_root = ASN1F_SEQUENCE( + CMSVersion("version", 1), + ASN1F_PACKET("sid", CMS_IssuerAndSerialNumber(), CMS_IssuerAndSerialNumber), + ASN1F_PACKET("digestAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_optional( + ASN1F_SET_OF( + "signedAttrs", + None, + CMS_Attribute, + implicit_tag=0xA0, + ) + ), + ASN1F_PACKET("signatureAlgorithm", X509_AlgorithmIdentifier(), + X509_AlgorithmIdentifier), + ASN1F_STRING("signature", ASN1_UTF8_STRING("")), + ASN1F_optional( + ASN1F_SET_OF( + "unsignedAttrs", + None, + CMS_Attribute, + implicit_tag=0xA1, + ) + ) + ) + + +# 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 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_BIT_STRING("content", "", explicit_tag=0xA0) + ) + ) + + ############################# # OCSP Status packets # ############################# @@ -1208,7 +1369,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): @@ -1231,7 +1392,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( @@ -1268,7 +1429,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())), @@ -1279,23 +1440,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", @@ -1304,40 +1448,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/libs/bluetoothids.py b/scapy/libs/bluetoothids.py new file mode 100644 index 00000000000..a4a869520ab --- /dev/null +++ b/scapy/libs/bluetoothids.py @@ -0,0 +1,767 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information + +""" +# Bluetooth Core Company Identifiers +# +# This file was generated. Its canonical location is +# https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/company_identifiers/company_identifiers.yaml +""" + +# 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 the Bluetooth SID listing of all company identifiers. + +# This file is automatically generated using +# scapy/tools/generate_bluetooth.py + +import gzip +import json +from base64 import b85decode + + +def _d(x: str) -> 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 8f18471f151..6ce6850294b 100644 --- a/scapy/libs/ethertypes.py +++ b/scapy/libs/ethertypes.py @@ -1,3 +1,4 @@ +# SPDX-License-Identifier: BSD-3-Clause # This file is part of Scapy # 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/rfc3961.py b/scapy/libs/rfc3961.py index 03d4d08790a..858a9fa2073 100644 --- a/scapy/libs/rfc3961.py +++ b/scapy/libs/rfc3961.py @@ -2,67 +2,174 @@ # 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 RFC 3961's cryptographic functions +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 `_ """ -# The following is a modified version of +# 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 +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: - from Cryptodome.Cipher import AES, DES3, ARC4, DES - from Cryptodome.Hash import HMAC, MD4, MD5, SHA - from Cryptodome.Protocol.KDF import PBKDF2 + # cryptography > 43.0 + from cryptography.hazmat.decrepit.ciphers import ( + algorithms as decrepit_algorithms, + ) except ImportError: - # Backward compatibility - from Crypto.Cipher import AES, DES3, ARC4, DES - from Crypto.Hash import HMAC, MD4, MD5, SHA - from Crypto.Protocol.KDF import PBKDF2 + decrepit_algorithms = algorithms except ImportError: - raise ImportError( - "To use kerberos cryptography, you need to install pycryptodome.\n" - "pip install pycryptodome" + raise ImportError("To use kerberos cryptography, you need to install cryptography.") + + +# cryptography's TripleDES allow the usage of a 56bit key, which thus behaves like DES +DES = decrepit_algorithms.TripleDES + + +# 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 -__all__ = [ - "EncryptionType", - "ChecksumType", - "Key", - "InvalidChecksum", -] +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 -class EncryptionType: - DES_CRC = 1 - DES_MD4 = 2 - DES_MD5 = 3 - DES3 = 16 - AES128 = 17 - AES256 = 18 - RC4 = 23 +# https://www.iana.org/assignments/kerberos-parameters/kerberos-parameters.xhtml#kerberos-parameters-2 -class ChecksumType: + +class ChecksumType(enum.IntEnum): CRC32 = 1 - MD4 = 2 - MD4_DES = 3 - MD5 = 7 - MD5_DES = 8 - SHA1 = 9 - SHA1_DES3 = 12 - SHA1_AES128 = 15 - SHA1_AES256 = 16 + 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 @@ -70,47 +177,71 @@ class InvalidChecksum(ValueError): pass +######### +# Utils # +######### + + +# https://www.gnu.org/software/shishi/ides.pdf - APPENDIX B + + def _n_fold(s, n): + # type: (bytes, int) -> bytes """ - https://www.gnu.org/software/shishi/ides.pdf - APPENDIX B + 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(x, nb): - x = bytes_int(x) + def rot13(y, nb): + # type: (bytes, int) -> bytes + x = bytes_int(y) mod = (1 << (nb * 8)) - 1 if nb == 0: - return x + 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 = math.lcm(n, m) + 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 = b"\x00" * n + out = bytearray(b"\x00" * n) for i in range(0, lcm, n): - out = ocadd(out, buf[i: i + n], 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 """ @@ -119,38 +250,123 @@ def _xorbytes(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.) - assert len(mac1) == len(mac2) - res = 0 - for x, y in zip(mac1, mac2): - res |= x ^ y - return res == 0 + 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( [ - b"\x01" * 8, - b"\xfe" * 8, - b"\xe0" * 4 + b"\xf1" * 4, - b"\x1f" * 4 + b"\x0e" * 4, - b"\x01\x1f\x01\x1f\x01\x0e\x01\x0e", - b"\x1f\x01\x1f\x01\x0e\x01\x0e\x01", - b"\x01\xe0\x01\xe0\x01\xf1\x01\xf1", + # 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", - b"\x1f\xe0\x1f\xe0\x0e\xf1\x0e\xf1", - b"\xe0\x1f\xe0\x1f\xf1\x0e\xf1\x0e", - b"\x1f\xfe\x1f\xfe\x0e\xfe\x0e\xfe", - b"\xfe\x1f\xfe\x1f\xfe\x0e\xfe\x0e", + # 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(object): + +class _EncryptionAlgorithmProfile(abc.ABCMeta): """ Base class for etype profiles. @@ -158,6 +374,8 @@ class _EncryptionAlgorithmProfile(object): :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: @@ -165,13 +383,81 @@ class _EncryptionAlgorithmProfile(object): :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. @@ -182,12 +468,34 @@ class _SimplifiedEncryptionProfile(_EncryptionAlgorithmProfile): :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 hash: underlying hash function + :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. """ @@ -199,55 +507,106 @@ def derive(cls, key, constant): plaintext = _n_fold(constant, cls.blocksize) rndseed = b"" while len(rndseed) < cls.seedsize: - ciphertext = cls.basic_encrypt(key, plaintext) + 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]) + return cls.random_to_key(rndseed[0 : cls.seedsize]).key @classmethod - def encrypt(cls, key, keyusage, plaintext, confounder): + def encrypt(cls, key, keyusage, plaintext, confounder, signtext=None): + # type: (Key, int, bytes, Optional[bytes], Optional[bytes]) -> bytes """ - encryption function + 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. """ - ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55)) - ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA)) + 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) - hmac = HMAC.new(ki.key, basic_plaintext, cls.hashmod).digest() - return cls.basic_encrypt(ke, basic_plaintext) + hmac[: cls.macsize] + 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): + def decrypt(cls, key, keyusage, ciphertext, presignfunc=None): + # type: (Key, int, bytes, Optional[Callable[[bytes, bytes], bytes]]) -> bytes """ decryption function """ - ki = cls.derive(key, struct.pack(">IB", keyusage, 0x55)) - ke = cls.derive(key, struct.pack(">IB", keyusage, 0xAA)) + 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 = bytearray(ciphertext[: -cls.macsize]), bytearray( - ciphertext[-cls.macsize:] - ) + basic_ctext, mac = ciphertext[: -cls.macsize], ciphertext[-cls.macsize :] if len(basic_ctext) % cls.padsize != 0: raise ValueError("ciphertext does not meet padding requirement") - basic_plaintext = cls.basic_decrypt(ke, bytes(basic_ctext)) - hmac = bytearray(HMAC.new(ki.key, basic_plaintext, cls.hashmod).digest()) - expmac = hmac[: cls.macsize] - if not _mac_equal(mac, expmac): - raise ValueError("ciphertext integrity failure") + 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:]) + 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.new(string).digest() + hashval = cls.hashmod().digest(string) if len(hashval) % cls.blocksize: hashval = hashval[: -(len(hashval) % cls.blocksize)] # Encrypt the hash with a derived key. @@ -255,49 +614,115 @@ def prf(cls, key, string): 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 = MD5 + hashmod = Hash_MD5 @classmethod - def encrypt(cls, key, keyusage, plaintext, confounder): + 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.new(basic_plaintext).digest() + checksum = cls.hashmod().digest(basic_plaintext) basic_plaintext = ( - basic_plaintext[: len(confounder)] + - checksum + - basic_plaintext[len(confounder) + len(checksum):] + basic_plaintext[: len(confounder)] + + checksum + + basic_plaintext[len(confounder) + len(checksum) :] ) - return cls.basic_encrypt(key, basic_plaintext) + return cls.basic_encrypt(key.key, basic_plaintext) @classmethod - def decrypt(cls, key, keyusage, ciphertext): + 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, ciphertext) + 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:] + mac = complex_plaintext[cls.padsize : cls.padsize + cls.macsize] + message = complex_plaintext[cls.padsize + cls.macsize :] - expmac = bytearray( - cls.hashmod.new(cofounder + b"\x00" * cls.macsize + message).digest() - ) + 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") @@ -308,6 +733,7 @@ def fixparity(deskey): return temp def addparity(l1): + # type: (List[int]) -> List[int] temp = list() for byte in l1: if (bin(byte).count("1") % 2) == 0: @@ -318,6 +744,7 @@ def addparity(l1): 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) @@ -328,7 +755,7 @@ def XOR(l1, l2): 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)]: + for block in [s[i : i + 8] for i in range(0, len(s), 8)]: temp56 = list() # removeMSBits for byte in block: @@ -342,7 +769,7 @@ def XOR(l1, l2): bintemp = bintemp[::-1] temp56 = list() - for bits7 in [bintemp[i: i + 7] for i in range(0, len(bintemp), 7)]: + for bits7 in [bintemp[i : i + 7] for i in range(0, len(bintemp), 7)]: temp56.append(int(bits7, 2)) odd = not odd @@ -352,8 +779,9 @@ def XOR(l1, l2): if bytes(tempkey) in WEAK_DES_KEYS: tempkey[7] = tempkey[7] ^ 0xF0 - cipher = DES.new(tempkey, DES.MODE_CBC, tempkey) - chekcsumkey = cipher.encrypt(s)[-8:] + 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 @@ -362,50 +790,68 @@ def XOR(l1, l2): @classmethod def basic_encrypt(cls, key, plaintext): + # type: (bytes, bytes) -> bytes assert len(plaintext) % 8 == 0 - des = DES.new(key.key, DES.MODE_CBC, b"\0" * 8) - return des.encrypt(bytes(plaintext)) + 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 = DES.new(key.key, DES.MODE_CBC, b"\0" * 8) - return des.decrypt(bytes(ciphertext)) + 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_MD5 - hashmod = MD5 + etype = EncryptionType.DES_CBC_MD5 + hashmod = Hash_MD5 + reqcksum = ChecksumType.RSA_MD5_DES + + +# RFC3961 sect 6.2.2 class _DESMD4(_DESCBC): - etype = EncryptionType.DES_MD4 - hashmod = MD4 + etype = EncryptionType.DES_CBC_MD4 + hashmod = Hash_MD4 + reqcksum = ChecksumType.RSA_MD4_DES + + +# RFC3961 sect 6.3 class _DES3CBC(_SimplifiedEncryptionProfile): - etype = EncryptionType.DES3 + etype = EncryptionType.DES3_CBC_SHA1_KD keysize = 24 seedsize = 21 blocksize = 8 padsize = 8 macsize = 20 - hashmod = SHA + 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 @@ -413,12 +859,11 @@ def parity(b): 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 = bytes(bytearray(firstbytes + [lastbyte])) - if keybytes in WEAK_DES_KEYS: + keybytes = bytearray(firstbytes + [lastbyte]) + if bytes(keybytes) in WEAK_DES_KEYS: keybytes[7] = keybytes[7] ^ 0xF0 return bytes(keybytes) - seed = bytearray(seed) if len(seed) != 21: raise ValueError("Wrong seed length") k1, k2, k3 = expand(seed[:7]), expand(seed[7:14]), expand(seed[14:]) @@ -426,44 +871,77 @@ def parity(b): @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 cls.derive(k, b"kerberos") + 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 = DES3.new(key.key, AES.MODE_CBC, b"\0" * 8) - return des3.encrypt(bytes(plaintext)) + 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 = DES3.new(key.key, AES.MODE_CBC, b"\0" * 8) - return des3.decrypt(bytes(ciphertext)) + 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(_SimplifiedEncryptionProfile): - # Base class for aes128-cts and aes256-cts. +class _AESEncryptionType_SHA1_96(_SimplifiedEncryptionProfile, abc.ABCMeta): blocksize = 16 padsize = 1 macsize = 12 - hashmod = SHA + 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] - prf = lambda p, s: HMAC.new(p, s, SHA).digest() - seed = PBKDF2(string, salt, cls.seedsize, iterations, prf) - tkey = cls.random_to_key(seed) - return cls.derive(tkey, b"kerberos") + 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 = AES.new(key.key, AES.MODE_CBC, b"\0" * 16) - ctext = aes.encrypt(_zeropad(bytes(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. @@ -473,90 +951,156 @@ def basic_encrypt(cls, key, plaintext): @classmethod def basic_decrypt(cls, key, ciphertext): + # type: (bytes, bytes) -> bytes assert len(ciphertext) >= 16 - aes = AES.new(key.key, AES.MODE_ECB) + aes = Cipher(algorithms.AES(key), modes.ECB()).decryptor() if len(ciphertext) == 16: - return aes.decrypt(ciphertext) + 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) + 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.decrypt(bytes(bb))), prev_cblock) + 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.decrypt(bytes(cblocks[-2]))) + 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.decrypt(bytes(cblocks[-1]) + bytes(omitted))), prev_cblock + bytearray(aes.update(bytes(cblocks[-1]) + bytes(omitted))), prev_cblock ) return plaintext + lastplaintext -class _AES128CTS(_AESEncryptionType): - etype = 17 # AES128 +# 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(_AESEncryptionType): - etype = 18 # AES256 +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 = 23 # RC4 + 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. Per the errata, do not map 9 to 8. + # 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=MD4.new(utf16string).digest()) + 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) - ki = HMAC.new(key.key, cls.usage_str(keyusage), MD5).digest() - cksum = HMAC.new(ki, confounder + plaintext, MD5).digest() - ke = HMAC.new(ki, cksum, MD5).digest() - return cksum + ARC4.new(ke).encrypt(bytes(confounder + plaintext)) + 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 = bytearray(ciphertext[:16]), bytearray(ciphertext[16:]) - ki = HMAC.new(key.key, cls.usage_str(keyusage), MD5).digest() - ke = HMAC.new(ki, cksum, MD5).digest() - basic_plaintext = bytearray(ARC4.new(ke).decrypt(bytes(basic_ctext))) - exp_cksum = bytearray(HMAC.new(ki, basic_plaintext, MD5).digest()) + 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.new(key.key, struct.pack(" bytes + return Hmac_SHA(key.key).digest(string) -class _ChecksumProfile(object): - # Base class for checksum profiles. Usable checksum classes must - # define: - # * checksum - # * verify (if verification is not just checksum-and-compare) - @classmethod - def verify(cls, key, keyusage, text, cksum): - expected = cls.checksum(key, keyusage, text) - if not _mac_equal(bytearray(cksum), bytearray(expected)): - raise InvalidChecksum("checksum verification failure") +class _RC4_EXPORT(_RC4): + etype = EncryptionType.RC4_HMAC_EXP + export = True -class _SimplifiedChecksum(_ChecksumProfile): - # Base class for checksums using the RFC 3961 simplified profile. - # Defines the checksum and verify methods. Subclasses must - # define: - # * macsize: Size of checksum in bytes - # * enc: Profile of associated etype +############ +# 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 checksum(cls, key, keyusage, text): - kc = cls.enc.derive(key, struct.pack(">IB", keyusage, 0x99)) - hmac = HMAC.new(kc.key, text, cls.enc.hashmod).digest() - return hmac[: cls.macsize] + 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 verify(cls, key, keyusage, text, cksum): - if key.etype != cls.enc.etype: - raise ValueError("Wrong key type for checksum") - super(_SimplifiedChecksum, cls).verify(key, keyusage, text, cksum) + 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 _SHA1AES128(_SimplifiedChecksum): - macsize = 12 - enc = _AES128CTS + +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 _SHA1AES256(_SimplifiedChecksum): - macsize = 12 - enc = _AES256CTS +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 _SHA1DES3(_SimplifiedChecksum): - macsize = 20 - enc = _DES3CBC +class _SHA256_128_AES128(_SimplifiedChecksum): + macsize = 16 + enc = _AES128CTS_SHA256_128 + rfc8009 = True -class _HMACMD5(_ChecksumProfile): - @classmethod - def checksum(cls, key, keyusage, text): - ksign = HMAC.new(key.key, b"signaturekey\0", MD5).digest() - md5hash = MD5.new(_RC4.usage_str(keyusage) + text).digest() - return HMAC.new(ksign, md5hash, MD5).digest() +class _SHA384_182_AES256(_SimplifiedChecksum): + macsize = 24 + enc = _AES256CTS_SHA384_192 + rfc8009 = True - @classmethod - def verify(cls, key, keyusage, text, cksum): - if key.etype != EncryptionType.RC4: - raise ValueError("Wrong key type for checksum") - super(_HMACMD5, cls).verify(key, keyusage, text, cksum) +############## +# Key object # +############## _enctypes = { - EncryptionType.DES_MD5: _DESMD5, - EncryptionType.DES_MD4: _DESMD4, - EncryptionType.DES3: _DES3CBC, - EncryptionType.AES128: _AES128CTS, - EncryptionType.AES256: _AES256CTS, - EncryptionType.RC4: _RC4, + # 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.SHA1_DES3: _SHA1DES3, - ChecksumType.SHA1_AES128: _SHA1AES128, - ChecksumType.SHA1_AES256: _SHA1AES256, + 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, cksumtype=None, key=None): - self.eptype = etype - try: - self.ep = _enctypes[etype] - except ValueError: - raise ValueError("Unknown etype '%s'" % etype) + 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[etype] + self.cp = _checksums[cksumtype] except ValueError: - raise ValueError("Unknown etype '%s'" % etype) - if key is not None and len(key) != self.ep.keysize: - raise ValueError( - "Wrong key length. Got %s. Expected %s" % (len(key), self.ep.keysize) - ) + 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 "" % ( - self.eptype, - " (%s octets)" % len(self.key) if self.key is not None else "", + name, + " (%s octets)" % len(self.key), ) - def encrypt(self, keyusage, plaintext, confounder=None): - return self.ep.encrypt(self, keyusage, bytes(plaintext), confounder) + 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): + 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) + 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): + 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("checksumtype not specified !") - return self.cp.checksum(self, keyusage, text) + 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. - def verify_checksum(self, keyusage, text, cksum): + :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("checksumtype not specified !") + 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: @@ -707,8 +1388,26 @@ def random_to_key(cls, etype, seed): 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: @@ -716,12 +1415,19 @@ def string_to_key(cls, etype, string, salt, params=None): 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 @@ -731,7 +1437,7 @@ def prfplus(key, pepper): return out[: key.ep.seedsize] return Key( - key1.eptype, + key1.etype, key=bytes( _xorbytes( bytearray(prfplus(key1, pepper1)), bytearray(prfplus(key2, pepper2)) diff --git a/scapy/libs/six.py b/scapy/libs/six.py deleted file mode 100644 index 94703a1b202..00000000000 --- a/scapy/libs/six.py +++ /dev/null @@ -1,1000 +0,0 @@ -# Copyright (c) 2010-2020 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 -# 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 published as part of Scapy - -"""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.16.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 - -if PY34: - from importlib.util import spec_from_loader -else: - spec_from_loader = None - - -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 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 find_spec(self, fullname, path, target=None): - if fullname in self.known_modules: - return spec_from_loader(fullname, 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 - - def create_module(self, spec): - return self.load_module(spec.name) - - def exec_module(self, module): - pass - -_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"), - 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("getoutput", "commands", "subprocess"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), - 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"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_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"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - 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"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - 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"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - 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 six.moves.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"), - 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 - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.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 - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.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"), - 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"), - MovedAttribute("parse_http_list", "urllib2", "urllib.request"), - MovedAttribute("parse_keqv_list", "urllib2", "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 - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.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 - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.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 - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __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 six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - 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 - del io - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - _assertNotRegex = "assertNotRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" - _assertNotRegex = "assertNotRegex" -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" - _assertNotRegex = "assertNotRegexpMatches" -_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) - - -def assertNotRegex(self, *args, **kwargs): - return getattr(self, _assertNotRegex)(*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,): - 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): - # This does exactly the same what the :func:`py3:functools.update_wrapper` - # function does on Python versions after 3.2. It sets the ``__wrapped__`` - # attribute on ``wrapper`` object and it doesn't raise an error if any of - # the attributes mentioned in ``assigned`` and ``updated`` are missing on - # ``wrapped`` object. - def _update_wrapper(wrapper, wrapped, - assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - for attr in assigned: - try: - value = getattr(wrapped, attr) - except AttributeError: - continue - else: - setattr(wrapper, attr, value) - for attr in updated: - getattr(wrapper, attr).update(getattr(wrapped, attr, {})) - wrapper.__wrapped__ = wrapped - return wrapper - _update_wrapper.__doc__ = functools.update_wrapper.__doc__ - - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - return functools.partial(_update_wrapper, wrapped=wrapped, - assigned=assigned, updated=updated) - wraps.__doc__ = functools.wraps.__doc__ - -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(type): - - def __new__(cls, name, this_bases, d): - if sys.version_info[:2] >= (3, 7): - # This version introduced PEP 560 that requires a bit - # of extra care (we mimic what is done by __build_class__). - resolved_bases = types.resolve_bases(bases) - if resolved_bases is not bases: - d['__orig_bases__'] = bases - else: - resolved_bases = bases - return meta(name, resolved_bases, d) - - @classmethod - def __prepare__(cls, name, this_bases): - return meta.__prepare__(name, bases) - 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) - if hasattr(cls, '__qualname__'): - orig_vars['__qualname__'] = cls.__qualname__ - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def ensure_binary(s, encoding='utf-8', errors='strict'): - """Coerce **s** to six.binary_type. - - For Python 2: - - `unicode` -> encoded to `str` - - `str` -> `str` - - For Python 3: - - `str` -> encoded to `bytes` - - `bytes` -> `bytes` - """ - if isinstance(s, binary_type): - return s - if isinstance(s, text_type): - return s.encode(encoding, errors) - raise TypeError("not expecting type '%s'" % type(s)) - - -def ensure_str(s, encoding='utf-8', errors='strict'): - """Coerce *s* to `str`. - - For Python 2: - - `unicode` -> encoded to `str` - - `str` -> `str` - - For Python 3: - - `str` -> `str` - - `bytes` -> decoded to `str` - """ - # Optimization: Fast return for the common case. - if type(s) is str: - return s - if PY2 and isinstance(s, text_type): - return s.encode(encoding, errors) - elif PY3 and isinstance(s, binary_type): - return s.decode(encoding, errors) - elif not isinstance(s, (text_type, binary_type)): - raise TypeError("not expecting type '%s'" % type(s)) - return s - - -def ensure_text(s, encoding='utf-8', errors='strict'): - """Coerce *s* to six.text_type. - - For Python 2: - - `unicode` -> `unicode` - - `str` -> `unicode` - - For Python 3: - - `str` -> `str` - - `bytes` -> decoded to `str` - """ - if isinstance(s, binary_type): - return s.decode(encoding, errors) - elif isinstance(s, text_type): - return s - else: - raise TypeError("not expecting type '%s'" % type(s)) - - -def python_2_unicode_compatible(klass): - """ - A class 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 - # 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/libs/winpcapy.py b/scapy/libs/winpcapy.py index 74da47a5a3e..42bfa850921 100644 --- a/scapy/libs/winpcapy.py +++ b/scapy/libs/winpcapy.py @@ -5,7 +5,9 @@ # 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 @@ -223,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 ## @@ -292,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. @@ -335,6 +392,18 @@ class pcap_if(Structure): 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 diff --git a/scapy/main.py b/scapy/main.py index dd4e2c2b243..663d5308b8c 100644 --- a/scapy/main.py +++ b/scapy/main.py @@ -7,20 +7,24 @@ 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 gzip import importlib import io import logging +import os +import pathlib +import pickle +import shutil +import sys import types import warnings + +from itertools import zip_longest from random import choice # Never add any global import, in main.py, that would trigger a @@ -30,20 +34,24 @@ log_loading, Scapy_Exception, ) -import scapy.libs.six as six from scapy.themes import DefaultTheme, BlackAndWhite, apply_ipython_style from scapy.consts import WINDOWS -from scapy.compat import ( +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 = [ @@ -55,24 +63,78 @@ "the wires and in the waves.", "Jean-Claude Van Damme"), ("We are in France, we say Skappee. OK? Merci.", "Sebastien Chabal"), ("Wanna support scapy? Star us on GitHub!", "Satoshi Nakamoto"), - ("What is dead may never die!", "Python 2"), + ("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. @@ -81,11 +143,13 @@ 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' @@ -95,6 +159,26 @@ def _read_config_file(cf, _globals=globals(), _locals=locals(), 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: @@ -119,8 +203,58 @@ def _validate_local(k): return k[0] != "_" and k not in ["range", "map"] -DEFAULT_PRESTART_FILE = _probe_config_file(".scapy_prestart.py") -DEFAULT_STARTUP_FILE = _probe_config_file(".scapy_startup.py") +# 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() + +# 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(): @@ -136,6 +270,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 +310,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 +321,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) @@ -299,14 +458,40 @@ def update_ipython_session(session): 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) + } + + def _scapy_builtins(): # type: () -> Dict[str, Any] """Load Scapy and return all builtins""" - return {k: v - for k, v in six.iteritems( - importlib.import_module(".all", "scapy").__dict__.copy() - ) - if _validate_local(k)} + return { + k: v + for k, v in importlib.import_module(".all", "scapy").__dict__.copy().items() + if _validate_local(k) + } + + +def _scapy_exts(): + # type: () -> Dict[str, Any] + """Load Scapy exts and return their builtins""" + from scapy.config import conf + 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 def save_session(fname="", session=None, pickleProto=-1): @@ -326,18 +511,18 @@ def save_session(fname="", session=None, pickleProto=-1): log_interactive.info("Saving session into [%s]", fname) if not session: - try: + if conf.interactive_shell in ["ipython", "ptipython"]: from IPython import get_ipython session = get_ipython().user_ns - except Exception: - session = six.moves.builtins.__dict__["scapy_session"] + else: + session = builtins.__dict__["scapy_session"] if not session: log_interactive.error("No session found ?!") return ignore = session.get("_scpybuiltins", []) - hard_ignore = ["scapy_session", "In", "Out"] + hard_ignore = ["scapy_session", "In", "Out", "open"] to_be_saved = session.copy() for k in list(to_be_saved): @@ -350,11 +535,15 @@ def save_session(fname="", session=None, pickleProto=-1): del to_be_saved[k] elif k in ignore or k in hard_ignore: del to_be_saved[k] - elif isinstance(i, (type, types.ModuleType)): + elif isinstance(i, (type, types.ModuleType, types.FunctionType)): if k[0] != "_": - log_interactive.warning("[%s] (%s) can't be saved.", k, - type(to_be_saved[k])) + log_interactive.warning("[%s] (%s) can't be saved.", k, type(i)) del to_be_saved[k] + else: + try: + pickle.dumps(i) + except Exception: + log_interactive.warning("[%s] (%s) can't be saved.", k, type(i)) try: os.rename(fname, fname + ".bak") @@ -362,7 +551,7 @@ def save_session(fname="", session=None, pickleProto=-1): pass f = gzip.open(fname, "wb") - six.moves.cPickle.dump(to_be_saved, f, pickleProto) + pickle.dump(to_be_saved, f, pickleProto) f.close() @@ -377,15 +566,15 @@ def load_session(fname=None): if fname is None: fname = conf.session try: - s = six.moves.cPickle.load(gzip.open(fname, "rb")) + s = pickle.load(gzip.open(fname, "rb")) except IOError: try: - s = six.moves.cPickle.load(open(fname, "rb")) + s = pickle.load(open(fname, "rb")) except IOError: # Raise "No such file exception" raise - scapy_session = six.moves.builtins.__dict__["scapy_session"] + scapy_session = builtins.__dict__["scapy_session"] s.update({k: scapy_session[k] for k in scapy_session["_scpybuiltins"]}) scapy_session.clear() scapy_session.update(s) @@ -404,22 +593,46 @@ def update_session(fname=None): if fname is None: fname = conf.session try: - s = six.moves.cPickle.load(gzip.open(fname, "rb")) + s = pickle.load(gzip.open(fname, "rb")) except IOError: - s = six.moves.cPickle.load(open(fname, "rb")) - scapy_session = six.moves.builtins.__dict__["scapy_session"] + s = pickle.load(open(fname, "rb")) + scapy_session = builtins.__dict__["scapy_session"] scapy_session.update(s) update_ipython_session(scapy_session) +@overload +def init_session(session_name, # type: Optional[Union[str, None]] + mydict, # type: Optional[Union[Dict[str, Any], None]] + ret, # type: Literal[True] + ): + # type: (...) -> Dict[str, Any] + pass + + +@overload +def init_session(session_name, # type: Optional[Union[str, None]] + mydict=None, # type: Optional[Union[Dict[str, Any], None]] + ret=False, # type: Literal[False] + ): + # type: (...) -> None + pass + + def init_session(session_name, # type: Optional[Union[str, None]] mydict=None, # type: Optional[Union[Dict[str, Any], None]] ret=False, # type: bool ): - # type: (...) -> Optional[Dict[str, Any]] + # type: (...) -> Union[Dict[str, Any], None] from scapy.config import conf SESSION = {} # type: Optional[Dict[str, Any]] + # Load Scapy + scapy_builtins = _scapy_builtins() + + # Load exts + scapy_builtins.update(_scapy_exts()) + if session_name: try: os.stat(session_name) @@ -428,10 +641,9 @@ def init_session(session_name, # type: Optional[Union[str, None]] else: try: try: - SESSION = six.moves.cPickle.load(gzip.open(session_name, - "rb")) + SESSION = pickle.load(gzip.open(session_name, "rb")) except IOError: - SESSION = six.moves.cPickle.load(open(session_name, "rb")) + SESSION = pickle.load(open(session_name, "rb")) log_loading.info("Using existing session [%s]", session_name) except ValueError: msg = "Error opening Python3 pickled session on Python2 [%s]" @@ -455,15 +667,12 @@ def init_session(session_name, # type: Optional[Union[str, None]] else: SESSION = {"conf": conf} - # Load Scapy - scapy_builtins = _scapy_builtins() - SESSION.update(scapy_builtins) SESSION["_scpybuiltins"] = scapy_builtins.keys() - six.moves.builtins.__dict__["scapy_session"] = SESSION + 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) if ret: return SESSION @@ -477,7 +686,7 @@ def init_session(session_name, # type: Optional[Union[str, None]] 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(' ') @@ -501,8 +710,90 @@ 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 +def get_fancy_banner(mini: Optional[bool] = None) -> str: + """ + Generates the fancy Scapy banner + + :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="" + ) + ) + + +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. """ @@ -530,7 +821,7 @@ def interact(mydict=None, argv=None, mybanner=None, loglevel=logging.INFO): if opt == "-h": _usage() elif opt == "-H": - conf.fancy_prompt = False + conf.fancy_banner = False conf.verb = 1 conf.logLevel = logging.WARNING elif opt == "-s": @@ -558,110 +849,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] + if PRESTART_FILE: + _read_config_file( + PRESTART_FILE, + interactive=True, + _locals=_scapy_prestart_builtins(), + default=DEFAULT_PRESTART, + ) + SESSION = init_session(session_name, 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: - log_loading.warning( - "IPython not available. Using standard Python shell " - "instead.\nAutoCompletion, History are disabled." - ) - if WINDOWS: - log_loading.warning( - "On Windows, colors are also disabled" - ) - conf.color_theme = BlackAndWhite() - IPYTHON = False - else: - IPYTHON = True + # 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: - IPYTHON = False + banner_text = "Welcome to Scapy (%s)" % conf.version - if conf.fancy_prompt: - from scapy.utils import get_terminal_width - mini_banner = (get_terminal_width() or 84) <= 75 + # 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 - 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 + [""] + # 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( + "No alternative Python interpreters found ! " + "Using standard Python shell instead." + ) + conf.interactive_shell = "python" + + 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" + + # 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: @@ -670,7 +1021,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 + "\"\"\")"] @@ -687,26 +1038,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 %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. - cfg.Completer.use_jedi = False + # 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) + else: + raise ValueError("Invalid conf.interactive_shell") if conf.session: save_session(conf.session, SESSION) diff --git a/scapy/modules/__init__.py b/scapy/modules/__init__.py index 0c399dc5ef3..1bf976f08af 100644 --- a/scapy/modules/__init__.py +++ b/scapy/modules/__init__.py @@ -6,3 +6,6 @@ """ 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 0c72d40a6fd..f4178b62f47 100644 --- a/scapy/modules/krack/__init__.py +++ b/scapy/modules/krack/__init__.py @@ -22,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 bb05728a271..5fd6fc99ae8 100644 --- a/scapy/modules/krack/automaton.py +++ b/scapy/modules/krack/automaton.py @@ -722,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 cfe2e1307e1..47b7c9364f1 100644 --- a/scapy/modules/krack/crypto.py +++ b/scapy/modules/krack/crypto.py @@ -10,7 +10,6 @@ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms from cryptography.hazmat.backends import default_backend -import scapy.libs.six as six from scapy.compat import orb, chb from scapy.layers.dot11 import Dot11TKIP from scapy.utils import mac2str @@ -158,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..dd518ec0ecc --- /dev/null +++ b/scapy/modules/ldaphero.py @@ -0,0 +1,2051 @@ +# 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.smb2 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: + """ + LDAP Hero - LDAP GUI browser over Scapy's LDAP_Client + + :param ssp: the SSP object to use when binding. + :param mech: the LDAP_BIND_MECHS to use when binding. + :param simple_username: if provided, used for Simple binding (instead of the 'ssp') + :param simple_password: + :param encrypt: request encryption by default (useful when using 'ssp') + :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') + """ + + def __init__( + self, + ssp: SSP = None, + mech: LDAP_BIND_MECHS = None, + simple_username: str = None, + simple_password: str = None, + encrypt: bool = False, + host: str = None, + port: int = None, + ssl: bool = False, + ): + self.client = LDAP_Client() + self.ssp = ssp + self.mech = mech + self.simple_username = simple_username + self.simple_password = simple_password + 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)) + 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, + 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) + + bindtypefrm = ttk.LabelFrame( + dlg, + text="Bind type", + ) + bindtypev = tk.StringVar() + ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="Sicily bind (NTLM)", + value=LDAP_BIND_MECHS.SICILY.value, + ).pack(anchor=tk.W) + ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="GSSAPI bind (Kerberos)", + value=LDAP_BIND_MECHS.SASL_GSSAPI.value, + ).pack(anchor=tk.W) + ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="SPNEGO bind (NTLM/Kerberos)", + value=LDAP_BIND_MECHS.SASL_GSS_SPNEGO.value, + ).pack(anchor=tk.W) + ttk.Radiobutton( + bindtypefrm, + variable=bindtypev, + text="Simple bind", + value=LDAP_BIND_MECHS.SIMPLE.value, + ).pack(anchor=tk.W) + bindtypefrm.grid(row=3, column=0, columnspan=2) + + encryptv = tk.BooleanVar() + encryptv.set(self.encrypt) + ttk.Label(dlg, text="Encrypt traffic after bind").grid(row=4, column=0) + encrbtn = ttk.Checkbutton(dlg, variable=encryptv) + encrbtn.grid(row=4, column=1) + + ttk.Button(dlg, text="OK", command=popup.dismiss).grid(row=5, column=0) + ttk.Button(dlg, text="Cancel", command=popup.cancel).grid(row=5, 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) + encrbtn.config(state=tk.DISABLED) + encryptv.set(False) + elif bindtype == LDAP_BIND_MECHS.SICILY: + domentry.config(state=tk.DISABLED) + encrbtn.config(state=tk.NORMAL) + else: + domentry.config(state=tk.NORMAL, textvariable=domainv) + encrbtn.config(state=tk.NORMAL) + + bindtypev.trace("w", 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()) + 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 + encrypt = False + elif bindtype == LDAP_BIND_MECHS.SICILY: + 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, + encrypt=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 + item = self.tk_tree.selection()[0] + + # 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 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 957d80d0236..38a0521fff9 100644 --- a/scapy/modules/nmap.py +++ b/scapy/modules/nmap.py @@ -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.libs.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()) + 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, [] + # 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 b026bd3696d..09bef7e4585 100644 --- a/scapy/modules/p0f.py +++ b/scapy/modules/p0f.py @@ -7,8 +7,6 @@ Clone of p0f v3 passive OS fingerprinting """ -from __future__ import absolute_import -from __future__ import print_function import re import struct import random @@ -22,7 +20,6 @@ from scapy.layers.inet6 import IPv6 from scapy.volatile import RandByte, RandShort, RandString from scapy.error import warning -from scapy.libs.six import integer_types, string_types _p0fpaths = ["/etc/p0f", "/usr/share/p0f", "/opt/local"] conf.p0f_base = select_path(_p0fpaths, "p0f.fp") @@ -336,13 +333,16 @@ def __init__(self, label_id, sig_line): class p0fKnowledgeBase(KnowledgeBase): """ - self.base = { - "mtu" (str): [sig(tuple), ...] - "tcp"/"http" (str): { - direction (str): [sig(tuple), ...] + .. code:: + + self.base = { + "mtu" (str): [sig(tuple), ...] + "tcp"/"http" (str): { + direction (str): [sig(tuple), ...] } - } - self.labels = (label(tuple), ...) + } + self.labels = (label(tuple), ...) + """ def lazy_init(self): try: @@ -756,10 +756,12 @@ def add_field(name, value): 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 + """ + 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: + 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 @@ -768,7 +770,8 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, 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.""" + For now, only TCP SYN/SYN+ACK packets are supported. + """ pkt = validate_packet(pkt) if not osgenre and not signature: @@ -778,7 +781,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, tcp_type = tcp.flags & (0x02 | 0x10) # SYN / SYN+ACK if signature: - if isinstance(signature, string_types): + if isinstance(signature, str): sig, _ = TCP_Signature.from_raw_sig(signature) else: raise TypeError("Unsupported signature type") @@ -834,7 +837,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, # 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, integer_types) else None + 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")) diff --git a/scapy/modules/p0fv2.py b/scapy/modules/p0fv2.py index 5b7b2da848a..353288b2bda 100644 --- a/scapy/modules/p0fv2.py +++ b/scapy/modules/p0fv2.py @@ -7,8 +7,6 @@ Clone of p0f v2 passive OS fingerprinting """ -from __future__ import absolute_import -from __future__ import print_function import time import struct import os @@ -23,7 +21,6 @@ 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.libs import six if conf.route is None: # unused import, only to initialize conf.route import scapy.route # noqa: F401 @@ -409,7 +406,7 @@ def p0f_impersonate(pkt, osgenre=None, osdetails=None, signature=None, # 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 + 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))] diff --git a/scapy/modules/ticketer.py b/scapy/modules/ticketer.py new file mode 100644 index 00000000000..87c591753bc --- /dev/null +++ b/scapy/modules/ticketer.py @@ -0,0 +1,2572 @@ +# 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.smb2 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) + + +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: "vno" in pkt.fields is not 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: + print("CCache tickets:") + + for i, cred in enumerate(self.ccache.credentials): + if cred.keyblock.keytype == 0: + continue + print( + "%s. %s -> %s" + % ( + i, + cred.client.toPN(), + cred.server.toPN(), + ) + ) + print(cred.sprintf(" %ticket_flags%")) + 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. + """ + del self.ccache.credentials[i] + + 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 + 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 + 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: + self.ccache.credentials.append(cred) + + 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 + component = None + try: + component, realm = _parse_upn(principal) + if salt is None and key is None: + salt = krb_get_salt(principal) + except ValueError: + try: + component, realm = _parse_spn(principal) + except ValueError: + raise ValueError("Invalid principal ! (must be UPN or SPN)") + + 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, + ) + for x in component.split("/") + ], + timestamp=int(datetime.now().timestamp()), + vno8=kvno if kvno < 256 else None, + key=KTKeyBlock( + keytype=key.etype, + keyvalue=key.key, + ), + vno=None if kvno < 256 else 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 ssp(self, i): + """ + 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) + return KerberosSSP( + ST=ticket, + KEY=sessionkey, + UPN=upn, + SPN=spn, + ) + elif isinstance(i, str): + spn = i + key = self.get_cred(spn) + return KerberosSSP( + SPN=spn, + KEY=key, + ) + 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, + ip=None, + key=None, + password=None, + realm=None, + fast=False, + armor_with=None, + spn=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, + **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) diff --git a/scapy/modules/voip.py b/scapy/modules/voip.py index edba6bde4a1..c0eb1ce006b 100644 --- a/scapy/modules/voip.py +++ b/scapy/modules/voip.py @@ -7,7 +7,6 @@ VoIP (Voice over IP) related functions """ -from __future__ import absolute_import import subprocess ################### # Listen VoIP # diff --git a/scapy/packet.py b/scapy/packet.py index 31188931664..3fb2eac6d92 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -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 @@ -31,8 +31,11 @@ EnumField, Field, FlagsField, + FlagValue, + MayEnd, MultiEnumField, MultipleTypeField, + PadField, PacketListField, RawVal, StrField, @@ -47,10 +50,9 @@ pretty_list, EDecimal from scapy.error import Scapy_Exception, log_runtime, warning from scapy.libs.test_pyx import PYX -import scapy.libs.six as six # Typing imports -from scapy.compat import ( +from typing import ( Any, Callable, Dict, @@ -66,6 +68,8 @@ Sequence, cast, ) +from scapy.compat import Self + try: import pyx except ImportError: @@ -75,9 +79,11 @@ _T = TypeVar("_T", Dict[str, Any], Optional[Dict[str, Any]]) -# six.with_metaclass typing is glitchy -class Packet(six.with_metaclass(Packet_metaclass, # type: ignore - BasePacket, _CanvasDumpExtended)): +class Packet( + BasePacket, + _CanvasDumpExtended, + metaclass=Packet_metaclass +): __slots__ = [ "time", "sent_time", "name", "default_fields", "fields", "fieldtype", @@ -85,6 +91,7 @@ class Packet(six.with_metaclass(Packet_metaclass, # type: ignore "packetfields", "original", "explicit", "raw_packet_cache", "raw_packet_cache_fields", "_pkt", "post_transforms", + "stop_dissection_after", # then payload, underlayer and parent "payload", "underlayer", "parent", "name", @@ -94,10 +101,11 @@ class Packet(six.with_metaclass(Packet_metaclass, # type: ignore "direction", "sniffed_on", # handle snaplen Vs real length "wirelen", - "comment" + "comments", + "process_information" ] name = None - fields_desc = [] # type: Sequence[AnyField] + 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]]] @@ -119,13 +127,23 @@ def from_hexcap(cls): 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): # type: () -> None - 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 + 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"", # type: Union[bytes, bytearray] @@ -133,6 +151,7 @@ def __init__(self, _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 @@ -147,8 +166,8 @@ def __init__(self, self.fields = {} # type: Dict[str, Any] self.fieldtype = {} # type: Dict[str, AnyField] self.packetfields = [] # type: List[AnyField] - self.payload = NoPayload() - self.init_fields() + self.payload = NoPayload() # type: Packet + self.init_fields(bool(_pkt)) self.underlayer = _underlayer self.parent = _parent if isinstance(_pkt, bytearray): @@ -160,7 +179,9 @@ def __init__(self, self.wirelen = None # type: Optional[int] self.direction = None # type: Optional[int] self.sniffed_on = None # type: Optional[_GlobInterfaceType] - self.comment = None # type: Optional[bytes] + 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: @@ -202,6 +223,26 @@ def __init__(self, Optional[bytes], ] + @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 + def __reduce__(self): # type: () -> Tuple[Type[Packet], Tuple[bytes], Packet._PickleType] """Used by pickling methods""" @@ -232,8 +273,8 @@ def __deepcopy__(self, """Used by copy.deepcopy""" return self.copy() - def init_fields(self): - # type: () -> None + def init_fields(self, for_dissect_only=False): + # type: (bool) -> None """ Initialize each fields of the fields_desc dict """ @@ -241,7 +282,7 @@ 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, # type: Sequence[AnyField] @@ -259,8 +300,8 @@ def do_init_fields(self, # We set default_fields last to avoid race issues self.default_fields = default_fields - def do_init_cached_fields(self): - # type: () -> None + 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 @@ -279,6 +320,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] @@ -313,8 +358,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) @@ -393,8 +437,7 @@ def remove_parent(self, other): point to the list owner packet.""" self.parent = None - def copy(self): - # type: () -> Packet + def copy(self) -> Self: """Returns a deep copy of the instance.""" clone = self.__class__() clone.fields = self.copy_fields_dict(self.fields) @@ -412,7 +455,9 @@ def copy(self): clone.payload = self.payload.copy() clone.payload.add_underlayer(clone) clone.time = self.time - clone.comment = self.comment + clone.comments = self.comments + clone.direction = self.direction + clone.sniffed_on = self.sniffed_on return clone def _resolve_alias(self, attr): @@ -469,7 +514,8 @@ def setfieldval(self, attr, val): 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 @@ -522,7 +568,7 @@ def _superdir(self): """ 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__) @@ -574,22 +620,16 @@ def __repr__(self): repr(self.payload), ct.punct(">")) - if six.PY2: - def __str__(self): - # type: () -> str - return self.build() - else: - def __str__(self): - # type: () -> str - 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) -> Packet + # type: (Any) -> Self if isinstance(other, Packet): cloneA = self.copy() cloneB = other.copy() @@ -638,14 +678,30 @@ def copy_fields_dict(self, fields): 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() @@ -658,12 +714,12 @@ 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 @@ -681,7 +737,7 @@ def self_build(self): except Exception as ex: try: ex.args = ( - "While dissecting field '%s': " % f.name + + "While building field '%s': " % f.name + ex.args[0], ) + ex.args[1:] except (AttributeError, IndexError): @@ -981,17 +1037,21 @@ def do_dissect(self, s): _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 @@ -1004,9 +1064,22 @@ def do_dissect_payload(self, s): :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: @@ -1048,7 +1121,7 @@ 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)): + for k, v in fval.items()): return cls # type: ignore except AttributeError: pass @@ -1069,7 +1142,7 @@ 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: @@ -1092,7 +1165,9 @@ def clone_with(self, payload=None, **kargs): self.raw_packet_cache_fields ) pkt.wirelen = self.wirelen - pkt.comment = self.comment + pkt.comments = self.comments + pkt.sniffed_on = self.sniffed_on + pkt.direction = self.direction if payload is not None: pkt.add_payload(payload) return pkt @@ -1132,8 +1207,8 @@ def loop(todo, done, self=self): 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) @@ -1222,7 +1297,7 @@ def haslayer(self, cls, _subclass=None): if _subclass: match = issubtype else: - match = lambda cls1, cls2: bool(cls1 == cls2) + 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 @@ -1255,7 +1330,7 @@ def getlayer(self, if _subclass: match = issubtype else: - match = lambda cls1, cls2: bool(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) @@ -1274,7 +1349,7 @@ def getlayer(self, 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 @@ -1377,27 +1452,42 @@ def _show_or_dump(self, """ 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 - pad = max(0, 10 - len(f.name)) * " " - s += "%s \\%s%s\\\n" % (label_lvl + lvl, ncol(f.name), pad) + s += "%s %s%s%s%s\n" % (label_lvl + lvl, + ct.punct("\\"), + ncol(f.name), + pad, + ct.punct("\\")) fvalue_gen = SetGen( fvalue, _iterpacket=0 @@ -1405,7 +1495,6 @@ def _show_or_dump(self, 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: - pad = max(0, 10 - len(f.name)) * " " begn = "%s %s%s%s " % (label_lvl + lvl, ncol(f.name), pad, @@ -1635,48 +1724,99 @@ def decode_payload_as(self, cls): pp = pp.underlayer self.payload.dissection_done(pp) - def command(self): - # type: () -> str + 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)) + 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): - fv = "[%s]" % ", ".join( - getattr(x, 'command', lambda: repr(x))() - for x in fv - ) - elif isinstance(fld, FlagsField): + 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() + fv = fv.command(json=json) else: - fv = repr(fld.i2h(self, 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 json(self): + # type: () -> str + """ + Returns a JSON representing the 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. + """ + 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) -> Packet + # 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 @@ -1790,7 +1930,7 @@ def hashret(self): return b"" def answers(self, other): - # type: (NoPayload) -> bool + # type: (Packet) -> bool return isinstance(other, (NoPayload, conf.padding_layer)) # noqa: E501 def haslayer(self, cls, _subclass=None): @@ -1840,6 +1980,10 @@ def command(self): # type: () -> str return "" + def json(self): + # type: () -> str + return "" + def route(self): # type: () -> Tuple[None, None, None] return (None, None, None) @@ -1857,7 +2001,11 @@ class Raw(Packet): 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): @@ -1878,7 +2026,7 @@ def mysummary(self): class Padding(Raw): name = "Padding" - def self_build(self, field_pos_list=None): + def self_build(self): # type: (Optional[Any]) -> bytes return b"" @@ -1939,7 +2087,7 @@ def bind_top_down(lower, # type: Type[Packet] """ 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 @@ -1983,7 +2131,7 @@ def split_bottom_up(lower, # type: Type[Packet] 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)] @@ -2002,9 +2150,9 @@ def split_top_down(lower, # type: Type[Packet] 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() + upper._overload_fields = upper._overload_fields.copy() # type: ignore del upper._overload_fields[lower] @@ -2068,20 +2216,18 @@ def explore(layer=None): # 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() # type: ignore + call_ptk = lambda x: x.run() # 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 @@ -2103,10 +2249,6 @@ def explore(layer=None): 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 @@ -2138,7 +2280,7 @@ def explore(layer=None): # 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", []) # type: List[str] cur_path = "" @@ -2155,15 +2297,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" ) @@ -2222,7 +2362,7 @@ def explore(layer=None): # Print print(conf.color_theme.layer_name("Packets contained in %s:" % result)) rtlst = [] # type: List[Tuple[Union[str, List[str]], ...]] - rtlst = [(lay.__name__ or "", lay._name or "") for lay in all_layers] + rtlst = [(lay.__name__ or "", cast(str, lay._name) or "") for lay in all_layers] print(pretty_list(rtlst, [("Class", "Name")], borders=True)) @@ -2251,29 +2391,29 @@ def _pkt_ls(obj, # type: Union[Packet, Type[Packet]] name = cur_fld.name default = cur_fld.default if verbose and isinstance(cur_fld, EnumField) \ - and hasattr(cur_fld, "i2s"): + 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( - cast(Packet, obj if is_pkt else obj()) - ) + 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( - cast(Packet, 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 @@ -2316,16 +2456,14 @@ def ls(obj=None, # type: Optional[Union[str, Packet, Type[Packet]]] :param case_sensitive: if obj is a string, is it case sensitive? :param verbose: """ - is_string = isinstance(obj, str) - - 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( - cast(str, obj), + obj, 0 if case_sensitive else re.I ) # We first order by accuracy, then length @@ -2349,7 +2487,7 @@ def ls(obj=None, # type: Optional[Union[str, Packet, Type[Packet]]] else: try: fields = _pkt_ls( - obj, # type: ignore + obj, verbose=verbose ) is_pkt = isinstance(obj, Packet) @@ -2397,13 +2535,25 @@ def rfc(cls, ret=False, legend=True): # when formatted, from its length in bits 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: @@ -2483,11 +2633,14 @@ def rfc(cls, ret=False, legend=True): # Fuzzing # ############# +_P = TypeVar('_P', bound=Packet) + + @conf.commands.register -def fuzz(p, # type: Packet +def fuzz(p, # type: _P _inplace=0, # type: int ): - # type: (...) -> Packet + # type: (...) -> _P """ Transform a layer into a fuzzy layer by replacing some default values by random objects. @@ -2497,7 +2650,7 @@ def fuzz(p, # type: Packet """ if not _inplace: p = p.copy() - q = p + q = cast(Packet, p) while not isinstance(q, NoPayload): new_default_fields = {} multiple_type_fields = [] # type: List[str] @@ -2518,9 +2671,10 @@ def fuzz(p, # type: Packet # 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: fld = cast(MultipleTypeField, q.get_field(name)) diff --git a/scapy/pipetool.py b/scapy/pipetool.py index 8ee9b1bfc91..a28b3534f1e 100644 --- a/scapy/pipetool.py +++ b/scapy/pipetool.py @@ -3,11 +3,10 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -from __future__ import print_function import os +import queue import subprocess import time -import scapy.libs.six as six from threading import Lock, Thread from scapy.automaton import ( @@ -20,7 +19,7 @@ from scapy.config import conf from scapy.utils import get_temp_file, do_graph -from scapy.compat import ( +from typing import ( Any, Callable, Dict, @@ -31,7 +30,6 @@ Union, Type, TypeVar, - _Generic_metaclass, cast, ) @@ -236,7 +234,7 @@ def graph(self, **kargs): do_graph(graph, **kargs) -class _PipeMeta(_Generic_metaclass): +class _PipeMeta(type): def __new__(cls, name, # type: str bases, # type: Tuple[type, ...] @@ -253,8 +251,7 @@ def __new__(cls, _TS = TypeVar("_TS", bound="TriggerSink") -@six.add_metaclass(_PipeMeta) -class Pipe: +class Pipe(metaclass=_PipeMeta): def __init__(self, name=None): # type: (Optional[str]) -> None self.sources = set() # type: Set['Pipe'] @@ -360,8 +357,8 @@ def stop(self): class Source(Pipe, ObjectPipe[Any]): def __init__(self, name=None): # type: (Optional[str]) -> None - Pipe.__init__(self, name=name) ObjectPipe.__init__(self, name) + Pipe.__init__(self, name=name) self.is_exhausted = False def _read_message(self): @@ -609,7 +606,7 @@ def send(self, msg): class PeriodicSource(ThreadGenSource): - """Generage messages periodically on low exit: + """Generate messages periodically on low exit: .. code:: @@ -719,6 +716,7 @@ def _start_unix(self): 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]) @@ -787,7 +785,7 @@ 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 @@ -815,7 +813,7 @@ def recv(self, block=True, timeout=None): """ try: return self.q.get(block=block, timeout=timeout) - except six.moves.queue.Empty: + except queue.Empty: return None diff --git a/scapy/plist.py b/scapy/plist.py index ad5995cb84a..0ea33d91f9d 100644 --- a/scapy/plist.py +++ b/scapy/plist.py @@ -8,12 +8,10 @@ """ -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, @@ -25,10 +23,9 @@ from scapy.utils import do_graph, hexdump, make_table, make_lined_table, \ make_tex_table, issubtype from functools import reduce -import scapy.libs.six as six # typings -from scapy.compat import ( +from typing import ( Any, Callable, DefaultDict, @@ -36,7 +33,6 @@ Generic, Iterator, List, - NamedTuple, Optional, Tuple, Type, @@ -46,6 +42,11 @@ ) from scapy.packet import Packet +try: + import pyx +except ImportError: + pass + if TYPE_CHECKING: from scapy.libs.matplot import Line2D @@ -62,8 +63,7 @@ _Inner = TypeVar("_Inner", Packet, QueryAnswer) -@six.add_metaclass(PacketList_metaclass) -class _PacketList(Generic[_Inner]): +class _PacketList(Generic[_Inner], metaclass=PacketList_metaclass): __slots__ = ["stats", "res", "listname"] def __init__(self, @@ -202,12 +202,6 @@ def summary(self, :param lfilter: truth function to apply to each packet to decide whether it will be displayed """ - # Python 2 backward compatibility - if prn is not None: - prn = lambda_tuple_converter(prn) - if lfilter is not None: - lfilter = lambda_tuple_converter(lfilter) - for r in self.res: if lfilter is not None: if not lfilter(*r): @@ -229,12 +223,6 @@ def nsummary(self, :param lfilter: truth function to apply to each packet to decide whether it will be displayed """ - # Python 2 backward compatibility - if prn is not None: - prn = lambda_tuple_converter(prn) - if lfilter is not None: - lfilter = lambda_tuple_converter(lfilter) - for i, res in enumerate(self.res): if lfilter is not None: if not lfilter(*res): @@ -256,9 +244,6 @@ def filter(self, func): function has to take a packet as the only argument and return a boolean value. """ - # Python 2 backward compatibility - func = lambda_tuple_converter(func) - return self.__class__([x for x in self.res if func(*x)], name="filtered %s" % self.listname) @@ -298,11 +283,6 @@ def plot(self, MATPLOTLIB_DEFAULT_PLOT_KARGS ) - # Python 2 backward compatibility - f = lambda_tuple_converter(f) - if lfilter is not None: - lfilter = lambda_tuple_converter(lfilter) - # Get the list of packets if lfilter is None: lst_pkts = [f(*e) for e in self.res] @@ -383,11 +363,6 @@ def multiplot(self, MATPLOTLIB_DEFAULT_PLOT_KARGS ) - # Python 2 backward compatibility - f = lambda_tuple_converter(f) - if lfilter is not None: - lfilter = lambda_tuple_converter(lfilter) - # Get the list of packets if lfilter is None: lst_pkts = (f(*e) for e in self.res) @@ -404,11 +379,11 @@ def multiplot(self, 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 @@ -471,7 +446,7 @@ def nzpadding(self, lfilter=None): p = self._elt2pkt(res) if p.haslayer(conf.padding_layer): pad = p.getlayer(conf.padding_layer).load # type: ignore - if pad == pad[0] * len(pad): + 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"), @@ -512,8 +487,8 @@ def _getsrcdst(pkt): raise TypeError() getsrcdst = _getsrcdst conv = {} # type: Dict[Tuple[Any, ...], Any] - for p in self.res: - p = self._elt2pkt(p) + for elt in self.res: + p = self._elt2pkt(elt) try: c = getsrcdst(p) except Exception: @@ -528,7 +503,7 @@ 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 ) @@ -587,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' @@ -621,13 +596,13 @@ def minmax(x): 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: @@ -805,7 +780,7 @@ def sr(self, multi=False, lookahead=None): _PacketIterable = Union[ - List[Packet], + Sequence[Packet], Packet, SetGen[Packet], _PacketList[Packet] diff --git a/scapy/pton_ntop.py b/scapy/pton_ntop.py index 0cce2d588b8..9fa13e89012 100644 --- a/scapy/pton_ntop.py +++ b/scapy/pton_ntop.py @@ -10,16 +10,13 @@ without IPv6 support, on Windows for instance. """ -from __future__ import absolute_import import socket import re import binascii from scapy.compat import plain_str, hex_bytes, bytes_encode, bytes_hex -from scapy.compat import ( - AddressFamily, - Union, -) +# 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") @@ -84,12 +81,14 @@ def _inet6_pton(addr): def inet_pton(af, addr): - # type: (AddressFamily, Union[bytes, str]) -> bytes + # 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: @@ -132,11 +131,13 @@ def _inet6_ntop(addr): def inet_ntop(af, addr): - # type: (AddressFamily, bytes) -> str + # 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/scapy/py.typed b/scapy/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/scapy/route.py b/scapy/route.py index 3d0d51d006a..3c2227be834 100644 --- a/scapy/route.py +++ b/scapy/route.py @@ -8,15 +8,13 @@ """ -from __future__ import absolute_import - from scapy.compat import plain_str from scapy.config import conf from scapy.error import Scapy_Exception, warning from scapy.interfaces import resolve_iface from scapy.utils import atol, ltoa, itom, pretty_list -from scapy.compat import ( +from typing import ( Any, Dict, List, @@ -34,11 +32,13 @@ class Route: def __init__(self): # type: () -> None self.routes = [] # type: List[Tuple[int, int, str, str, str, int]] - self.resync() + self.invalidate_cache() + if conf.route_autoload: + self.resync() def invalidate_cache(self): # type: () -> None - self.cache = {} # type: Dict[str, Tuple[str, str, str]] + self.cache = {} # type: Dict[Tuple[str, Optional[str]], Tuple[str, str, str]] def resync(self): # type: () -> None @@ -69,7 +69,6 @@ def make_route(self, metric=1, # type: int ): # type: (...) -> Tuple[int, int, str, str, str, int] - from scapy.arch import get_if_addr if host is not None: thenet, msk = host, 32 elif net is not None: @@ -86,20 +85,41 @@ def make_route(self, 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): # type: (*Any, **Any) -> None - """Ex: - add(net="192.168.1.0/24",gw="1.2.3.4") + """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): # type: (*Any, **Any) -> None - """delt(host|net, gw|dev)""" + """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: @@ -145,16 +165,18 @@ def ifadd(self, iff, 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): - # type: (Optional[str], int) -> Tuple[str, str, str] + 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): @@ -162,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: @@ -178,8 +200,10 @@ 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, (conf.loopback_name, a, "0.0.0.0")) # noqa: E501 ) @@ -188,14 +212,19 @@ def route(self, dst=None, verbose=conf.verb): if not paths: if verbose: - warning("No route found (no default route?)") - return conf.loopback_name, "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): @@ -217,5 +246,5 @@ def get_if_bcast(self, iff): conf.route = Route() -# Load everything, update conf.iface -conf.ifaces.reload() +# Update conf.iface +conf.ifaces.load_confiface() diff --git a/scapy/route6.py b/scapy/route6.py index 00b4049e736..4062359fea7 100644 --- a/scapy/route6.py +++ b/scapy/route6.py @@ -13,7 +13,6 @@ # Routing/Interfaces stuff # ############################################################################# -from __future__ import absolute_import import socket from scapy.config import conf from scapy.interfaces import resolve_iface, NetworkInterface @@ -26,7 +25,7 @@ from scapy.error import warning, log_loading from scapy.utils import pretty_list -from scapy.compat import ( +from typing import ( Any, Dict, List, @@ -41,8 +40,11 @@ class Route6: def __init__(self): # type: () -> None - self.resync() + 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): # type: () -> None @@ -51,8 +53,8 @@ def invalidate_cache(self): def flush(self): # type: () -> None self.invalidate_cache() - self.ipv6_ifaces = set() # type: Set[Union[str, NetworkInterface]] - self.routes = [] # type: List[Tuple[str, int, str, str, List[str], int]] # noqa: E501 + self.routes.clear() + self.ipv6_ifaces.clear() def resync(self): # type: () -> None @@ -218,7 +220,7 @@ def ifadd(self, iff, addr): self.ipv6_ifaces.add(iff) def route(self, dst="", dev=None, verbose=conf.verb): - # type: (str, Optional[Any], int) -> Tuple[str, str, str] + # type: (str, Optional[str], int) -> Tuple[str, str, str] """ Provide best route to IPv6 destination address, based on Scapy internal routing table content. @@ -250,35 +252,6 @@ def route(self, dst="", 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 = conf.loopback_name - - 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: @@ -307,7 +280,7 @@ def route(self, dst="", dev=None, verbose=conf.verb): if verbose: warning("No route found for IPv6 destination %s " "(no default route?)", dst) - return (conf.loopback_name, "::", "::") + 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])) diff --git a/scapy/scapypipes.py b/scapy/scapypipes.py index 4959837462d..9311eda22f2 100644 --- a/scapy/scapypipes.py +++ b/scapy/scapypipes.py @@ -3,11 +3,10 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -from __future__ import print_function +from queue import Queue, Empty import socket import subprocess -from scapy.libs.six.moves.queue import Queue, Empty from scapy.automaton import ObjectPipe from scapy.config import conf from scapy.compat import raw @@ -17,7 +16,7 @@ from scapy.utils import ContextManagerSubprocess, PcapReader, PcapWriter from scapy.supersocket import SuperSocket -from scapy.compat import ( +from typing import ( Any, Callable, List, @@ -204,16 +203,17 @@ class WrpcapSink(Sink): This attribute has no effect after calling :py:meth:`PipeEngine.start`. """ - def __init__(self, fname, name=None, linktype=None): - # type: (str, Optional[str], Optional[int]) -> 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 # type: Optional[PcapWriter] self.linktype = linktype + self.kwargs = kwargs def start(self): # type: () -> None - self.f = PcapWriter(self.fname, linktype=self.linktype) + self.f = PcapWriter(self.fname, linktype=self.linktype, **self.kwargs) def stop(self): # type: () -> None @@ -382,7 +382,7 @@ def stop(self): self.fd.close() def push(self, msg): - # type: (Packet) -> None + # type: (bytes) -> None self.fd.send(msg) def fileno(self): @@ -418,7 +418,7 @@ 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 @@ -429,7 +429,7 @@ def start(self): self.fd.listen(1) def push(self, msg): - # type: (Packet) -> None + # type: (bytes) -> None if self.connected: self.fd.send(msg) else: @@ -484,7 +484,7 @@ def start(self): self.connected = True def push(self, msg): - # type: (Packet) -> None + # type: (bytes) -> None self.fd.send(msg) def deliver(self): @@ -524,7 +524,7 @@ def start(self): self.fd.bind((self.addr, self.port)) def push(self, msg): - # type: (Packet) -> None + # type: (bytes) -> None if self._destination: self.fd.sendto(msg, self._destination) else: @@ -660,7 +660,7 @@ 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 diff --git a/scapy/sendrecv.py b/scapy/sendrecv.py index 30b705d3b67..94ab1ae30f8 100644 --- a/scapy/sendrecv.py +++ b/scapy/sendrecv.py @@ -7,13 +7,14 @@ 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 warnings from scapy.compat import plain_str from scapy.data import ETH_P_ALL @@ -25,6 +26,7 @@ NetworkInterface, ) from scapy.packet import Packet +from scapy.pton_ntop import inet_pton from scapy.utils import get_temp_file, tcpdump, wrpcap, \ ContextManagerSubprocess, PcapReader, EDecimal from scapy.plist import ( @@ -34,12 +36,11 @@ ) from scapy.error import log_runtime, log_interactive, Scapy_Exception from scapy.base_classes import Gen, SetGen -from scapy.libs import six from scapy.sessions import DefaultSession from scapy.supersocket import SuperSocket, IterSocket # Typing imports -from scapy.compat import ( +from typing import ( Any, Callable, Dict, @@ -90,9 +91,12 @@ class debug: :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 will be sent in an individual thread + :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. """ @@ -107,9 +111,8 @@ 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 """ @@ -125,9 +128,10 @@ def __init__(self, rcv_pks=None, # type: Optional[SuperSocket] prebuild=False, # type: bool _flood=None, # type: Optional[_FloodGenerator] - threaded=False, # type: bool + threaded=True, # type: bool session=None, # type: Optional[_GlobSessionType] - chainEX=False # type: bool + chainEX=False, # type: bool + stop_filter=None # type: Optional[Callable[[Packet], bool]] ): # type: (...) -> None # Instantiate all arguments @@ -148,10 +152,13 @@ def __init__(self, self.timeout = timeout 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() # Instantiate packet holders if prebuild and not self._flood: self.tobesent = list(pkt) # type: _PacketIterable @@ -167,34 +174,48 @@ def __init__(self, self.timeout = None while retry >= 0: + self.breakout.clear() self.hsent = {} # type: Dict[bytes, List[Packet]] 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.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 self._flood: # Flood: stop send thread 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): @@ -231,6 +252,12 @@ 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: + 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()""" @@ -238,7 +265,7 @@ def _sndrcv_snd(self): p = None try: if self.verbose: - print("Begin emission:") + 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 @@ -247,9 +274,11 @@ def _sndrcv_snd(self): # Send packet self.pks.send(p) 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: @@ -268,6 +297,12 @@ def _sndrcv_snd(self): 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 @@ -292,9 +327,7 @@ def _process_packet(self, r): self.noans += 1 sentpkt._answered = 1 break - if self._send_done and self.noans >= self.notans and not self.multi: - if self.sniffer: - self.sniffer.stop(join=False) + self._stop_sniffer_if_done() if not ok: if self.verbose > 1: os.write(1, b".") @@ -305,20 +338,19 @@ def _process_packet(self, r): def _sndrcv_rcv(self, callback): # type: (Callable[[], None]) -> None """Function used to receive packets and check their hashret""" + # This is blocking. self.sniffer = None # type: Optional[AsyncSniffer] - try: - self.sniffer = AsyncSniffer() - self.sniffer._run( - prn=self._process_packet, - timeout=self.timeout, - store=False, - opened_socket=self.rcv_pks, - session=self.session, - started_callback=callback - ) - except KeyboardInterrupt: - if self.chainCC: - raise + 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): @@ -422,29 +454,38 @@ def _send(x, # type: _PacketIterable @conf.commands.register def send(x, # type: _PacketIterable - iface=None, # type: Optional[_GlobInterfaceType] **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 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 """ - iface = _interface_selection(iface, x) + 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(), + lambda iface: iface.l3socket(ipv6), iface=iface, **kargs ) @@ -465,7 +506,7 @@ def sendp(x, # type: _PacketIterable :param inter: time (in s) between two packets (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)) @@ -485,15 +526,16 @@ def sendp(x, # type: _PacketIterable @conf.commands.register -def sendpfast(x, # type: _PacketIterable - pps=None, # type: Optional[float] - mbps=None, # type: Optional[float] - realtime=False, # type: bool - loop=None, # type: Optional[int] - file_cache=False, # type: bool - iface=None, # type: Optional[_GlobInterfaceType] - replay_args=None, # type: Optional[List[str]] - parse_results=False, # type: bool +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 @@ -501,8 +543,8 @@ def sendpfast(x, # type: _PacketIterable :param pps: packets 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. 0 implies - infinite loop + :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 @@ -515,7 +557,7 @@ def sendpfast(x, # type: _PacketIterable iface = conf.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: @@ -523,8 +565,11 @@ def sendpfast(x, # type: _PacketIterable else: argv.append("--topspeed") - if loop is not None: - 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") @@ -540,12 +585,15 @@ def sendpfast(x, # type: _PacketIterable 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()) @@ -553,7 +601,8 @@ def sendpfast(x, # type: _PacketIterable 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 @@ -614,29 +663,29 @@ def _parse_tcpreplay_result(stdout_b, stderr_b, argv): return {} -def _interface_selection(iface, # type: Optional[_GlobInterfaceType] - packet # type: _PacketIterable - ): - # type: (...) -> _GlobInterfaceType +def _interface_selection(packet: _PacketIterable) -> Tuple[NetworkInterface, bool]: """ Select the network interface according to the layer 3 destination """ - - if iface is None: + _iff, src, _ = next(packet.__iter__()).route() + ipv6 = False + if src: try: - iff = next(packet.__iter__()).route()[0] - except AttributeError: - iff = None - return iff or conf.iface - - return iface + 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, # 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 @@ -644,10 +693,23 @@ def sr(x, # type: _PacketIterable # 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 """ - iface = _interface_selection(iface, x) - 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 @@ -655,10 +717,21 @@ def sr(x, # type: _PacketIterable @conf.commands.register def sr1(*args, **kargs): - # type: (*Packet, **Any) -> Optional[Packet] + # 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 """ + 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]) @@ -666,7 +739,7 @@ def sr1(*args, **kargs): @conf.commands.register -def srp(x, # type: Packet +def srp(x, # type: _PacketIterable promisc=None, # type: Optional[bool] iface=None, # type: Optional[_GlobInterfaceType] iface_hint=None, # type: Optional[str] @@ -692,7 +765,7 @@ def srp(x, # type: Packet @conf.commands.register def srp1(*args, **kargs): - # type: (*Packet, **Any) -> Optional[Packet] + # type: (*Any, **Any) -> Optional[Packet] """ Send and receive packets at layer 2 and return only the first answer """ @@ -713,8 +786,8 @@ def srp1(*args, **kargs): def __sr_loop(srfunc, # type: Callable[..., Tuple[SndRcvList, PacketList]] pkts, # type: _PacketIterable - prn=lambda x: x[1].summary(), # type: Callable[[QueryAnswer], Any] # noqa: E501 - prnfail=lambda x: x.summary(), # type: Callable[[Packet], Any] + 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] @@ -742,7 +815,7 @@ def __sr_loop(srfunc, # type: Callable[..., Tuple[SndRcvList, PacketList]] 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 @@ -751,21 +824,28 @@ def __sr_loop(srfunc, # type: Callable[..., Tuple[SndRcvList, PacketList]] 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: @@ -875,14 +955,27 @@ def srflood(x, # type: _PacketIterable # 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 """ - iface = resolve_iface(iface or conf.iface) - s = iface.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 @@ -892,7 +985,6 @@ def srflood(x, # type: _PacketIterable def sr1flood(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 @@ -900,14 +992,28 @@ def sr1flood(x, # type: _PacketIterable # 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 """ - iface = resolve_iface(iface or conf.iface) - s = iface.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: @@ -1001,7 +1107,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). @@ -1043,12 +1149,20 @@ def __init__(self, *args, **kwargs): self.running = False 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, name="AsyncSniffer" @@ -1069,20 +1183,18 @@ def _run(self, iface=None, # type: Optional[_GlobInterfaceType] started_callback=None, # type: Optional[Callable[[], Any]] session=None, # type: Optional[_GlobSessionType] - session_kwargs={}, # type: Dict[str, Any] + 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=prn, store=store, - **session_kwargs) - else: - session.prn = prn - session.store = store + session = session() # sniff_sockets follows: {socket: label} sniff_sockets = {} # type: Dict[SuperSocket, _GlobInterfaceType] if opened_socket is not None: @@ -1094,7 +1206,7 @@ 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" @@ -1107,7 +1219,7 @@ def _run(self, if isinstance(offline, list) and \ all(isinstance(elt, str) for elt in offline): # List of files - sniff_sockets.update((PcapReader( + sniff_sockets.update((PcapReader( # type: ignore fname if flt is None else tcpdump(fname, args=["-w", "-"], @@ -1117,14 +1229,14 @@ def _run(self, ), fname) for fname in offline) elif isinstance(offline, dict): # Dict of files - sniff_sockets.update((PcapReader( + sniff_sockets.update((PcapReader( # type: ignore fname if flt is None else tcpdump(fname, args=["-w", "-"], flt=flt, getfd=True, quiet=quiet) - ), label) for fname, label in six.iteritems(offline)) + ), label) for fname, label in offline.items()) elif isinstance(offline, (Packet, PacketList, list)): # Iterables (list of packets, PacketList..) offline = IterSocket(offline) @@ -1137,7 +1249,7 @@ def _run(self, )] = offline else: # Other (file descriptors...) - sniff_sockets[PcapReader( + sniff_sockets[PcapReader( # type: ignore offline if flt is None else tcpdump(offline, args=["-w", "-"], @@ -1158,7 +1270,7 @@ def _run(self, sniff_sockets.update( (_RL2(ifname)(type=ETH_P_ALL, iface=ifname, **karg), iflabel) - for ifname, iflabel in six.iteritems(iface) + for ifname, iflabel in iface.items() ) else: iface = iface or conf.iface @@ -1179,7 +1291,7 @@ def _run(self, if not nonblocking_socket: # select is blocking: Add special control socket from scapy.automaton import ObjectPipe - close_pipe = ObjectPipe[None]() + close_pipe = ObjectPipe[None]("control_socket") sniff_sockets[close_pipe] = "control_socket" # type: ignore def stop_cb(): @@ -1202,12 +1314,12 @@ def stop_cb(): # 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 = select_func(list(sniff_sockets.keys()), remain) @@ -1215,8 +1327,28 @@ def stop_cb(): for s in sockets: 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 = s.recv() + 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: @@ -1239,18 +1371,6 @@ 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] @@ -1259,14 +1379,15 @@ def stop_cb(): # 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 @@ -1279,9 +1400,13 @@ 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)" ) @@ -1296,6 +1421,8 @@ 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 diff --git a/scapy/sessions.py b/scapy/sessions.py index e4ccb86384c..3c58dc2c6a3 100644 --- a/scapy/sessions.py +++ b/scapy/sessions.py @@ -10,105 +10,57 @@ import socket import struct -from scapy.compat import raw, orb +from scapy.compat import orb from scapy.config import conf -from scapy.packet import NoPayload, Packet -from scapy.plist import PacketList +from scapy.packet import Packet from scapy.pton_ntop import inet_pton # Typing imports -from scapy.compat import ( +from typing import ( Any, Callable, DefaultDict, Dict, + Iterator, List, Optional, Tuple, - cast + 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, # type: Optional[Callable[[Packet], Any]] - store=False, # type: bool - supersession=None, # type: Optional[DefaultSession] - *args, # type: Any - **karg # type: Any - ): - # type: (...) -> None - self.__prn = prn - self.__store = store - self.lst = [] # type: List[Packet] - 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): - # type: () -> bool - return self.__store - - @store.setter - def store(self, val): - # type: (bool) -> None - if self._supersession: - self._supersession.store = val - else: - self.__store = val - - @property - def prn(self): - # type: () -> Optional[Callable[[Packet], Any]] - return self.__prn - - @prn.setter - def prn(self, f): - # type: (Optional[Any]) -> None - if self._supersession: - self._supersession.prn = f - else: - self.__prn = f + def __init__(self, supersession: Optional[Self] = None): + if supersession and not isinstance(supersession, DefaultSession): + supersession = supersession() + self.supersession = supersession - @property - def count(self): - # type: () -> int - if self._supersession: - return self._supersession.count - else: - return self.__count - - def toPacketList(self): - # type: () -> PacketList - 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): - # type: (Optional[Packet]) -> None - """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 not isinstance(pkt, Packet): - raise TypeError("Only provide a Packet.") - 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): @@ -123,39 +75,13 @@ def __init__(self, *args, **kwargs): DefaultSession.__init__(self, *args, **kwargs) self.fragments = defaultdict(list) # type: DefaultDict[Tuple[Any, ...], List[Packet]] # noqa: E501 - def _ip_process_packet(self, packet): - # type: (Packet) -> Optional[Packet] - from scapy.layers.inet import _defrag_list, IP - 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 = [] # type: List[Packet] - _defrag_list(self.fragments[uniq], defrag, []) - defragmented_packet = defrag[0] - defragmented_packet = defragmented_packet.__class__( - raw(defragmented_packet) - ) - defragmented_packet.time = packet.time - return defragmented_packet - finally: - del self.fragments[uniq] + def process(self, packet: Packet) -> Optional[Packet]: + from scapy.layers.inet import IP, _defrag_ip_pkt + if not packet: return None - else: + if IP not in packet: return packet - - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None - if not pkt: - return None - super(IPSession, self).on_packet_received(self._ip_process_packet(pkt)) + return _defrag_ip_pkt(packet, self.fragments)[1] # type: ignore class StringBuffer(object): @@ -174,16 +100,28 @@ def __init__(self): # type: () -> None self.content = bytearray(b"") self.content_len = 0 + self.noff = 0 # negative offset self.incomplete = [] # type: List[Tuple[int, int]] - def append(self, data, seq): - # type: (bytes, int) -> None + 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. @@ -192,11 +130,15 @@ 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): # type: () -> None @@ -220,9 +162,28 @@ def __str__(self): 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:: @@ -245,8 +206,8 @@ def tcp_reassemble(cls, data, metadata, session): https://scapy.readthedocs.io/en/latest/usage.html#how-to-use-tcpsession-to-defragment-tcp-packets :param app: Whether the socket is on application layer = has no TCP - layer. This is used for instance if you are using a native - TCP socket. Default to False + layer. This is identical to StreamSocket so only use this if your + underlying source of data isn't a socket.socket. """ def __init__(self, app=False, *args, **kwargs): @@ -254,7 +215,7 @@ def __init__(self, app=False, *args, **kwargs): super(TCPSession, self).__init__(*args, **kwargs) self.app = app if app: - self.data = b"" + self.data = StringBuffer() self.metadata = {} # type: Dict[str, Any] self.session = {} # type: Dict[str, Any] else: @@ -266,6 +227,9 @@ def __init__(self, app=False, *args, **kwargs): 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 @@ -283,64 +247,106 @@ def xor(x, y): # Uni-directional return src + dst + struct.pack("!HH", pkt.dport, pkt.sport) - def _process_packet(self, pkt): - # type: (Packet) -> Optional[Packet] + 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 - pay_class = pkt.__class__ - if not hasattr(pay_class, "tcp_reassemble"): - # Being on top of TCP, we have no way of knowing - # when a packet ends. - return pkt - self.data += bytes(pkt) - pkt = pay_class.tcp_reassemble(self.data, self.metadata, self.session) - if pkt: - self.data = b"" - self.metadata = {} - return pkt + 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 or TCP not in pkt: + 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 = pay.original # Match packets by a unique TCP identifier - seq = pkt[TCP].seq 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 hasattr(pay_class, "tcp_reassemble"): - tcp_reassemble = pay_class.tcp_reassemble - else: - # We can't know for sure when a packet ends. - # Ignore. - return pkt - metadata["pay_class"] = pay_class - metadata["tcp_reassemble"] = tcp_reassemble + metadata["pay_class"] = pay_class = pkt[TCP].guess_payload_class(new_data) + metadata["tcp_reassemble"] = tcp_reassemble = streamcls(pay_class) else: tcp_reassemble = metadata["tcp_reassemble"] - if "seq" not in metadata: - metadata["seq"] = seq - # 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 - # Note that this take care of retransmission packets. - data.append(new_data, seq) + + 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: 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 @@ -348,21 +354,51 @@ def _process_packet(self, pkt): 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 # type: Optional[Packet] if data.full(): # Reassemble using all previous packets - packet = tcp_reassemble(bytes(data), metadata, tcp_session) + 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: if "seq" in metadata: pkt[TCP].seq = metadata["seq"] - # Clear buffer - data.clear() # Clear TCP reassembly metadata metadata.clear() - del self.tcp_frags[ident] + # 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 - pay.underlayer.remove_payload() + if pay: + pay.underlayer.remove_payload() if IP in pkt: pkt[IP].len = None pkt[IP].chksum = None @@ -371,18 +407,21 @@ def _process_packet(self, pkt): return pkt return None - def on_packet_received(self, pkt): - # type: (Optional[Packet]) -> None - """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]: """ - if not pkt: - return None - # First, defragment IP if necessary - pkt = self._ip_process_packet(pkt) - if not pkt: - return None + Will be called by sniff() to ask for a packet + """ + 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 ff070d6e039..5ffd7a30f63 100644 --- a/scapy/supersocket.py +++ b/scapy/supersocket.py @@ -7,7 +7,6 @@ SuperSocket. """ -from __future__ import absolute_import from select import select, error as select_error import ctypes import errno @@ -17,13 +16,17 @@ from scapy.config import conf from scapy.consts import DARWIN, WINDOWS -from scapy.data import MTU, ETH_P_IP, SOL_PACKET, SO_TIMESTAMPNS +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 from scapy.interfaces import network_name -import scapy.libs.six as six -from scapy.packet import Packet -import scapy.packet +from scapy.packet import Packet, NoPayload from scapy.plist import ( PacketList, SndRcvList, @@ -33,21 +36,21 @@ # Typing imports from scapy.interfaces import _GlobInterfaceType -from scapy.compat import ( +from typing import ( Any, + Dict, Iterator, List, Optional, Tuple, Type, cast, - _Generic_metaclass ) # Utils -class _SuperSocket_metaclass(_Generic_metaclass): +class _SuperSocket_metaclass(type): desc = None # type: Optional[str] def __repr__(self): @@ -79,8 +82,7 @@ class tpacket_auxdata(ctypes.Structure): # SuperSocket -@six.add_metaclass(_SuperSocket_metaclass) -class SuperSocket: +class SuperSocket(metaclass=_SuperSocket_metaclass): closed = False # type: bool nonblocking_socket = False # type: bool auxdata_available = False # type: bool @@ -100,6 +102,11 @@ def __init__(self, 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() @@ -111,10 +118,15 @@ def send(self, x): else: return 0 - if six.PY2 or WINDOWS: + if WINDOWS: def _recv_raw(self, sock, x): # type: (socket.socket, int) -> Tuple[bytes, Any, Optional[float]] - """Internal function to receive a Packet""" + """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: @@ -122,6 +134,10 @@ 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: @@ -170,16 +186,27 @@ def _recv_raw(self, sock, x): def recv_raw(self, x=MTU): # type: (int) -> Tuple[Optional[Type[Packet]], Optional[bytes], Optional[float]] # noqa: E501 - """Returns a tuple containing (cls, pkt_data, time)""" + """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): - # type: (int) -> Optional[Packet] + 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 None try: - pkt = cls(val) # type: Packet + pkt = cls(val, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -198,6 +225,8 @@ def fileno(self): def close(self): # type: () -> None + """Gracefully close this socket + """ if self.closed: return self.closed = True @@ -211,13 +240,20 @@ def close(self): def sr(self, *args, **kargs): # type: (Any, Any) -> Tuple[SndRcvList, PacketList] + """Send and Receive multiple packets + """ from scapy import sendrecv - ans, unans = sendrecv.sndrcv(self, *args, **kargs) # type: SndRcvList, PacketList # noqa: E501 - return ans, unans + 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 + # 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 @@ -228,8 +264,7 @@ def sr1(self, *args, **kargs): def sniff(self, *args, **kargs): # type: (Any, Any) -> PacketList from scapy import sendrecv - pkts = sendrecv.sniff(opened_socket=self, *args, **kargs) # type: PacketList # noqa: E501 - return pkts + return sendrecv.sniff(opened_socket=self, *args, **kargs) def tshark(self, *args, **kargs): # type: (Any, Any) -> None @@ -304,24 +339,23 @@ def __init__(self, self.ins.bind((iface, type)) else: self.iface = "any" - if not six.PY2: - try: - # Receive Auxiliary Data (VLAN tags) - 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) - - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + try: + # Receive Auxiliary Data (VLAN tags) + 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) + + 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 @@ -337,7 +371,7 @@ def recv(self, x=MTU): lvl = 3 try: - pkt = cls(data) + pkt = cls(data, **kwargs) except KeyboardInterrupt: raise except Exception: @@ -375,83 +409,148 @@ def send(self, x): 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): - # type: (socket.socket) -> None + 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): - # type: (socket.socket, Optional[Type[Packet]]) -> 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=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=None, **kwargs): + # type: (Optional[int], Any) -> Optional[Packet] + if x is None: + x = MTU + # Block but in PEEK mode data = self.ins.recv(x, socket.MSG_PEEK) + if data == b"": + raise EOFError x = len(data) - if x == 0: - return None - pkt = self.basecls(data) # type: Packet + pkt = self.rcvcls(self._buf + data, self.metadata, self.streamsession) + if pkt is None: # Incomplete packet. + self._buf += self.ins.recv(x) + return self.recv(x) + 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 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): # type: (socket.socket, Optional[Type[Packet]]) -> None - self._buf = b"" - super(SSLStreamSocket, self).__init__(sock, basecls) + 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): - # type: (int) -> Optional[Packet] - pkt = None # type: Optional[Packet] - 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 - - x = len(self._buf) - pkt = self.basecls(self._buf) - if pkt is not None: - pad = pkt.getlayer(conf.padding_layer) - - 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): # noqa: E501 - x -= len(pad.load) - pad = pad.payload - self._buf = self._buf[x:] + return self.recv(x) return pkt + @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, + ) + + +# Old name: SSLStreamSocket +SSLStreamSocket = StreamSocketPeekless + class L2ListenTcpdump(SuperSocket): desc = "read packets at layer 2 using tcpdump" @@ -462,6 +561,7 @@ def __init__(self, filter=None, # type: Optional[str] nofilter=False, # type: bool prog=None, # type: Optional[str] + quiet=False, # type: bool *arg, # type: Any **karg # type: Any ): @@ -485,13 +585,14 @@ def __init__(self, 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.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): - # type: (int) -> Optional[Packet] - return self.reader.recv(x) + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] + return self.reader.recv(x, **kwargs) def close(self): # type: () -> None @@ -528,7 +629,7 @@ def _iter(obj=cast(SndRcvList, obj)): yield r self.iter = _iter() elif isinstance(obj, (list, PacketList)): - if isinstance(obj[0], bytes): # type: ignore + if isinstance(obj[0], bytes): self.iter = iter(obj) else: self.iter = (y for x in obj for y in x) @@ -540,11 +641,11 @@ def select(sockets, remain=None): # type: (List[SuperSocket], Any) -> List[SuperSocket] return sockets - def recv(self, *args): - # type: (*Any) -> Optional[Packet] + def recv(self, x=None, **kwargs): + # type: (Optional[int], Any) -> Optional[Packet] try: pkt = next(self.iter) - return pkt.__class__(bytes(pkt)) + return pkt.__class__(bytes(pkt), **kwargs) except StopIteration: raise EOFError diff --git a/scapy/themes.py b/scapy/themes.py index 5153bed597c..7124bf0c26d 100644 --- a/scapy/themes.py +++ b/scapy/themes.py @@ -11,15 +11,17 @@ # Color themes # ################## +import html import sys -from scapy.compat import ( +from typing import ( Any, - Callable, List, Optional, Tuple, + cast, ) +from scapy.compat import Protocol class ColorTable: @@ -32,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"), @@ -42,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 @@ -74,14 +77,27 @@ def ansi_to_pygments(self, x): Color = ColorTable() +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: (...) -> Callable[[Any], str] - def do_style(val, fmt=fmt, fmt2=fmt2, before=before, after=after): - # type: (Any, Optional[str], str, str, str) -> 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: sval = str(val) else: @@ -91,6 +107,31 @@ def do_style(val, fmt=fmt, fmt2=fmt2, before=before, after=after): 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__ @@ -100,7 +141,7 @@ def __reduce__(self): return (self.__class__, (), ()) def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + # type: (str) -> _ColorFormatterType if attr in ["__getstate__", "__setstate__", "__getinitargs__", "__reduce_ex__"]: raise AttributeError() @@ -119,7 +160,7 @@ class NoTheme(ColorTheme): class AnsiColorTheme(ColorTheme): def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + # type: (str) -> _ColorFormatterType if attr.startswith("__"): raise AttributeError(attr) s = "style_%s" % attr @@ -134,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 @@ -168,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 @@ -183,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 @@ -251,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 @@ -261,8 +279,7 @@ class ColorOnBlackTheme(AnsiColorTheme): class FormatTheme(ColorTheme): - def __getattr__(self, attr): - # type: (str) -> Callable[[Any], str] + def __getattr__(self, attr: str) -> _ColorFormatterType: if attr.startswith("__"): raise AttributeError(attr) colfmt = self.__class__.__dict__.get("style_%s" % attr, "%s") @@ -270,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}" @@ -288,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@]@" @@ -388,8 +417,7 @@ def apply_ipython_style(shell): if isinstance(conf.color_theme, (FormatTheme, NoTheme)): # Formatable if isinstance(conf.color_theme, HTMLTheme): - from scapy.compat import html_escape - prompt = html_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) diff --git a/scapy/tools/UTscapy.py b/scapy/tools/UTscapy.py index 34c858a8336..bd703869e74 100644 --- a/scapy/tools/UTscapy.py +++ b/scapy/tools/UTscapy.py @@ -7,11 +7,9 @@ Unit testing infrastructure for Scapy """ -from __future__ import print_function - +import builtins import bz2 import copy -import code import getopt import glob import hashlib @@ -27,10 +25,9 @@ import warnings import zlib -from scapy.consts import WINDOWS -import scapy.libs.six as six +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 @@ -42,8 +39,6 @@ def _utf8_support(): Check UTF-8 support for the output """ try: - if six.PY2: - return False if WINDOWS: return (sys.stdout.encoding == "utf-8") return True @@ -69,20 +64,17 @@ class Bunch: def retry_test(func): """Retries the passed function 3 times before failing""" - success = False + v = None + tb = None for _ in range(3): try: - result = func() + return func() except Exception: t, v, tb = sys.exc_info() time.sleep(1) - else: - success = True - break - if not success: - six.reraise(t, v, tb) - assert success - return result + + if v and tb: + raise v.with_traceback(tb) def scapy_path(fname): @@ -96,9 +88,12 @@ def scapy_path(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 = False + conf.debug_dissector = self.new_value def __exit__(self, exc_type, exc_value, traceback): conf.debug_dissector = self.old_dbg @@ -178,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) @@ -212,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() @@ -299,11 +294,6 @@ def __init__(self, name): self.expand = 1 def prepare(self, theme): - 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") if self.result == "passed": self.fresult = theme.success(self.result) else: @@ -462,18 +452,12 @@ def docs_campaign(test_campaign): # 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): @@ -563,7 +547,7 @@ def run_test(test, get_interactive_session, theme, verb=3, # 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.prepare(theme) if verb > 2: @@ -601,12 +585,14 @@ def run_campaign(test_campaign, get_interactive_session, theme, )[0] # Drop - 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) + 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): @@ -617,7 +603,7 @@ def drop(scapy_ses): else: failed += 1 if drop_to_interpreter: - drop(scapy_ses) + drop(t, scapy_ses) test_campaign.duration += t.duration except KeyboardInterrupt: failed += 1 @@ -626,8 +612,6 @@ def drop(scapy_ses): test_campaign.interrupted = True if verb: print("Campaign interrupted!") - if drop_to_interpreter: - drop(scapy_ses) test_campaign.passed = passed test_campaign.failed = failed @@ -986,6 +970,9 @@ def main(): 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] @@ -1108,11 +1095,6 @@ def main(): # Disable tests if needed - # Discard Python3 tests when using Python2 - if six.PY2: - KW_KO.append("python3_only") - if VERB > 2: - print(" " + arrow + " Python 2 mode") try: if NON_ROOT or os.getuid() != 0: # Non root # Discard root tests @@ -1122,6 +1104,9 @@ def main(): 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: @@ -1129,10 +1114,6 @@ def main(): KW_KO.append("disabled") - # Process extras - if six.PY3: - KW_KO.append("FIXME_py3") - if ANNOTATIONS_MODE: try: from pyannotate_runtime import collect_types @@ -1153,7 +1134,7 @@ def main(): for m in MODULES: try: mod = import_module(m) - six.moves.builtins.__dict__.update(mod.__dict__) + builtins.__dict__.update(mod.__dict__) except ImportError as e: raise getopt.GetoptError("cannot import [%s]: %s" % (m, e)) @@ -1176,7 +1157,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] @@ -1238,7 +1219,7 @@ def main(): 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: diff --git a/scapy/tools/automotive/isotpscanner.py b/scapy/tools/automotive/isotpscanner.py index aacadf231c2..e66d7122375 100755 --- a/scapy/tools/automotive/isotpscanner.py +++ b/scapy/tools/automotive/isotpscanner.py @@ -4,7 +4,6 @@ # Copyright (C) Nils Weiss # Copyright (C) Alexander Schroeder -from __future__ import print_function import getopt import sys @@ -14,12 +13,17 @@ from ast import literal_eval -import scapy.libs.six as six from scapy.config import conf from scapy.consts import LINUX -from scapy.compat import Tuple, Optional, Any -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 diff --git a/scapy/tools/automotive/obdscanner.py b/scapy/tools/automotive/obdscanner.py index eb3ced417c0..5318fb82bf4 100755 --- a/scapy/tools/automotive/obdscanner.py +++ b/scapy/tools/automotive/obdscanner.py @@ -5,7 +5,6 @@ # Copyright (C) Friedrich Feigel # Copyright (C) Nils Weiss -from __future__ import print_function import getopt import sys @@ -15,11 +14,10 @@ from ast import literal_eval -import scapy.libs.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 diff --git a/scapy/tools/check_asdis.py b/scapy/tools/check_asdis.py index ff24cd0e92f..abcf3e47477 100755 --- a/scapy/tools/check_asdis.py +++ b/scapy/tools/check_asdis.py @@ -3,7 +3,6 @@ # See https://scapy.net/ for more information # Copyright (C) Philippe Biondi -from __future__ import print_function import getopt 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 697e730b06d..92f6d1996df 100644 --- a/scapy/tools/generate_ethertypes.py +++ b/scapy/tools/generate_ethertypes.py @@ -11,16 +11,20 @@ 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. @@ -32,27 +36,33 @@ # ... #Comment # """ -ALIASES = { - b"IP": b"IPv4", - b"IPV6": b"IPv6" -} +ALIASES = {"IP": "IPv4", "IPV6": "IPv6"} for line in DATA.split(b"\n"): - match = re.match(reg, line) - if match: - name = match.group(1) - name = ALIASES.get(name, name).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("../libs/ethertypes.py", "rb") as inp: +# 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", "wb") as out: - ini, sep, _ = data.partition(b"DATA = b\"\"\"") - COMPILED = ini + sep + b"\n" + COMPILED + b"\"\"\"\n" +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/utils.py b/scapy/utils.py index 060ba139d34..e6a432e8477 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -7,19 +7,28 @@ 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 argparse import array +import base64 import collections import decimal import difflib +import enum import gzip +import inspect +import locale +import math import os +import pickle import random import re +import shutil import socket import struct import subprocess @@ -27,21 +36,29 @@ import tempfile import threading import time +import traceback import warnings -import scapy.libs.six as six -from scapy.libs.six.moves import range, input, zip_longest - from scapy.config import conf from scapy.consts import DARWIN, OPENBSD, WINDOWS -from scapy.data import MTU, DLT_EN10MB -from scapy.compat import orb, 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.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 scapy.compat import ( +from typing import ( cast, Any, AnyStr, @@ -50,7 +67,6 @@ IO, Iterator, List, - Literal, Optional, TYPE_CHECKING, Tuple, @@ -58,14 +74,16 @@ Union, overload, ) +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 - _SuperSocket = SuperSocket -else: - _SuperSocket = object + import prompt_toolkit _ByteStream = Union[IO[bytes], gzip.GzipFile] @@ -133,19 +151,10 @@ def __floordiv__(self, other): # type: (_Decimal) -> EDecimal return EDecimal(Decimal.__floordiv__(self, Decimal(other))) - if sys.version_info >= (3,): - def __divmod__(self, other): - # type: (_Decimal) -> Tuple[EDecimal, EDecimal] - r = Decimal.__divmod__(self, Decimal(other)) - return EDecimal(r[0]), EDecimal(r[1]) - else: - def __div__(self, other): - # type: (_Decimal) -> EDecimal - return EDecimal(Decimal.__div__(self, Decimal(other))) - - def __rdiv__(self, other): - # type: (_Decimal) -> EDecimal - return EDecimal(Decimal.__rdiv__(self, Decimal(other))) + def __divmod__(self, other): + # type: (_Decimal) -> Tuple[EDecimal, EDecimal] + r = Decimal.__divmod__(self, Decimal(other)) + return EDecimal(r[0]), EDecimal(r[1]) def __mod__(self, other): # type: (_Decimal) -> EDecimal @@ -161,7 +170,10 @@ def __pow__(self, other, modulo=None): def __eq__(self, other): # type: (Any) -> bool - return super(EDecimal, self).__eq__(other) or float(self) == other + if isinstance(other, Decimal): + return super(EDecimal, self).__eq__(other) + else: + return bool(float(self) == other) def normalize(self, precision): # type: ignore # type: (int) -> EDecimal @@ -177,12 +189,12 @@ def get_temp_file(keep, autoext, fd): @overload -def get_temp_file(keep=False, autoext="", fd=False): # noqa: F811 +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): # noqa: F811 +def get_temp_file(keep=False, autoext="", fd=False): # type: (bool, str, bool) -> Union[IO[bytes], str] """Creates a temporary file. @@ -221,6 +233,34 @@ def get_temp_dir(keep=False): return dname +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, color=False): # type: (AnyStr, bool) -> str r = "" @@ -243,10 +283,9 @@ def restart(): 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) @@ -257,7 +296,7 @@ def lhex(x): from scapy.volatile import VolatileValue if isinstance(x, VolatileValue): return repr(x) - if isinstance(x, six.integer_types): + if isinstance(x, int): return hex(x) if isinstance(x, tuple): return "(%s)" % ", ".join(lhex(v) for v in x) @@ -361,57 +400,120 @@ def repr_hex(s): @conf.commands.register -def hexdiff(a, b, autojunk=False): - # type: (Union[Packet, AnyStr], Union[Packet, AnyStr], bool) -> None +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... - For the autojunk parameter, see - https://docs.python.org/3.8/library/difflib.html#difflib.SequenceMatcher + 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 autojunk: Setting it to True will likely increase the comparison - speed a lot on big byte strings, but will reduce accuracy (will tend - to miss insertion and see replacements instead for instance). + :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. """ - - # Compare the strings using difflib - xb = bytes_encode(a) yb = bytes_encode(b) - 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))] + 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 = [] - 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] - - if autojunk: + + 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 - lbx = len(backtrackx) - lby = len(backtracky) - backtrackx += [b''] * (max(lbx, lby) - lbx) - backtracky += [b''] * (max(lbx, lby) - lby) + 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 @@ -454,7 +556,7 @@ def hexdiff(a, b, autojunk=False): 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=' ') @@ -506,7 +608,7 @@ def _fletcher16(charbuf): # 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 @@ -597,13 +699,22 @@ def zerofree_randstring(length): for _ in range(length)) +def stror(s1, s2): + # type: (bytes, bytes) -> bytes + """ + Returns the binary OR of the 2 provided strings s1 and s2. s1 and s2 + must be of same length. + """ + 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): @@ -612,7 +723,19 @@ def strand(s1, s2): 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 @@ -636,7 +759,7 @@ def atol(x): try: ip = inet_aton(x) except socket.error: - ip = inet_aton(socket.gethostbyname(x)) + raise ValueError("Bad IP format: %s" % x) return cast(int, struct.unpack("!I", ip)[0]) @@ -674,10 +797,7 @@ 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 @@ -703,6 +823,102 @@ def itom(x): 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 @@ -759,14 +975,10 @@ 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): # type: () -> ContextManagerCaptureOutput - import mock + from unittest import mock def write(s, decorator=self): # type: (str, ContextManagerCaptureOutput) -> None @@ -984,7 +1196,7 @@ class Enum_metaclass(type): 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 @@ -1017,7 +1229,7 @@ def __repr__(self): def export_object(obj): # type: (Any) -> None import zlib - print(bytes_base64(zlib.compress(six.moves.cPickle.dumps(obj, 2), 9))) + print(base64.b64encode(zlib.compress(pickle.dumps(obj, 2), 9)).decode()) def import_object(obj=None): @@ -1025,7 +1237,7 @@ def import_object(obj=None): import zlib if obj is None: obj = sys.stdin.read() - return six.moves.cPickle.loads(zlib.decompress(base64_bytes(obj.strip()))) # noqa: E501 + return pickle.loads(zlib.decompress(base64.b64decode(obj.strip()))) def save_object(fname, obj): @@ -1033,14 +1245,14 @@ def save_object(fname, obj): """Pickle a Python object""" fd = gzip.open(fname, "wb") - six.moves.cPickle.dump(obj, fd) + pickle.dump(obj, fd) fd.close() def load_object(fname): # type: (str) -> Any """unpickle a Python object""" - return six.moves.cPickle.load(gzip.open(fname, "rb")) + return pickle.load(gzip.open(fname, "rb")) @conf.commands.register @@ -1056,7 +1268,7 @@ def corrupt_bytes(data, p=0.01, n=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() # type: ignore + return s.tobytes() @conf.commands.register @@ -1072,7 +1284,7 @@ def corrupt_bits(data, p=0.01, n=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() # type: ignore + return s.tobytes() ############################# @@ -1159,13 +1371,18 @@ def __new__(cls, name, bases, dct): dct['alternative'].alternative = newcls return newcls - def __call__(cls, filename): # type: ignore + 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( @@ -1179,7 +1396,12 @@ def __call__(cls, filename): # type: ignore if "alternative" in cls.__dict__: cls = cls.__dict__["alternative"] - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) + i = cls.__new__( + cls, + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) try: i.__init__(filename, fdesc, magic) return i @@ -1195,12 +1417,14 @@ def open(fname # type: Union[IO[bytes], str] """Open (if necessary) filename, and read the magic.""" if isinstance(fname, str): filename = fname - try: - fdesc = gzip.open(filename, "rb") # type: _ByteStream - magic = fdesc.read(4) - except IOError: - fdesc = open(filename, "rb") - magic = fdesc.read(4) + 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 = fname filename = getattr(fdesc, "name", "No name") @@ -1208,8 +1432,7 @@ def open(fname # type: Union[IO[bytes], str] return filename, fdesc, magic -@six.add_metaclass(PcapReader_metaclass) -class RawPcapReader: +class RawPcapReader(metaclass=PcapReader_metaclass): """A stateful pcap reader. Each packet is returned as a string""" # TODO: use Generics to properly type the various readers. @@ -1249,24 +1472,24 @@ def __init__(self, filename, fdesc=None, magic=None): # type: ignore self.linktype = linktype self.snaplen = snaplen + def __enter__(self): + # type: () -> RawPcapReader + return self + def __iter__(self): # type: () -> RawPcapReader return self - def next(self): - # type: () -> Packet + 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() # type: ignore + return self._read_packet() except EOFError: raise StopIteration - def __next__(self): - # type: () -> Packet - return self.next() - def _read_packet(self, size=MTU): # type: (int) -> Tuple[bytes, RawPcapReader.PacketMetadata] """return a single packet read from the file as a tuple containing @@ -1278,7 +1501,14 @@ 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)) @@ -1327,8 +1557,10 @@ def fileno(self): return -1 if WINDOWS else self.f.fileno() def close(self): - # type: () -> Optional[Any] - return self.f.close() + # 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 @@ -1343,7 +1575,7 @@ def select(sockets, # type: List[SuperSocket] return sockets -class PcapReader(RawPcapReader, _SuperSocket): +class PcapReader(RawPcapReader): def __init__(self, filename, fdesc=None, magic=None): # type: ignore # type: (str, IO[bytes], bytes) -> None RawPcapReader.__init__(self, filename, fdesc, magic) @@ -1362,15 +1594,15 @@ def __enter__(self): # type: () -> PcapReader return self - def read_packet(self, size=MTU): - # type: (int) -> Packet + 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) # type: Packet + p = self.LLcls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -1387,11 +1619,15 @@ def read_packet(self, size=MTU): p.wirelen = pkt_info.wirelen return p - def recv(self, size=MTU): - # type: (int) -> Packet - return self.read_packet(size=size) + 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): + def __next__(self): # type: ignore # type: () -> Packet try: return self.read_packet() @@ -1416,25 +1652,33 @@ class RawPcapNgReader(RawPcapReader): PacketMetadata = collections.namedtuple("PacketMetadataNg", # type: ignore ["linktype", "tsresol", "tshigh", "tslow", "wirelen", - "comment"]) + "comments", "ifname", "direction", + "process_information"]) 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 = [] # type: List[Tuple[int, int, int]] + 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, - 10: self._read_block_dsb, + 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( @@ -1461,16 +1705,21 @@ def _read_block(self, size=MTU): 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("Invalid block length !") + warning("PcapNg: Invalid block length !") raise EOFError - block = self.f.read(blocklen - 12) + + _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) - return self.blocktypes.get( - blocktype, - lambda block, size: None - )(block, size) + if blocktype in self.blocktypes: + return self.blocktypes[blocktype](block, size) + return None def _read_block_tail(self, blocklen): # type: (int) -> None @@ -1483,10 +1732,12 @@ def _read_block_tail(self, blocklen): 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": @@ -1494,14 +1745,41 @@ def _read_block_shb(self): elif endian == b"\x4d\x3c\x2b\x1a": self.endian = "<" else: - warning("Bad magic in Section Header block (not a pcapng file?)") + warning("PcapNg: Bad magic in Section Header Block" + " (not a pcapng file?)") + raise EOFError + + try: + 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 - blocklen = struct.unpack(self.endian + "I", _blocklen)[0] - if blocklen < 16: - warning("Invalid SHB block length!") + # 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 - options = self.f.read(blocklen - 16) + 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) @@ -1513,34 +1791,32 @@ def _read_packet(self, size=MTU): # type: ignore """ while True: - res = self._read_block() + res = self._read_block(size=size) if res is not None: return res def _read_options(self, options): - # type: (bytes) -> Dict[str, Any] - """Section Header Block""" - opts = self.default_options.copy() # type: Dict[str, Any] + # 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 - ) - if code == 1 and length >= 1 and 4 + length < len(options): - comment = options[4:4 + length] - newline_index = comment.find(b"\n") - if newline_index == -1: - warning("PcapNg: invalid comment option") - break - opts["comment"] = comment[:newline_index] + 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)) @@ -1552,12 +1828,34 @@ def _read_block_idb(self, block, _): """Interface Description Block""" # 2 bytes LinkType + 2 bytes Reserved # 4 bytes Snaplen - options = self._read_options(block[8:-4]) + 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 = struct.unpack( # type: ignore + interface: Tuple[int, int, Dict[str, Any]] = struct.unpack( self.endian + "HxxI", block[:8] - ) + (options["tsresol"],) # type: Tuple[int, int, int] + ) + (options,) except struct.error: warning("PcapNg: IDB is too small %d/8 !" % len(block)) raise EOFError @@ -1591,17 +1889,52 @@ def _read_block_epb(self, block, size): # Parse options options = self._read_options(block[opt_offset:]) - comment = options.get("comment", None) + + 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], # noqa: E501 + tsresol=self.interfaces[intid][2]['tsresol'], # noqa: E501 tshigh=tshigh, tslow=tslow, wirelen=wirelen, - comment=comment)) + ifname=ifname, + direction=direction, + process_information=process_information, + comments=comments)) def _read_block_spb(self, block, size): # type: (bytes, int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] @@ -1621,11 +1954,14 @@ def _read_block_spb(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], # noqa: E501 + tsresol=self.interfaces[intid][2]['tsresol'], # noqa: E501 tshigh=None, tslow=None, wirelen=wirelen, - comment=None)) + ifname=None, + direction=None, + process_information={}, + comments=None)) def _read_block_pkt(self, block, size): # type: (bytes, int) -> Tuple[bytes, RawPcapNgReader.PacketMetadata] @@ -1642,11 +1978,14 @@ def _read_block_pkt(self, block, size): 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, - comment=None)) + ifname=None, + direction=None, + process_information={}, + comments=None)) def _read_block_dsb(self, block, size): # type: (bytes, int) -> None @@ -1676,7 +2015,7 @@ def _read_block_dsb(self, block, size): # TLS Key Log if secrets_type == 0x544c534b: - if getattr(conf, "tls_nss_keys", False) is False: + 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.") @@ -1695,13 +2034,43 @@ def _read_block_dsb(self, block, size): else: # Note: these attributes are only available when the TLS # layer is loaded. - conf.tls_nss_keys = keys # type: ignore - conf.tls_session_enable = True # type: ignore + conf.tls_nss_keys = keys + conf.tls_session_enable = True else: warning("PcapNg: Unknown DSB secrets type (0x%x)!", secrets_type) + def _read_block_pib(self, block, _): + # type: (bytes, int) -> None + """Apple Process Information Block""" -class PcapNgReader(RawPcapNgReader, PcapReader, _SuperSocket): + # 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 @@ -1713,15 +2082,15 @@ def __enter__(self): # type: () -> PcapNgReader return self - def read_packet(self, size=MTU): - # type: (int) -> Packet + 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, comment) = rp + s, (linktype, tsresol, tshigh, tslow, wirelen, comments, ifname, direction, process_information) = rp # noqa: E501 try: cls = conf.l2types.num2layer[linktype] # type: Type[Packet] - p = cls(s) # type: Packet + p = cls(s, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -1734,17 +2103,20 @@ def read_packet(self, size=MTU): if tshigh is not None: p.time = EDecimal((tshigh << 32) + tslow) / tsresol p.wirelen = wirelen - p.comment = comment + 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 recv(self, size=MTU): - # type: (int) -> Packet - return self.read_packet() + def recv(self, size: int = MTU, **kwargs: Any) -> 'Packet': # type: ignore + return self.read_packet(size=size, **kwargs) class GenericPcapWriter(object): nano = False - linktype = None # type: Optional[int] + linktype: int def _write_header(self, pkt): # type: (Optional[Union[Packet, bytes]]) -> None @@ -1752,11 +2124,14 @@ def _write_header(self, pkt): 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] - comment=None # type: Optional[bytes] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] ): # type: (...) -> None raise NotImplementedError @@ -1769,7 +2144,7 @@ def _get_time(self, # type: (...) -> Tuple[float, int] if hasattr(packet, "time"): if sec is None: - packet_time = packet.time # type: ignore + packet_time = packet.time tmp = int(packet_time) usec = int(round((packet_time - tmp) * (1000000000 if self.nano else 1000000))) @@ -1780,7 +2155,7 @@ def _get_time(self, def write_header(self, pkt): # type: (Optional[Union[Packet, bytes]]) -> None - if self.linktype is None: + if not hasattr(self, 'linktype'): try: if pkt is None or isinstance(pkt, bytes): # Can't guess LL @@ -1833,17 +2208,29 @@ def write_packet(self, if wirelen is None: if hasattr(packet, "wirelen"): - wirelen = packet.wirelen # type: ignore + wirelen = packet.wirelen if wirelen is None: wirelen = caplen - comment = getattr(packet, "comment", None) - + 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, - comment=comment + ifname=ifname, + direction=direction, + linktype=linktype, + comments=comments, ) @@ -1918,6 +2305,7 @@ def __init__(self, sync=False, # type: bool nano=False, # type: bool snaplen=MTU, # type: int + bufsz=4096, # type: int ): # type: (...) -> None """ @@ -1935,14 +2323,14 @@ def __init__(self, """ - self.linktype = linktype + 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 @@ -1977,7 +2365,7 @@ def _write_header(self, pkt): finally: g.close() - if self.linktype is None: + if not hasattr(self, 'linktype'): raise ValueError( "linktype could not be guessed. " "Please pass a linktype while creating the writer" @@ -1989,11 +2377,14 @@ def _write_header(self, pkt): 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] - comment=None # type: Optional[bytes] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] ): # type: (...) -> None """ @@ -2001,6 +2392,8 @@ def _write_packet(self, :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: float @@ -2032,7 +2425,7 @@ def _write_packet(self, self.f.write(struct.pack(self.endian + "IIII", int(sec), usec, caplen, wirelen)) - self.f.write(packet) + self.f.write(bytes(packet)) if self.sync: self.f.flush() @@ -2047,7 +2440,9 @@ def __init__(self, self.header_present = False self.tsresol = 1000000 - self.linktype = DLT_EN10MB + # 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 = "<" @@ -2064,7 +2459,7 @@ def _get_time(self, # type: (...) -> Tuple[float, int] if hasattr(packet, "time"): if sec is None: - sec = float(packet.time) # type: ignore + sec = float(packet.time) if usec is None: usec = 0 @@ -2104,7 +2499,7 @@ def _write_header(self, pkt): if not self.header_present: self.header_present = True self._write_block_shb() - self._write_block_idb() + self._write_block_idb(linktype=self.linktype) def _write_block_shb(self): # type: () -> None @@ -2118,23 +2513,34 @@ def _write_block_shb(self): # Minor Version block_shb += struct.pack(self.endian + "H", 0) # Section Length - block_shb += struct.pack(self.endian + "Q", 0) + block_shb += struct.pack(self.endian + "q", -1) self.f.write(self.build_block(block_type, block_shb)) - def _write_block_idb(self): - # type: () -> None + 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", self.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) - self.f.write(self.build_block(block_type, block_idb)) + # 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 @@ -2150,10 +2556,12 @@ def _write_block_spb(self, raw_pkt): 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] - comment=None # type: Optional[bytes] + comments=None, # type: Optional[List[bytes]] + flags=None, # type: Optional[int] ): # type: (...) -> None @@ -2173,7 +2581,7 @@ def _write_block_epb(self, # Block Type block_type = struct.pack(self.endian + "I", 6) # Interface ID - block_epb = struct.pack(self.endian + "I", 0) + block_epb = struct.pack(self.endian + "I", ifid) # Timestamp (High) block_epb += struct.pack(self.endian + "I", ts_high) # Timestamp (Low) @@ -2185,28 +2593,33 @@ def _write_block_epb(self, # Packet Data block_epb += raw_pkt - # Comment option - comment_opt = None - if comment: - comment = bytes_encode(comment) - if not comment.endswith(b"\n"): - comment += b"\n" - comment_opt = struct.pack(self.endian + "HH", 1, len(comment)) - - # Pad Option Value to 32 bits - comment_opt += self._add_padding(bytes_encode(comment)) - comment_opt += struct.pack(self.endian + "HH", 0, 0) + # 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=comment_opt)) + options=opts)) - def _write_packet(self, + 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] - comment=None # type: Optional[bytes] + ifname=None, # type: Optional[bytes] + direction=None, # type: Optional[int] + comments=None, # type: Optional[List[bytes]] ): # type: (...) -> None """ @@ -2214,6 +2627,8 @@ def _write_packet(self, :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: float @@ -2223,6 +2638,21 @@ def _write_packet(self, :param wirelen: The length of the packet on the wire. If not 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 """ @@ -2231,8 +2661,21 @@ def _write_packet(self, 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, comment=comment) + orglen=wirelen, comments=comments, ifid=ifid, flags=flags) if self.sync: self.f.flush() @@ -2253,7 +2696,7 @@ def _get_time(self, # type: (...) -> Tuple[float, int] if hasattr(packet, "time"): if sec is None: - sec = float(packet.time) # type: ignore + sec = float(packet.time) if usec is None: usec = 0 @@ -2273,9 +2716,9 @@ def rderf(filename, count=-1): class ERFEthernetReader_metaclass(PcapReader_metaclass): - def __call__(cls, filename): # type: ignore + def __call__(cls, filename): # type: (Union[IO[bytes], str]) -> Any - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) + i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) # type: ignore filename, fdesc = cls.open(filename) try: i.__init__(filename, fdesc) @@ -2285,7 +2728,12 @@ def __call__(cls, filename): # type: ignore if "alternative" in cls.__dict__: cls = cls.__dict__["alternative"] - i = cls.__new__(cls, cls.__name__, cls.__bases__, cls.__dict__) + i = cls.__new__( + cls, + cls.__name__, + cls.__bases__, + cls.__dict__ # type: ignore + ) try: i.__init__(filename, fdesc) return i @@ -2314,8 +2762,8 @@ def open(fname # type: ignore return filename, fdesc -@six.add_metaclass(ERFEthernetReader_metaclass) -class ERFEthernetReader(PcapReader): +class ERFEthernetReader(PcapReader, + metaclass=ERFEthernetReader_metaclass): def __init__(self, filename, fdesc=None): # type: ignore # type: (Union[IO[bytes], str], IO[bytes]) -> None @@ -2336,8 +2784,8 @@ def _convert_erf_timestamp(self, t): # 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): - # type: (int) -> Packet + def read_packet(self, size=MTU, **kwargs): + # type: (int, **Any) -> Packet # General ERF Header have exactly 16 bytes hdr = self.f.read(16) @@ -2364,10 +2812,10 @@ def read_packet(self, size=MTU): # Ethernet has 2 bytes of padding containing `offset` and `pad`. Both # of the fields are disregarded by Endace. - p = s[2:size] + pb = s[2:size] from scapy.layers.l2 import Ether try: - p = Ether(p) + p = Ether(pb, **kwargs) # type: Packet except KeyboardInterrupt: raise except Exception: @@ -2488,7 +2936,7 @@ def import_hexcap(input_string=None): p = "" try: if input_string: - input_function = six.StringIO(input_string).readline + input_function = StringIO(input_string).readline else: input_function = input while True: @@ -2664,7 +3112,7 @@ def tcpdump( "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") @@ -2708,6 +3156,8 @@ def tcpdump( 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." @@ -2748,7 +3198,7 @@ def tcpdump( stdout=stdout, stderr=stderr, ) - elif isinstance(pktlist, six.string_types): + elif isinstance(pktlist, str): # file with ContextManagerSubprocess(prog[0], suppress=_suppress): proc = subprocess.Popen( @@ -2757,7 +3207,6 @@ def tcpdump( stderr=stderr, ) elif use_tempfile: - pktlist = cast(Union[IO[bytes], "_PacketIterable"], pktlist) tmpfile = get_temp_file( # type: ignore autoext=".pcap", fd=True @@ -2848,15 +3297,10 @@ def get_terminal_width(): 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+) - sizex = None # type: Optional[int] - 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/ @@ -2870,30 +3314,30 @@ def get_terminal_width(): # sizey = bottom - top + 1 return sizex return sizex - else: - # 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: - pass + # 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: int + sortBy=0, # type: Optional[int] borders=False, # type: bool ): # type: (...) -> str @@ -2914,8 +3358,9 @@ def pretty_list(rtlst, # type: List[Tuple[Union[str, List[str]], ...]] # Windows has a fat terminal border _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] @@ -2972,6 +3417,20 @@ def pretty_list(rtlst, # type: List[Tuple[Union[str, List[str]], ...]] return "\n".join(fmt % x for x in rtslst) +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] @@ -2990,9 +3449,6 @@ def __make_table( vz = {} # type: Dict[Tuple[str, str], str] vxf = {} # type: Dict[str, str] - # Python 2 backward compatibility - fxyz = lambda_tuple_converter(fxyz) - tmp_len = 0 for e in data: xx, yy, zz = [str(s) for s in fxyz(*e)] @@ -3126,14 +3582,490 @@ 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, + spaces: bool = False, + globsupport: bool = False, + ) -> Callable[[DecoratorCallable], DecoratorCallable]: + """ + Decorator to register a command + """ + def func(cmd: DecoratorCallable) -> DecoratorCallable: + cmd.cliutil_type = _CLIUtilMetaclass.TYPE.COMMAND # type: ignore + cmd._spaces = spaces # type: ignore + cmd._globsupport = globsupport # type: ignore + cls._inspectkwargs(cmd) + if cmd._globsupport and not cmd._spaces: # type: ignore + raise ValueError("Cannot use globsupport without spaces.") + 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]: # noqa: E501 + """ + 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 + 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 _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 = document.text.split(" ") + 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:]) + arg = " ".join(args) + if cmd in self.commands_complete: + for possible_arg in self.commands_complete[cmd](self, arg): + yield Completion(possible_arg, start_position=-len(arg)) + 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 + args = cmd.split(" ")[1:] + cmd = cmd.split(" ")[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._spaces: # 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: + print("Bad number of arguments !") + 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 + 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): - # type: (Any, _PacketIterable, float) -> None + def __init__(self, sock, pkt, interval=0.5, ignore_exceptions=True): + # type: (Any, _PacketIterable, float, bool) -> None """ Thread to send packets periodically Args: @@ -3147,15 +4079,33 @@ def __init__(self, sock, pkt, interval=0.5): 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): # type: () -> None while not self._stopped.is_set() and not self._socket.closed: for p in self._pkts: - self._socket.send(p) - time.sleep(self._interval) + 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 diff --git a/scapy/utils6.py b/scapy/utils6.py index 26d4003f995..0ee7ee89886 100644 --- a/scapy/utils6.py +++ b/scapy/utils6.py @@ -8,7 +8,6 @@ """ Utility functions for IPv6. """ -from __future__ import absolute_import import socket import struct import time @@ -18,14 +17,18 @@ 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, RandBin from scapy.error import warning, Scapy_Exception from functools import reduce, cmp_to_key -from scapy.compat import ( +from typing import ( Iterator, List, Optional, @@ -86,6 +89,8 @@ 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) + 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 addrs.sort(key=cmp_to_key(cset_sort)) # Sort with global addresses first @@ -590,18 +595,6 @@ def in6_isanycast(x): # RFC 2526 return False -def _in6_bitops(xa1, xa2, operator=0): - # type: (bytes, bytes, int) -> bytes - a1 = struct.unpack('4I', xa1) - a2 = struct.unpack('4I', xa2) - 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) - - def in6_or(a1, a2): # type: (bytes, bytes) -> bytes """ @@ -609,7 +602,7 @@ def in6_or(a1, a2): 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): @@ -619,7 +612,7 @@ def in6_and(a1, a2): 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): @@ -629,7 +622,7 @@ def in6_xor(a1, a2): 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): @@ -651,6 +644,22 @@ 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 """ diff --git a/scapy/volatile.py b/scapy/volatile.py index bbd331199f6..1500840c909 100644 --- a/scapy/volatile.py +++ b/scapy/volatile.py @@ -9,7 +9,6 @@ Fields that hold random numbers. """ -from __future__ import absolute_import import copy import random import time @@ -22,9 +21,8 @@ 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.libs.six.moves import zip_longest -from scapy.compat import ( +from typing import ( List, TypeVar, Generic, @@ -110,9 +108,12 @@ def _command_args(self): # type: () -> str return '' - def command(self): - # type: () -> str - return "%s(%s)" % (self.__class__.__name__, self._command_args()) + 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 @@ -510,12 +511,12 @@ def __mul__(self, n): return self._fix() * n -class RandString(_RandString[bytes]): +class RandString(_RandString[str]): _DEFAULT_CHARS = (string.ascii_uppercase + string.ascii_lowercase + - string.digits).encode("utf-8") + string.digits) def __init__(self, size=None, chars=_DEFAULT_CHARS): - # type: (Optional[Union[int, RandNum]], bytes) -> None + # type: (Optional[Union[int, RandNum]], str) -> None if size is None: size = RandNumExpo(0.01) self.size = size @@ -535,21 +536,22 @@ def _command_args(self): return ret def _fix(self): - # type: () -> bytes - s = b"" + # type: () -> str + s = "" for _ in range(int(self.size)): - rdm_chr = random.choice(self.chars) - s += rdm_chr if isinstance(rdm_chr, str) else chb(rdm_chr) + s += random.choice(self.chars) return s -class RandBin(RandString): - def __init__(self, size=None): - # type: (Optional[Union[int, RandNum]]) -> None - super(RandBin, self).__init__( - size=size, - chars=b"".join(chb(c) for c in range(256)) - ) +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 @@ -562,12 +564,20 @@ def _command_args(self): 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 @@ -1191,7 +1201,7 @@ def __init__(self, else: # Invalid template raise ValueError("UUID template is invalid") - rnd_f = [RandInt] + [RandShort] * 2 + [RandByte] * 8 # type: ignore # noqa: E501 + rnd_f = [RandInt] + [RandShort] * 2 + [RandByte] * 8 uuid_template = [] # type: List[Union[int, RandNum]] for i, t in enumerate(template): if t == "*": @@ -1412,115 +1422,3 @@ class CorruptedBits(CorruptedBytes): def _fix(self): # type: () -> bytes return corrupt_bits(self.s, self.p, self.n) - - -class CyclicPattern(VolatileValue[bytes]): - """ - Generate a cyclic pattern - - :param size: Size of generated pattern. Default is random size. - :param start: Start offset of the generated pattern. - :param charset_type: Charset types: - 0: basic (0-9A-Za-z) - 1: extended - 2: maximum (almost printable chars) - - - The code of this class was inspired by - - PEDA - Python Exploit Development Assistance for GDB - Copyright (C) 2012 Long Le Dinh - License: This work is licensed under a Creative Commons - Attribution-NonCommercial-ShareAlike 3.0 Unported License. - """ - - @staticmethod - def cyclic_pattern_charset(charset_type=None): - # type: (Optional[int]) -> str - """ - :param charset_type: charset type - 0: basic (0-9A-Za-z) - 1: extended (default) - 2: maximum (almost printable chars) - :return: list of charset - """ - - charset = \ - [string.ascii_uppercase, string.ascii_lowercase, string.digits] - - if charset_type == 1: # extended type - charset[1] = "%$-;" + re.sub("[sn]", "", charset[1]) - charset[2] = "sn()" + charset[2] - - if charset_type == 2: # maximum type - charset += [string.punctuation] - - return "".join( - ["".join(k) for k in zip_longest(*charset, fillvalue="")]) - - @staticmethod - def de_bruijn(charset, n, maxlen): - # type: (str, int, int) -> str - """ - Generate the De Bruijn Sequence up to `maxlen` characters - for the charset `charset` and subsequences of length `n`. - Algorithm modified from wikipedia - https://en.wikipedia.org/wiki/De_Bruijn_sequence - """ - k = len(charset) - a = [0] * k * n - sequence = [] # type: List[str] - - def db(t, p): - # type: (int, int) -> None - if len(sequence) == maxlen: - return - - if t > n: - if n % p == 0: - for j in range(1, p + 1): - sequence.append(charset[a[j]]) - if len(sequence) == maxlen: - return - else: - a[t] = a[t - p] - db(t + 1, p) - for j in range(a[t - p] + 1, k): - a[t] = j - db(t + 1, t) - - db(1, 1) - return ''.join(sequence) - - def __init__(self, size=None, start=0, charset_type=None): - # type: (Optional[int], int, Optional[int]) -> None - self.size = size if size is not None else RandNumExpo(0.01) - self.start = start - self.charset_type = charset_type - - 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.start != 0: - ret += ", start=%r" % self.start - - if self.charset_type: - ret += ", charset_type=%r" % self.charset_type - - return ret - - def _fix(self): - # type: () -> bytes - if isinstance(self.size, VolatileValue): - size = self.size._fix() - else: - size = self.size - charset = self.cyclic_pattern_charset(self.charset_type or 0) - pattern = self.de_bruijn(charset, 3, size + self.start) - return pattern[self.start:size + self.start].encode('utf-8') 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 6f096c420da..b1c21b579bb 100755 --- a/setup.py +++ b/setup.py @@ -1,19 +1,28 @@ #! /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 + 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,77 +38,52 @@ 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) + 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>=3.0.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='GPL-2.0-only', - 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", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Topic :: Security", - "Topic :: System :: Networking", - "Topic :: System :: Networking :: Monitoring", - ] ) diff --git a/test/answering_machines.uts b/test/answering_machines.uts index 197ca4b2d38..15a8e01e436 100644 --- a/test/answering_machines.uts +++ b/test/answering_machines.uts @@ -8,15 +8,21 @@ + 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 @@ -32,12 +38,24 @@ 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 ("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): @@ -50,17 +68,140 @@ 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 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.rdata == "192.168.1.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, - IP()/UDP()/DNS(qd=DNSQR(qname="www.secdev.org")), + 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, + 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 a = DHCPv6_am() @@ -102,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): @@ -123,3 +264,161 @@ def check_WiFi_am_reply(packet): 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/bpf.uts b/test/bpf.uts index eea7c51141b..5c7fb861973 100644 --- a/test/bpf.uts +++ b/test/bpf.uts @@ -12,13 +12,13 @@ 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 conf.loopback_name += Get the MAC address of conf.loopback_name -get_if_raw_hwaddr(conf.loopback_name) == (ARPHDR_LOOPBACK, b'\x00'*6) +get_if_hwaddr(conf.loopback_name) == "00:00:00:00:00:00" ############ @@ -36,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) @@ -50,8 +50,7 @@ len(iflist) > 0 = 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()]) @@ -115,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): diff --git a/test/configs/bsd.utsc b/test/configs/bsd.utsc index f1ac3007460..912a4f7c210 100644 --- a/test/configs/bsd.utsc +++ b/test/configs/bsd.utsc @@ -8,6 +8,7 @@ "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": [ @@ -15,7 +16,9 @@ "test/windows.uts", "test/contrib/automotive/ecu_am.uts", "test/contrib/automotive/gm/gmlanutils.uts", - "test/contrib/isotpscan.uts" + "test/contrib/isotp_packet.uts", + "test/contrib/isotpscan.uts", + "test/contrib/isotp_soft_socket.uts" ], "onlyfailed": true, "preexec": { @@ -28,6 +31,8 @@ "linux", "windows", "ipv6", - "vcan_socket" + "vcan_socket", + "tun", + "tap" ] } diff --git a/test/configs/cryptography.utsc b/test/configs/cryptography.utsc index debb353a173..53b307d2897 100644 --- a/test/configs/cryptography.utsc +++ b/test/configs/cryptography.utsc @@ -1,17 +1,21 @@ { "testfiles": [ - "test/tls*.uts", + "test/contrib/macsec.uts", "test/scapy/layers/dot11.uts", "test/scapy/layers/ipsec.uts", - "test/contrib/macsec.uts" + "test/scapy/layers/kerberos.uts", + "test/scapy/layers/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/tls*.uts": "load_layer(\"tls\")" + "test/scapy/layers/tls/*.uts": "load_layer(\"tls\")" }, "kw_ko": [ - "mock" + "mock", + "needs_root" ] } diff --git a/test/configs/linux.utsc b/test/configs/linux.utsc index 87d6b71c649..25fbb6bd1d6 100644 --- a/test/configs/linux.utsc +++ b/test/configs/linux.utsc @@ -2,6 +2,7 @@ "testfiles": [ "test/*.uts", "test/scapy/layers/*.uts", + "test/scapy/layers/tls/*.uts", "test/contrib/*.uts", "test/tools/*.uts", "test/contrib/automotive/*.uts", @@ -10,7 +11,7 @@ "test/contrib/automotive/gm/*.uts", "test/contrib/automotive/bmw/*.uts", "test/contrib/automotive/xcp/*.uts", - "test/tls/tests_tls_netaccess.uts" + "test/contrib/automotive/autosar/*.uts" ], "remove_testfiles": [ "test/windows.uts", @@ -20,9 +21,7 @@ "onlyfailed": true, "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\")" }, "kw_ko": [ "osx", diff --git a/test/configs/solaris.utsc b/test/configs/solaris.utsc index ada75031993..d76e8b77a4f 100644 --- a/test/configs/solaris.utsc +++ b/test/configs/solaris.utsc @@ -8,6 +8,7 @@ "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": [ diff --git a/test/configs/windows.utsc b/test/configs/windows.utsc index 0f7ad559324..468979a5ca5 100644 --- a/test/configs/windows.utsc +++ b/test/configs/windows.utsc @@ -2,13 +2,14 @@ "testfiles": [ "test\\*.uts", "test\\scapy\\layers\\*.uts", - "test\\tls\\tests_tls_netaccess.uts", + "test\\scapy\\layers\\tls\\*.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": [ @@ -19,14 +20,15 @@ "onlyfailed": true, "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\")" }, "kw_ko": [ + "as_resolvers", "brotli", + "broken_windows", "ipv6", "linux", + "native_tls13", "mock_read_routes_bsd", "open_ssl_client", "osx", diff --git a/test/configs/windows2.utsc b/test/configs/windows2.utsc index 1435cd8047a..a1c8e302953 100644 --- a/test/configs/windows2.utsc +++ b/test/configs/windows2.utsc @@ -2,11 +2,12 @@ "testfiles": [ "*.uts", "scapy\\layers\\*.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\\tls\\*.uts", + "contrib\\automotive\\obd\\*.uts", + "contrib\\automotive\\gm\\*.uts", + "contrib\\automotive\\bmw\\*.uts", + "contrib\\automotive\\*.uts", + "contrib\\automotive\\autosar\\*.uts", "contrib\\*.uts" ], "remove_testfiles": [ @@ -17,17 +18,16 @@ "onlyfailed": true, "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", 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/hsfz.uts b/test/contrib/automotive/bmw/hsfz.uts index e889867b1dd..9f199b38012 100644 --- a/test/contrib/automotive/bmw/hsfz.uts +++ b/test/contrib/automotive/bmw/hsfz.uts @@ -13,36 +13,36 @@ load_contrib("automotive.bmw.hsfz", globals_dict=globals()) = Basic Test 1 -pkt = HSFZ(type=1, src=0xf4, dst=0x10)/Raw(b'\x11\x22\x33') +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(type=1, src=0xf4, dst=0x10)/Raw(b'\x11\x22\x33\x11\x11\x11\x11\x11') +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.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 +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(src=0xf4, dst=0x10)/Raw(b"0" * 20) +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.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 +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 @@ -50,9 +50,9 @@ assert len(pkt[1]) == pkt.length - 2 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.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 +assert pkt.source == 0xf4 +assert pkt.target == 0x10 +assert pkt.control == 1 assert pkt.securitySeed == b"0" * 20 assert pkt.load == b'p' * 100 @@ -60,17 +60,103 @@ assert pkt.load == b'p' * 100 pkt = HSFZ(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.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.src == 0xf4 -assert pkt.dst == 0x10 -assert pkt.type == 1 +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 467d41c287b..b267317a580 100644 --- a/test/contrib/automotive/ccp.uts +++ b/test/contrib/automotive/ccp.uts @@ -877,7 +877,7 @@ assert dto.hashret() == cro.hashret() + Tests on a virtual CAN-Bus -= CAN Socket sr1 with dto.ansers(cro) == True += CAN Socket sr1 with dto.answers(cro) == True sock1 = TestSocket(CCP) sock2 = TestSocket(CAN) @@ -903,7 +903,7 @@ assert hasattr(dto, "load") == False assert dto.MTA0_extension == 2 assert dto.MTA0_address == 0x34002006 -= CAN Socket sr1 with dto.ansers(cro) == False += CAN Socket sr1 with dto.answers(cro) == False sock1 = TestSocket(CCP) sock2 = TestSocket(CAN) diff --git a/test/contrib/automotive/doip.uts b/test/contrib/automotive/doip.uts index 33dc9793f6d..f224e39a9c6 100644 --- a/test/contrib/automotive/doip.uts +++ b/test/contrib/automotive/doip.uts @@ -10,6 +10,8 @@ = 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()) @@ -297,7 +299,7 @@ assert p.previous_msg == b'\x10\x03' + pcap based tests = read diag_ack pcap file -pkt = rdpcap("test/pcaps/doip_ack.pcap").res[0] +pkt = rdpcap(scapy_path("test/pcaps/doip_ack.pcap")).res[0] assert len(pkt) == 70 @@ -313,7 +315,7 @@ assert pkt.previous_msg == b'\x22\xFD\x31' = read main pcap file -pkts = rdpcap("test/pcaps/doip.pcap.gz") +pkts = rdpcap(scapy_path("test/pcaps/doip.pcap.gz")) ips = [p for p in pkts if p.proto == 6] assert len(ips) > 1 @@ -380,3 +382,543 @@ 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 af1d70e67db..4ced520357b 100644 --- a/test/contrib/automotive/ecu.uts +++ b/test/contrib/automotive/ecu.uts @@ -606,8 +606,9 @@ assert unanswered_packets[0].diagnosticSessionType == 4 = Analyze multiple UDS messages -with PcapReader(scapy_path("test/pcaps/ecu_trace.pcap.gz")) as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"use_ext_address":False, "basecls":UDS}, count=50, opened_socket=sock, timeout=3) +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 @@ -637,7 +638,7 @@ assert len(ecu.log["TransferData"]) == 2 session = EcuSession() with PcapReader(scapy_path("test/pcaps/ecu_trace.pcap.gz")) as sock: - udsmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "use_ext_address":False, "basecls":UDS}, count=50, opened_socket=sock, timeout=3) + udsmsgs = sniff(session=ISOTPSession(supersession=session, use_ext_address=False, basecls=UDS), count=50, opened_socket=sock, timeout=3) assert len(udsmsgs) == 50 @@ -667,12 +668,12 @@ session = EcuSession() conf.contribs['CAN']['swap-bytes'] = True with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=2, opened_socket=sock, timeout=3) + 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.state == EcuState(session=3) - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=8, opened_socket=sock) + 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 read1") assert len(ecu.supported_responses) == 3 @@ -680,13 +681,13 @@ with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: assert ecu.state.session == 3 print("assert 1") assert ecu.state.communication_control == 1 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=10, opened_socket=sock) + 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) == 4 assert ecu.state.session == 2 assert ecu.state.communication_control == 1 - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=16, opened_socket=sock) + 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) == 6 @@ -701,8 +702,10 @@ session = EcuSession(verbose=False, store_supported_responses=False) conf.contribs['GMLAN']['GMLAN_ECU_AddressingScheme'] = 4 conf.contribs['CAN']['swap-bytes'] = True -with PcapReader(scapy_path("test/pcaps/gmlan_trace.pcap.gz")) as sock: - gmlanmsgs = sniff(session=ISOTPSession, session_kwargs={"supersession": session, "rx_id":[0x241, 0x641, 0x101], "basecls":GMLAN}, count=200, opened_socket=sock, timeout=6) +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 diff --git a/test/contrib/automotive/gm/gmlanutils.uts b/test/contrib/automotive/gm/gmlanutils.uts index 0d489f6fb43..68ee1c99480 100644 --- a/test/contrib/automotive/gm/gmlanutils.uts +++ b/test/contrib/automotive/gm/gmlanutils.uts @@ -1,4 +1,5 @@ % Regression tests for gmlanutil +~ scanner + Configuration ~ conf diff --git a/test/contrib/automotive/gm/scanner.uts b/test/contrib/automotive/gm/scanner.uts index 6a232d94c14..59a5e265dc6 100644 --- a/test/contrib/automotive/gm/scanner.uts +++ b/test/contrib/automotive/gm/scanner.uts @@ -108,6 +108,11 @@ 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] @@ -125,6 +130,7 @@ assert True == e._evaluate_response(s, GMLAN(b"\x27\x01"), GMLAN(b"\x67\x01ab"), 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 diff --git a/test/contrib/automotive/interface_mockup.py b/test/contrib/automotive/interface_mockup.py index 81368f2655e..1e7b2388afd 100644 --- a/test/contrib/automotive/interface_mockup.py +++ b/test/contrib/automotive/interface_mockup.py @@ -15,7 +15,6 @@ from scapy.main import load_layer, load_contrib from scapy.config import conf from scapy.error import log_runtime, Scapy_Exception -import scapy.libs.six as six from scapy.consts import LINUX load_layer("can", globals_dict=globals()) @@ -73,21 +72,12 @@ def test_and_setup_socket_can(iface_name): # """ Define helper functions for CANSocket creation on all platforms """ # ############################################################################ if _socket_can_support: - if six.PY3: - 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='socketcan', channel=iface, timeout=0.01) # noqa: E501 - new_can_socket0 = lambda: PythonCANSocket(bustype='socketcan', channel=iface0, timeout=0.01) # noqa: E501 - new_can_socket1 = lambda: PythonCANSocket(bustype='socketcan', channel=iface1, timeout=0.01) # noqa: E501 - can_socket_string_list = ["-i", "socketcan", "-c", iface0] - sys.__stderr__.write("Using PythonCANSocket socketcan\n") + 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 @@ -171,7 +161,7 @@ def exit_if_no_isotp_module(): # ############################################################################ # """ Evaluate if ISOTP kernel module is installed and available """ # ############################################################################ -if LINUX and _root and six.PY3 and _socket_can_support: +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) @@ -192,14 +182,13 @@ def exit_if_no_isotp_module(): # ############################################################################ # """ reload ISOTP kernel module in case configuration changed """ # ############################################################################ -if six.PY3: - import importlib - if "scapy.contrib.isotp" in sys.modules: - importlib.reload(scapy.contrib.isotp) # type: ignore # noqa: F405 +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 six.PY3 and ISOTP_KERNEL_MODULE_AVAILABLE: +if ISOTP_KERNEL_MODULE_AVAILABLE: if ISOTPSocket is not ISOTPNativeSocket: # type: ignore raise Scapy_Exception("Error in ISOTPSocket import!") else: diff --git a/test/contrib/automotive/obd/obd.uts b/test/contrib/automotive/obd/obd.uts index 17f3df69a82..fa65e95e447 100644 --- a/test/contrib/automotive/obd/obd.uts +++ b/test/contrib/automotive/obd/obd.uts @@ -445,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) @@ -465,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 @@ -485,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) @@ -497,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 diff --git a/test/contrib/automotive/scanner/configuration.uts b/test/contrib/automotive/scanner/configuration.uts index 1cf12b7223b..1db1b8706f1 100644 --- a/test/contrib/automotive/scanner/configuration.uts +++ b/test/contrib/automotive/scanner/configuration.uts @@ -93,8 +93,8 @@ try: except KeyError: pass -assert len(config["MyTestCase3"]) == 2 -assert len(config["MyTestCase2"]) == 3 +assert len(config["MyTestCase3"]) == 3 +assert len(config["MyTestCase2"]) == 4 try: print(config["MyTestCase3"]["local_config"]) diff --git a/test/contrib/automotive/scanner/enumerator.uts b/test/contrib/automotive/scanner/enumerator.uts index b0024d7a7c2..b1ac0cc8716 100644 --- a/test/contrib/automotive/scanner/enumerator.uts +++ b/test/contrib/automotive/scanner/enumerator.uts @@ -1,4 +1,5 @@ % Regression tests for enumerators +~ linux + Load general modules @@ -13,7 +14,7 @@ from scapy.contrib.automotive.scanner.staged_test_case import StagedAutomotiveTe 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 @@ -188,7 +189,7 @@ assert e._retry_pkt[EcuState(session=1)] = ServiceEnumerator execute -from scapy.libs.six.moves.queue import Queue +from queue import Queue from scapy.supersocket import SuperSocket class MockISOTPSocket(SuperSocket): @@ -218,7 +219,19 @@ class MockISOTPSocket(SuperSocket): 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") @@ -387,10 +400,10 @@ 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()) == 4 -assert len(config.TestCase2.items()) == 3 -assert len(config["TestCase1"].items()) == 4 -assert len(config.MyTestCase.items()) == 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 @@ -408,7 +421,7 @@ config = tce.configuration # type: AutomotiveTestCaseExecutorConfiguration assert not config.verbose assert not config.debug assert len(config.test_cases) == 1 -assert len(config.MyTestCase.items()) == 0 +assert len(config.MyTestCase.items()) == 1 assert isinstance(tce.socket, SingleConversationSocket) @@ -432,7 +445,7 @@ 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()) == 0 +assert len(config.StagedAutomotiveTestCase.items()) == 1 assert isinstance(tce.socket, SingleConversationSocket) = Basic tests with two stages @@ -460,11 +473,11 @@ 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()) == 1 -assert len(config.StagedTest.items()) == 1 -assert len(config.TestCase1.items()) == 1 -assert len(config.TestCase2.items()) == 1 -assert len(config.MyTestCase.items()) == 1 +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) @@ -990,6 +1003,10 @@ assert tce.scan_completed 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")), @@ -999,6 +1016,10 @@ class MyTestCase1(AutomotiveTestCase): 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")), @@ -1067,4 +1088,4 @@ 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)) \ No newline at end of file +assert not tce.enter_state(EcuState(session=1), EcuState(session=3)) diff --git a/test/contrib/automotive/scanner/uds_scanner.uts b/test/contrib/automotive/scanner/uds_scanner.uts index 78a39287c6f..49649b52d4c 100644 --- a/test/contrib/automotive/scanner/uds_scanner.uts +++ b/test/contrib/automotive/scanner/uds_scanner.uts @@ -25,10 +25,12 @@ from scapy.contrib.automotive.ecu import * load_layer("can") +conf.debug_dissector = False + = Define Testfunction -def executeScannerInVirtualEnvironment(supported_responses, enumerators, unstable_socket=True, **kwargs): +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 @@ -56,11 +58,26 @@ def executeScannerInVirtualEnvironment(supported_responses, enumerators, unstabl sim = threading.Thread(target=answering_machine_thread) try: sim.start() - scanner = UDS_Scanner( - tester, reset_handler=reset, reconnect_handler=reconnect, - test_cases=enumerators, timeout=0.1, - retry_if_none_received=True, unittest=True, - **kwargs) + 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) @@ -74,7 +91,7 @@ def executeScannerInVirtualEnvironment(supported_responses, enumerators, unstabl cleanup_testsockets() tester_obj_pipe.close() ecu_obj_pipe.close() - if six.PY3 and LINUX: + if LINUX: pickle_test(scanner) return scanner @@ -192,9 +209,11 @@ scanner = executeScannerInVirtualEnvironment( 0x2A, 0x2C, 0x2E, 0x2F, 0x31, 0x34, 0x35, 0x36, 0x37, 0x38, 0x3D, 0x3E, 0x83, 0x84, 0x85, - 0x87]}) + 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 @@ -224,17 +243,180 @@ 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) == 8 +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 "incorrectMessageLengthOrInvalidFormat received 17 times" in result +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] @@ -254,6 +436,54 @@ 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")]), @@ -613,7 +843,7 @@ resps = [EcuResponse(None, [UDS()/UDS_CCPR(controlType=1)]), es = [UDS_CCEnumerator] -scanner = executeScannerInVirtualEnvironment(resps, es) +scanner = executeScannerInVirtualEnvironment(resps, es, inter=0.001) assert scanner.scan_completed assert scanner.progress() > 0.95 @@ -1028,6 +1258,71 @@ 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 diff --git a/test/contrib/automotive/someip.uts b/test/contrib/automotive/someip.uts index 183b2202f1c..ec1a38f780d 100644 --- a/test/contrib/automotive/someip.uts +++ b/test/contrib/automotive/someip.uts @@ -55,12 +55,10 @@ binstr = b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x00\x00\xde\ 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") +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 == 0x1 -assert p.method_id == None -assert p.event_id == 0x0111 +assert p.sub_id == 0x8111 assert p.client_id == 0x3333 assert p.session_id == 0x4444 assert p.proto_ver == 0x02 @@ -68,13 +66,12 @@ 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") +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 == 0x0 -assert p.method_id == 0x0111 -assert p.event_id == None +assert p.sub_id == 0x0111 assert p.client_id == 0x3333 assert p.session_id == 0x4444 assert p.proto_ver == 0x02 @@ -116,7 +113,7 @@ pstr = bytes(p) binstr = b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x01\x00\x00" assert pstr == binstr -= Build TP fragmented += Build TP fragmented payload p = SOMEIP() p.msg_type = 0x20 p.add_payload(Raw("A"*1400)) @@ -130,6 +127,20 @@ 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 @@ -196,21 +207,26 @@ assert p.eventgroup_id == 0x8888 = Build and check flags p = SD() -p.set_flag("REBOOT", 1) +p.flags = "REBOOT" assert p.flags == 0x80 -p.set_flag("REBOOT", 0) +p.flags = "" assert p.flags == 0x00 -p.set_flag("UNICAST", 1) +p.flags = "UNICAST" assert p.flags == 0x40 -p.set_flag("UNICAST", 0) +p.flags = "" +assert p.flags == 0x00 + +p.flags = "EXPLICIT_INITIAL_DATA_CONTROL" +assert p.flags == 0x20 + +p.flags = "" assert p.flags == 0x00 -p.set_flag("REBOOT", 1) -p.set_flag("UNICAST", 1) -assert p.flags == 0xc0 +p.flags = "REBOOT+UNICAST+EXPLICIT_INITIAL_DATA_CONTROL" +assert p.flags == 0xe0 + SD Get SOME/IP Packet @@ -220,8 +236,7 @@ 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_MSGID_SUBID == 0x8100 assert SD.SOMEIP_CLIENT_ID == 0x0000 assert SD.SOMEIP_MINIMUM_SESSION_ID == 0x0001 assert SD.SOMEIP_PROTO_VER == 0x01 @@ -232,7 +247,6 @@ 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 @@ -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/uds.uts b/test/contrib/automotive/uds.uts index 221554dd9a7..1545076039d 100644 --- a/test/contrib/automotive/uds.uts +++ b/test/contrib/automotive/uds.uts @@ -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 @@ -1046,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') @@ -1086,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 @@ -1096,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 @@ -1113,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 @@ -1139,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') @@ -1171,6 +1195,23 @@ 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') @@ -1343,8 +1384,7 @@ 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 @@ -1387,6 +1427,11 @@ rftpr_build = UDS()/UDS_RFTPR(modeOfOperation=0x1, 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 nrc = UDS(b'\x7f\x22\x33') diff --git a/test/contrib/bfd.uts b/test/contrib/bfd.uts index 88517005489..7de9dd30681 100644 --- a/test/contrib/bfd.uts +++ b/test/contrib/bfd.uts @@ -2,10 +2,46 @@ = BFD, basic instantiation -from scapy.contrib.bfd import BFD +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' \ No newline at end of file diff --git a/test/contrib/bgp.uts b/test/contrib/bgp.uts index d9e0c5992a9..3d8e30f2431 100644 --- a/test/contrib/bgp.uts +++ b/test/contrib/bgp.uts @@ -87,6 +87,13 @@ h = BGP(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x 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 @@ -315,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 ################################# @@ -597,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 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 002b2021a89..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/ diff --git a/test/contrib/cansocket_native.uts b/test/contrib/cansocket_native.uts index e5dae7a5ec9..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/ @@ -140,7 +140,7 @@ 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 diff --git a/test/contrib/cansocket_python_can.uts b/test/contrib/cansocket_python_can.uts index 21c12a373b3..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/ @@ -294,7 +294,7 @@ sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x0 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.3, count=4) +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) assert len(packetsVCan0) == 4 sock0.close() @@ -336,8 +336,8 @@ sock1.send(CAN(flags='extended', identifier=0x40, length=8, data=b'\x01\x02\x03\ 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.3, count=4) -packetsVCan1 = sock1.sniff(timeout=0.3, count=6) +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan0) == 4 assert len(packetsVCan1) == 6 @@ -377,7 +377,7 @@ sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x0 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.3, count=6) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan1) == 6 sock0.close() @@ -415,7 +415,7 @@ sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x0 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, count=4) +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) assert len(packetsVCan0) == 4 sock0.close() @@ -459,8 +459,8 @@ sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x0 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, count=4) -packetsVCan1 = sock1.sniff(timeout=0.3, count=6) +packetsVCan0 = sock0.sniff(timeout=0.5, count=4) +packetsVCan1 = sock1.sniff(timeout=0.5, count=6) assert len(packetsVCan0) == 4 assert len(packetsVCan1) == 6 @@ -482,7 +482,7 @@ def bridgeWithRemovePackageFromVCan0ToVCan1(): bSock1 = CANSocket(bustype='socketcan', channel='vcan1') def pnr(pkt): if(pkt.identifier == 0x10020000): - pkt = None + pkt = False else: pkt = pkt return pkt @@ -503,7 +503,7 @@ sock0.send(CAN(flags='extended', identifier=0x10030000, length=8, data=b'\x01\x0 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.3, count=5) +packetsVCan1 = sock1.sniff(timeout=0.5, count=5) assert len(packetsVCan1) == 5 @@ -525,7 +525,7 @@ def bridgeWithRemovePackageFromVCan1ToVCan0(): bSock1 = CANSocket(bustype='socketcan', channel='vcan1') def pnr(pkt): if(pkt.identifier == 0x10050000): - pkt = None + pkt = False else: pkt = pkt return pkt @@ -544,7 +544,7 @@ sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x0 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, count=3) +packetsVCan0 = sock0.sniff(timeout=0.5, count=3) assert len(packetsVCan0) == 3 @@ -567,13 +567,13 @@ 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 @@ -598,8 +598,8 @@ sock1.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x0 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, count=3) -packetsVCan1 = sock1.sniff(timeout=0.3, count=5) +packetsVCan0 = sock0.sniff(timeout=0.5, count=3) +packetsVCan1 = sock1.sniff(timeout=0.5, count=5) assert len(packetsVCan0) == 3 assert len(packetsVCan1) == 5 diff --git a/test/contrib/cdp.uts b/test/contrib/cdp.uts index 06b9d7f598d..9a6601bcc72 100644 --- a/test/contrib/cdp.uts +++ b/test/contrib/cdp.uts @@ -89,3 +89,27 @@ 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 2026bd97498..16dfc0f99b7 100644 --- a/test/contrib/coap.uts +++ b/test/contrib/coap.uts @@ -27,7 +27,8 @@ assert p.options == [('Uri-Path', b'.well-known'), ('Uri-Path', b'core')] 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' 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/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 3c32c2ffdda..d2d820c669e 100644 --- a/test/contrib/enipTCP.uts +++ b/test/contrib/enipTCP.uts @@ -6,6 +6,7 @@ from scapy.contrib.enipTCP import * #from scapy.all import * + + Test ENIP/TCP Encapsulation Header = Encapsulation Header Default Values pkt=ENIPTCP() @@ -17,40 +18,44 @@ 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 -= 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 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 @@ -60,14 +65,15 @@ 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 @@ -76,14 +82,13 @@ 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 @@ -106,6 +111,7 @@ assert pkt.senderContext == 0 assert pkt.options == 0 assert pkt[ENIPRegisterSession].protocolVersion == 1 assert pkt[ENIPRegisterSession].options == 0 +raw(pkt) + ENIP Send RR Data @@ -115,10 +121,10 @@ 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' @@ -129,10 +135,9 @@ 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.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' @@ -144,12 +149,13 @@ 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.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 @@ -158,16 +164,14 @@ 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 @@ -175,19 +179,13 @@ 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.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 index cdff9e2b64b..e4465b382f1 100644 --- a/test/contrib/erspan.uts +++ b/test/contrib/erspan.uts @@ -14,14 +14,14 @@ assert pkt.seqnum_present == 0 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\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\x11\x11\x11\x11\x11\x11\x90\x00' +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 == 0 +assert pkt[GRE][ERSPAN].ver == 1 assert pkt[Ether].src == "11:11:11:11:11:11" + ERSPAN III diff --git a/test/contrib/ethercat.uts b/test/contrib/ethercat.uts index 20f63571759..6e1d872212e 100644 --- a/test/contrib/ethercat.uts +++ b/test/contrib/ethercat.uts @@ -183,6 +183,9 @@ assert frm[EtherCat].length == 60 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 @@ -215,6 +218,8 @@ assert frm[EtherCatAPRD].c == 0 assert frm[EtherCat]._reserved == 0 +conf.max_list_count = old_max_list_count + = EtherCat and Type12 DLPDU layers for type_id in EtherCat.ETHERCAT_TYPE12_DLPDU_TYPES: diff --git a/test/contrib/geneve.uts b/test/contrib/geneve.uts index 5811f56741b..5e730dcaf3a 100644 --- a/test/contrib/geneve.uts +++ b/test/contrib/geneve.uts @@ -30,6 +30,16 @@ assert (s == b'\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x81\x00\x00\x01\ 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()) @@ -66,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 569528a9cd7..155c258aef3 100644 --- a/test/contrib/gtp.uts +++ b/test/contrib/gtp.uts @@ -22,17 +22,17 @@ 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=0, 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 == 0 @@ -55,7 +55,7 @@ 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].P == 1 and gtp[GTP_U_Header].R == 1 and gtp[GTP_U_Header].QFI == 63 and gtp[GTP_U_Header].PPI == 4 +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' diff --git a/test/contrib/gtp_v2.uts b/test/contrib/gtp_v2.uts index eefdc57e956..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" @@ -196,15 +199,16 @@ ie = IE_MMContext_EPS(ietype=107, length=70, CR_flag=0, instance=0, Sec_Mode=4, 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 = "d89ef3da40e2fa163e956dce08004500007f0001000040114bbd0a0a0f3d0a0f0b5b084b084b006b5a234883005f0000180f76d163006b0046008800910000020000021890aa80be385102083701a2907066f8bd9f2a28b717671c71c71c71c71c71c70100003d090002625a00028040000812345678900000000000000000006d000900880005000470677731" +h = "d89ef3da40e2fa163e956dce08004500008a0001000040114bbd0a0a0f3d0a0f0b5b084b084b00765a234883006a0000180f76d163006b0046008800910000020000021890aa80be385102083701a2907066f8bd9f2a28b717671c71c71c71c71c71c70100003d090002625a00028040000812345678900000000000000000006d0014008800100004706777310474657374056c6f63616c" gtp = Ether(hex_bytes(h)) ie = gtp.IE_list[1].IE_list[0] -ie.fqdn_tr_bit == 4 and ie.fqdn == b'pgw1' +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_tr_bit=4, fqdn=b'pgw1')], ietype=109, length=9, CR_flag=0, instance=0) +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_tr_bit == 4 and ie2.fqdn == b'pgw1' +ie2.fqdn == b'pgw1.test.local' = IE_PAA, dissection h = "3333333333332222222222228100a384080045b800ed00000000fc1193430a2a00010a2a00027f61084b00d91c47482000cd140339f4d99f66000100080002081132547600004b000800000000000001e24056000d001832f420303932f4200001e2405300030032f4205200010006570009008a000010927f0000025700090187000010927f00000247001a00196161616161616161616161616161616161616161616161616163000100014f000500017f0000034d0004000808000048000800000017000000a4105d002c0049000100e55700090385000010927f00000250001600580700000000000000000000000000000000000000007200020014005311004c" @@ -347,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" @@ -439,6 +443,11 @@ assert not (GTPHeader(seq=1)/GTPV2EchoResponse()).answers(GTPHeader(seq=1)/GTPV2 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", seq=1) / ("X"*32) res = GTPHeader(gtp_type="create_session_res", seq=1) / ("Y"*32) 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 26ac760bcff..3c195ccbe01 100644 --- a/test/contrib/http2.uts +++ b/test/contrib/http2.uts @@ -1438,7 +1438,7 @@ assert str(h[16]) == 'accept-encoding: gzip, deflate' 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) @@ -1493,7 +1493,7 @@ assert tbl.get_idx_by_name('x-requested-by') == h2.HPackHdrTable._static_entries 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) @@ -1522,7 +1522,7 @@ 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) @@ -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( @@ -1789,6 +1789,101 @@ 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) +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 + = HTTP/2 HPackHdrTable : Parsing Textual Representation without body ~ http2 hpack hpackhdrtable helpers diff --git a/test/contrib/icmp_extensions.uts b/test/contrib/icmp_extensions.uts deleted file mode 100644 index 13bdaf4482f..00000000000 --- a/test/contrib/icmp_extensions.uts +++ /dev/null @@ -1,8 +0,0 @@ -+ ICMP Extensions tests - -= Basic build - -p = IP(src="192.0.2.1", dst="192.0.2.2")/ICMP()/ICMPExtensionHeader(version = 2)/ICMPExtensionMPLS(classnum = 1, classtype = 1) -print(raw(p)) -b = b'E\x00\x00$\x00\x01\x00\x00@\x01\xf6\xd4\xc0\x00\x02\x01\xc0\x00\x02\x02\x08\x00\xf6\xfa\x00\x00\x00\x00 \x00\xdf\xff\x00\x04\x01\x01' -assert raw(p) == b diff --git a/test/contrib/ikev2.uts b/test/contrib/ikev2.uts index a29ed5de879..9d51493daa1 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,29 +85,27 @@ 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 -a = IKEv2_payload_CERT_CRT(raw(IKEv2_payload_CERT_CRT())) -assert len(a.x509Cert) > 0 -assert a.length == len(a.x509Cert) + 5 -b = IKEv2_payload_CERT_CRL(raw(IKEv2_payload_CERT_CRL())) -assert len(b.x509CRL) > 0 -assert b.length == len(b.x509CRL) + 5 - -c = IKEv2_payload_CERT_STR(raw(IKEv2_payload_CERT_STR(cert_data=b'dummy'))) +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 @@ -117,28 +120,1618 @@ assert isinstance(c, EncryptedTrafficSelector) = Test TSi with multiple TrafficSelector dissection -a = IKEv2_payload_TSi() +a = IKEv2_TSi() a.traffic_selector.extend(IPv4TrafficSelector() * 2) a.traffic_selector.extend(IPv6TrafficSelector() * 3) assert len(a.traffic_selector) == 5 -b = IKEv2_payload_TSi(raw(a)) +b = IKEv2_TSi(raw(a)) assert len(b.traffic_selector) == 5 = Test automatic calculation of number_of_TSs field -a = IKEv2_payload_TSi(traffic_selector=IPv4TrafficSelector() * 2) -b = IKEv2_payload_TSi(raw(a)) +a = IKEv2_TSi(traffic_selector=IPv4TrafficSelector() * 2) +b = IKEv2_TSi(raw(a)) assert b.number_of_TSs == 2 -c = IKEv2_payload_TSr(traffic_selector=IPv4TrafficSelector() * 2) -d = IKEv2_payload_TSr(raw(c)) +c = IKEv2_TSr(traffic_selector=IPv4TrafficSelector() * 2) +d = IKEv2_TSr(raw(c)) assert d.number_of_TSs == 2 -= IKEv2_payload_Encrypted_Fragment, simple tests += 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=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=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/isotp_message_builder.uts b/test/contrib/isotp_message_builder.uts index 27a67bb90a2..8856f88f50c 100644 --- a/test/contrib/isotp_message_builder.uts +++ b/test/contrib/isotp_message_builder.uts @@ -6,10 +6,7 @@ = Definition of utility functions # hexadecimal to bytes convenience function -if six.PY2: - dhex = lambda s: "".join(s.split()).decode('hex') -else: - dhex = bytes.fromhex +dhex = bytes.fromhex = Import isotp @@ -83,7 +80,7 @@ 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 is 0xE2 +assert msg.rx_ext_address == 0xE2 assert msg.data == dhex("01 02 03 04") = Single CAN frame that has 2 valid interpretations @@ -113,9 +110,9 @@ 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 is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xEA +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") @@ -132,9 +129,9 @@ 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 is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xAE +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 @@ -146,9 +143,9 @@ 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 is 0x1A +assert msg.rx_ext_address == 0x1A assert msg.tx_id == 0x641 -assert msg.ext_address is 0x1A +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 @@ -158,9 +155,9 @@ 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 is 0x07 +assert msg.rx_ext_address == 0x07 assert msg.tx_id == 0x641 -assert msg.ext_address is 0x07 +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) @@ -187,21 +184,21 @@ 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 is 0xEA +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 is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xEA +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 is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xEA +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") @@ -221,9 +218,9 @@ msgs = [ m.feed(msgs) msg = m.pop() assert msg.rx_id == 0x241 -assert msg.rx_ext_address is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xEA +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 @@ -248,9 +245,9 @@ isotpmsgs = [x for x in m] assert len(isotpmsgs) == 3 msg = isotpmsgs[0] assert msg.rx_id == 0x241 -assert msg.rx_ext_address is 0xEA +assert msg.rx_ext_address == 0xEA assert msg.tx_id == 0x641 -assert msg.ext_address is 0xEA +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] @@ -260,4 +257,4 @@ 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 \ No newline at end of file +assert type(msg) == Raw diff --git a/test/contrib/isotp_native_socket.uts b/test/contrib/isotp_native_socket.uts index 62d7dd8db1a..4a61c2e41b1 100644 --- a/test/contrib/isotp_native_socket.uts +++ b/test/contrib/isotp_native_socket.uts @@ -12,10 +12,7 @@ with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: = Definition of constants, utility functions and mock classes # hexadecimal to bytes convenience function -if six.PY2: - dhex = lambda s: "".join(s.split()).decode('hex') -else: - dhex = bytes.fromhex +dhex = bytes.fromhex + Compatibility with can-isotp linux kernel modules @@ -356,6 +353,17 @@ with ISOTPNativeSocket(iface0, tx_id=0x641, rx_id=0x241) as s: 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() @@ -503,7 +511,7 @@ assert not rxThread.is_alive() assert succ + ISOTPNativeSocket MITM attack tests -~ python3_only vcan_socket needs_root linux +~ 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() diff --git a/test/contrib/isotp_packet.uts b/test/contrib/isotp_packet.uts index d3f11a322c6..e0225eb09ef 100644 --- a/test/contrib/isotp_packet.uts +++ b/test/contrib/isotp_packet.uts @@ -14,10 +14,7 @@ from scapy.contrib.isotp.isotp_scanner import get_isotp_packet = Define helpers # hexadecimal to bytes convenience function -if six.PY2: - dhex = lambda s: "".join(s.split()).decode('hex') -else: - dhex = bytes.fromhex +dhex = bytes.fromhex + ISOTP packet check @@ -198,25 +195,19 @@ 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'))) +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 == 0 -assert p.extended_message_size == 2000 +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(message_size=0, - extended_message_size=2000, - data=b'\xad'))) +p = ISOTPHeader(bytes(ISOTPHeader()/ISOTP_FF_FD(message_size=2000, data=b'\xad'))) assert p.length == 7 -assert p.message_size == 0 -assert p.extended_message_size == 2000 +assert p.message_size == 2000 assert len(p.data) == 1 assert p.data == b'\xad' assert p.type == 1 @@ -406,17 +397,6 @@ except Scapy_Exception: assert ex -= Fragment exception -~ not_pypy - -ex = False -try: - fragments = ISOTP(b"a" * (1 << 32)).fragment() -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")), @@ -465,3 +445,70 @@ 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 index fb333685041..2e7bdfaeccf 100644 --- a/test/contrib/isotp_soft_socket.uts +++ b/test/contrib/isotp_soft_socket.uts @@ -7,20 +7,18 @@ = Imports import time from io import BytesIO -import scapy.libs.six as six from scapy.layers.can import * from scapy.contrib.isotp import * from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler from test.testsocket import TestSocket, 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 -try: - from cStringIO import StringIO # Python 2 -except ImportError: - from io import StringIO +from io import StringIO log_stream = StringIO() handler = logging.StreamHandler(log_stream) @@ -30,10 +28,7 @@ log_isotp.addHandler(handler) = Definition of utility functions # hexadecimal to bytes convenience function -if six.PY2: - dhex = lambda s: "".join(s.split()).decode('hex') -else: - dhex = bytes.fromhex +dhex = bytes.fromhex + Test sniffer @@ -59,12 +54,24 @@ with TestSocket(CAN) as s, TestSocket(CAN) as tx_sock: assert sniffed[0]['ISOTP'].data == bytearray(range(1, 0x29)) assert sniffed[0]['ISOTP'].tx_id == 0x641 -assert sniffed[0]['ISOTP'].ext_address is 0xEA +assert sniffed[0]['ISOTP'].ext_address == 0xEA assert sniffed[0]['ISOTP'].rx_id == 0x241 -assert sniffed[0]['ISOTP'].rx_ext_address is 0xEA +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: @@ -77,6 +84,28 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ 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: @@ -89,6 +118,28 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ 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: @@ -109,6 +160,26 @@ with TestSocket(CAN) as cans, TestSocket(CAN) as stim, ISOTPSoftSocket(cans, tx_ 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(): @@ -149,6 +220,26 @@ def test(): 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: @@ -162,6 +253,22 @@ with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241) as s: 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"")) @@ -175,6 +282,25 @@ with ISOTPSoftSocket(TestSocket(CAN), tx_id=0x641, rx_id=0x241, rx_ext_address=0 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) @@ -234,6 +360,19 @@ with TestSocket(CAN) as isocan, ISOTPSoftSocket(isocan, tx_id=0x641, rx_id=0x241 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) @@ -288,6 +427,53 @@ 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 @@ -332,6 +518,51 @@ 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) @@ -375,6 +606,51 @@ 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: @@ -465,7 +741,7 @@ 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''') -pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession, timeout=1, session_kwargs={"use_ext_address": False}) +pkts = sniff(opened_socket=CandumpReader(candump_fd), session=ISOTPSession(use_ext_address=False), timeout=1) assert len(pkts) == 6 if not len(pkts): @@ -548,13 +824,6 @@ isotp = pkts[0] assert isotp.data == dhex("") assert (isotp.rx_id == 0x241) -= ISOTPSession tests - -ses = ISOTPSession() -ses.on_packet_received(None) -ses.on_packet_received([None, None]) -assert True - = 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: @@ -675,6 +944,16 @@ 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 sniff msg = ISOTP(b'\x11\x22\x33\x11\x22\x33\x11\x22\x33\x11\x22\x33') @@ -871,6 +1150,22 @@ with TestSocket(CAN) as cs1, ISOTPSoftSocket(cs1, tx_id=0x641, rx_id=0x241, padd 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 diff --git a/test/contrib/isotpscan.uts b/test/contrib/isotpscan.uts index 1c75e8cf936..85b8eae4759 100644 --- a/test/contrib/isotpscan.uts +++ b/test/contrib/isotpscan.uts @@ -79,7 +79,7 @@ for idx in range(1, 4): sockets.append(ISOTPSoftSocket(sock_recv, tx_id=0x700 + idx, rx_id=0x600 + idx)) found_packets = scan(sock_sender, range(0x5ff, 0x604), - noise_ids=[0x701], sniff_time=0.02) + noise_ids=[0x701], sniff_time=0.1) for s in sockets: s.close() @@ -97,7 +97,7 @@ 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.02) + 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') @@ -208,9 +208,37 @@ with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock verbose=False) s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, " \ - "padding=False, basecls=ISOTP)\n" + "padding=False, fd=False, basecls=ISOTP)\n" s2 = "ISOTPSocket(can0, tx_id=0x603, rx_id=0x703, " \ - "padding=False, basecls=ISOTP)\n" + "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 @@ -235,9 +263,9 @@ with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602), ISOTPSoftSocket(sock verbose=False) s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, " \ - "padding=False, basecls=ISOTP)\n" + "padding=False, fd=False, basecls=ISOTP)\n" s2 = "ISOTPSocket(can0, tx_id=0x603, rx_id=0x703, " \ - "padding=False, basecls=ISOTP)\n" + "padding=False, fd=False, basecls=ISOTP)\n" assert s1 not in result assert s2 in result @@ -264,9 +292,9 @@ with ISOTPSoftSocket(sock_recv1, tx_id=0x702, rx_id=0x602, ext_address=0x11, rx_ verbose=False) s1 = "ISOTPSocket(can0, tx_id=0x602, rx_id=0x702, padding=False, " \ - "ext_address=0x22, rx_ext_address=0x11, basecls=ISOTP)" + "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, basecls=ISOTP)" + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" assert s1 in result assert s2 in result @@ -293,9 +321,9 @@ with ISOTPSoftSocket(sock_recv1, tx_id=0x1ffff702, rx_id=0x1ffff602, ext_address verbose=False) s1 = "ISOTPSocket(can0, tx_id=0x1ffff602, rx_id=0x1ffff702, padding=False, " \ - "ext_address=0x22, rx_ext_address=0x11, basecls=ISOTP)" + "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, basecls=ISOTP)" + "ext_address=0x22, rx_ext_address=0x11, fd=False, basecls=ISOTP)" print(result) assert s1 in result assert s2 in result 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 8bddeb32ebe..bc9ed43b1d9 100644 --- a/test/contrib/lldp.uts +++ b/test/contrib/lldp.uts @@ -83,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 @@ -311,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/macsec.uts b/test/contrib/macsec.uts index 26caaaf2cc4..218373e1fad 100755 --- a/test/contrib/macsec.uts +++ b/test/contrib/macsec.uts @@ -14,13 +14,14 @@ 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].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].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) @@ -30,13 +31,13 @@ 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].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' +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) @@ -47,13 +48,13 @@ 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].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 d[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' assert raw(d) == raw(m) = MACsec - basic decap - decrypted @@ -74,13 +75,13 @@ 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].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' +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) @@ -90,13 +91,13 @@ 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].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 e[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' assert raw(e)[:-16] == raw(m) = MACsec - basic decryption - integrity only @@ -108,13 +109,13 @@ 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].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 d[MACsec].SCI == b'\x52\x54\x00\x13\x01\x56\x00\x01' assert raw(d) == raw(m) = MACsec - basic decap - integrity only @@ -130,71 +131,71 @@ assert raw(r) == raw(p) 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 @@ -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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001F09478A9B09007D06F46E9B6A1DA25DD"))) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F30313233340001F09478A9B09007D06F46E9B6A1DA25DD")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400012F0BC5AF409E06D609EA8B7D0FA5EA50"))) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400012F0BC5AF409E06D609EA8B7D0FA5EA50")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334000117FE1981EBDD4AFC5062697E8BAA0C23"))) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F3031323334000117FE1981EBDD4AFC5062697E8BAA0C23")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400014DBD2F6A754A6CF728CC129BA6931577"))) +ref = Raw(bytes.fromhex("D609B1F056637A0D46DF998D88E5222AB2C2846512153524C0895E8108000F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333400014DBD2F6A754A6CF728CC129BA6931577")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED13B4C72B389DC5018E72A171DD85A5D3752274D3A019FBCAED09A425CD9B2E1C9B72EEE7C9DE7D52B3F3D6A5284F4A6D3FE22A5D6C2B960494C3"))) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED13B4C72B389DC5018E72A171DD85A5D3752274D3A019FBCAED09A425CD9B2E1C9B72EEE7C9DE7D52B3F3D6A5284F4A6D3FE22A5D6C2B960494C3")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457EDC1623F55730C93533097ADDAD25664966125352B43ADACBD61C5EF3AC90B5BEE929CE4630EA79F6CE51912AF39C2D1FDC2051F8B7B3C9D397EF2"))) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457EDC1623F55730C93533097ADDAD25664966125352B43ADACBD61C5EF3AC90B5BEE929CE4630EA79F6CE51912AF39C2D1FDC2051F8B7B3C9D397EF2")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED9CA46984430203ED416EBDC2FE2622BA3E5EAB6961C36383009E187E9B0C88564653B9ABD216441C6AB6F0A232E9E44C978CF7CD84D43484D101"))) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED9CA46984430203ED416EBDC2FE2622BA3E5EAB6961C36383009E187E9B0C88564653B9ABD216441C6AB6F0A232E9E44C978CF7CD84D43484D101")) assert raw(e) == raw(ref) dt = sa.decrypt(e) 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') e = sa.encrypt(m) -ref = Raw(bytes(bytearray.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED88D9F7D1F1578EE34BA7B1ABC89893EF1D3398C9F1DD3E47FBD8553E0FF786EF5699EB01EA10420D0EBD39A0E273C4C7F95ED843207D7A497DFA"))) +ref = Raw(bytes.fromhex("E20106D7CD0DF0761E8DCD3D88E54C2A76D457ED88D9F7D1F1578EE34BA7B1ABC89893EF1D3398C9F1DD3E47FBD8553E0FF786EF5699EB01EA10420D0EBD39A0E273C4C7F95ED843207D7A497DFA")) assert raw(e) == raw(ref) dt = sa.decrypt(e) assert raw(dt) == raw(m) diff --git a/test/contrib/modbus.uts b/test/contrib/modbus.uts index a65b2532aaa..ed9f89564b8 100644 --- a/test/contrib/modbus.uts +++ b/test/contrib/modbus.uts @@ -105,7 +105,7 @@ assert isinstance(p.payload, ModbusPDU0BGetCommEventCounterError) 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') +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') @@ -534,3 +534,10 @@ 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/mqtt.uts b/test/contrib/mqtt.uts index a255b9e3aeb..ad444a05104 100644 --- a/test/contrib/mqtt.uts +++ b/test/contrib/mqtt.uts @@ -112,16 +112,21 @@ assert subscribe.topics[0].QOS == 1 = MQTTSuback, packet instantiation -sk = MQTT()/MQTTSuback(msgid=1, retcode=0) +sk = MQTT()/MQTTSuback(msgid=1, retcodes=[0]) assert sk.type == 9 assert sk.msgid == 1 -assert sk.retcode == 0 +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.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)]) @@ -181,4 +186,4 @@ assert MQTTUnsubscribe in u and len(u.topics) == 2 and u.topics[1].topic == b"c/ = 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" \ No newline at end of file +assert MQTTSubscribe in u and len(u.topics) == 2 and u.topics[1].topic == b"c/d" diff --git a/test/contrib/nsh.uts b/test/contrib/nsh.uts index 5c8f3ac6e4b..0751edd1338 100644 --- a/test/contrib/nsh.uts +++ b/test/contrib/nsh.uts @@ -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/openflow3.uts b/test/contrib/openflow3.uts index b2b88e655ef..d423af65e18 100755 --- a/test/contrib/openflow3.uts +++ b/test/contrib/openflow3.uts @@ -74,6 +74,9 @@ 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)) @@ -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/pcom.uts b/test/contrib/pcom.uts index 110a4db86cd..b5ad57e8586 100755 --- a/test/contrib/pcom.uts +++ b/test/contrib/pcom.uts @@ -16,10 +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 diff --git a/test/contrib/pfcp.uts b/test/contrib/pfcp.uts index ec6a7100576..df2d581b2b6 100644 --- a/test/contrib/pfcp.uts +++ b/test/contrib/pfcp.uts @@ -32,7 +32,7 @@ for name, cls in pfcp_mod.__dict__.items(): def command(pkt): f = [] - for fn, fv in sorted(six.iteritems(pkt.fields), key=lambda item: item[0]): + 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 \ diff --git a/test/contrib/pim.uts b/test/contrib/pim.uts index 195c645e6dc..497e0d97958 100644 --- a/test/contrib/pim.uts +++ b/test/contrib/pim.uts @@ -7,7 +7,7 @@ = 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' +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) @@ -24,6 +24,8 @@ assert (hello_pkt[PIMv2Hello].option[2][PIMv2HelloLANPruneDelay].value[0][PIMv2H 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' @@ -81,33 +83,32 @@ assert (jp_pkt[PIMv2JoinPrune].jp_ips[2].prune_ips[0][PIMv2PruneAddrs].src_ip == = 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)]) + 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)])]) + 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=[]) - ] - ) + 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' @@ -115,14 +116,160 @@ assert raw(join_pkt) == b'\x01\x00^\x00\x00\r\xc2\x02=\x80\x00\x01\x08\x00E\xc0\ 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")]) + 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_rpc.uts b/test/contrib/pnio_rpc.uts index 444768f94c5..62e433d534e 100644 --- a/test/contrib/pnio_rpc.uts +++ b/test/contrib/pnio_rpc.uts @@ -5,11 +5,10 @@ from scapy.layers.dcerpc import * from scapy.contrib.pnio import * from scapy.contrib.pnio_rpc import * -from scapy.libs.six import itervalues = Check that we have UUIDs -for v in itervalues(RPC_INTERFACE_UUID): +for v in RPC_INTERFACE_UUID.values(): assert isinstance(v, UUID) + Check Block 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/roce.uts b/test/contrib/roce.uts index 757163d3049..ff19affec3c 100644 --- a/test/contrib/roce.uts +++ b/test/contrib/roce.uts @@ -124,3 +124,24 @@ 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/rtcp.uts b/test/contrib/rtcp.uts index 13afe20eb59..0686016edf1 100644 --- a/test/contrib/rtcp.uts +++ b/test/contrib/rtcp.uts @@ -76,3 +76,23 @@ raw = b"\x81\xc9\x00\x07\xa2\xdf\x02\x72\x49\x6e\x93\xbd\x00\xff\xff\xff" \ 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 index ea4c25fa3ed..50bb4b33612 100644 --- a/test/contrib/rtps.uts +++ b/test/contrib/rtps.uts @@ -405,3 +405,74 @@ p1 = RTPS( assert p0.build() == d assert p1.build() == d assert p1 == p0 + ++ 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 diff --git a/test/contrib/rtr.uts b/test/contrib/rtr.uts index 55af4e275fd..0ec0d4e4812 100644 --- a/test/contrib/rtr.uts +++ b/test/contrib/rtr.uts @@ -230,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/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 index 2a22a690248..f79d43ecf2e 100644 --- a/test/contrib/stun.uts +++ b/test/contrib/stun.uts @@ -32,7 +32,7 @@ assert parsed.attributes == [ STUNPriority(priority=1847591167), STUNIceControlling(tie_breaker=0x1b0ab98b6e8effa6), STUNUsername(length=37, username="oNph:Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25"), - STUNMessageIntegrity(hmac_sha1=0x77e7f03e7f9e1996be42339159db1f682147bcfc), + STUNMessageIntegrity(hmac_sha1=0xfcbc4721681fdb59913342be96199e7f3ef0e777), STUNFingerprint(crc_32=0x8718c3a4) ] @@ -59,7 +59,7 @@ assert parsed.attributes == [ STUNIceControlling(tie_breaker=0xa696819e91c937da), STUNUseCandidate(), STUNPriority(priority=1845501695), - STUNMessageIntegrity(hmac_sha1=0x5fbd89b9c76b674db13a433112f3e0b1faaa87c1), + STUNMessageIntegrity(hmac_sha1=0xc187aafab1e0f31231433ab14d676bc7b989bd5f), STUNFingerprint(crc_32=0xc9566cfc) ] @@ -78,8 +78,8 @@ assert parsed.length == 44 assert parsed.magic_cookie == 0x2112A442 assert parsed.transaction_id == 0xcfacb2a43aa2de5a9d56d85a, parsed.transaction_id assert parsed.attributes == [ - STUNXorMappedAddress(xport=40480, xip="172.20.0.42"), - STUNMessageIntegrity(hmac_sha1=0x7d968d4241fa89d8e3f8ffe302c8975823c91fb7), + STUNXorMappedAddress(length=8, xport=40480, xip="172.20.0.42"), + STUNMessageIntegrity(hmac_sha1=0xb71fc9235897c802e3fff8e3d889fa41428d967d), STUNFingerprint(crc_32=0xea9b6559) ] @@ -99,14 +99,51 @@ assert STUN.stun_message_type.i2repr(None, parsed.stun_message_type) == "Binding assert parsed.length == 88 assert parsed.magic_cookie == 0x2112A442 assert parsed.transaction_id == 0x3479476534635936316a796a, parsed.transaction_id -assert parsed.attributes[0] == STUNXorMappedAddress(xport=25000, xip="172.20.0.200"), parsed.attributes +assert parsed.attributes[0] == STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"), parsed.attributes assert parsed.attributes == [ - STUNXorMappedAddress(xport=25000, xip="172.20.0.200"), + STUNXorMappedAddress(length=8, xport=25000, xip="172.20.0.200"), STUNUsername(length=37, username="Ht11MaRZHc4GOLJUsbu1R3YCs72HYN25:oNph"), - STUNMessageIntegrity(hmac_sha1=0x317065df81598d6cc8ca3bd684ca65fb6d03674b), + 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" \ @@ -136,3 +173,70 @@ assert parsed.transaction_id == 0x1d9357a1e94a2051271996d9, parsed.transaction_i 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 011e78bf706..94f07ad6a42 100644 --- a/test/contrib/tacacs.uts +++ b/test/contrib/tacacs.uts @@ -1,3 +1,8 @@ +# 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 @@ -188,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/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/fields.uts b/test/fields.uts index d7e12e33539..38263f2a49f 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -95,6 +95,19 @@ r = m.addfield(None, b"FOO", "c0:01:be:ef:ba:be") r assert r == b"FOO\xc0\x01\xbe\xef\xba\xbe" += LEMACField class +~ core field +m = LEMACField("foo", None) +r = m.i2m(None, None) +r +assert r == b"\x00\x00\x00\x00\x00\x00" +r = m.getfield(None, b"\xbe\xba\xef\xbe\x01\xc0ABCD") +r +assert r == (b"ABCD","c0:01:be:ef:ba:be") +r = m.addfield(None, b"FOO", "be:ba:ef:be:01:c0") +r +assert r == b"FOO\xc0\x01\xbe\xef\xba\xbe" + = SourceMACField conf.route.add(net="1.2.3.4/32", dev=conf.iface) p = Ether() / ARP(pdst="1.2.3.4") @@ -126,15 +139,16 @@ assert r == b"FOO\x01\x02\x03\x04" = SourceIPField ~ core field defaddr = conf.route.route('0.0.0.0')[1] -class Test(Packet): fields_desc = [SourceIPField("sourceip", None)] +class Test(Packet): fields_desc = [SourceIPField("sourceip")] assert Test().sourceip == defaddr assert Test(raw(Test())).sourceip == defaddr assert IP(dst="0.0.0.0").src == defaddr assert IP(raw(IP(dst="0.0.0.0"))).src == defaddr -assert IP(dst="0.0.0.0/31").src == defaddr -assert IP(raw(IP(dst="0.0.0.0/31"))).src == defaddr +defaddr = conf.route.route('1.1.1.1')[1] +assert IP(dst="1.1.1.1").src == defaddr +assert IP(raw(IP(dst="1.1.1.1"))).src == defaddr #= ByteField @@ -150,7 +164,7 @@ class TestThreeBytesField(Packet): fields_desc = [ X3BytesField('test1', None), ThreeBytesField('test2', None), - LEX3BytesField('test3', None), + XLE3BytesField('test3', None), LEThreeBytesField('test4', None), ] @@ -182,6 +196,21 @@ assert p.test2 == 0xc000ff3333 assert p.test3 == 0xffeeddccbbaa9988776655 assert p.test4 == 309404098707666285700277845 +class TestFuzzNBytesField(Packet): + fields_desc = [ + NBytesField('test1', 0, 128), + ] + +f = fuzz(TestFuzzNBytesField()) +assert f.test1.max == 2 ** (128 * 8) - 1 + +p2 = TestNBytesField(raw(p)) +assert p2.sprintf('%test1% %test2% %test3% %test4%') == '18838586676582 0xc000ff3333 0xffeeddccbbaa9988776655 309404098707666285700277845' +assert p2.test1 == 18838586676582 +assert p2.test2 == 0xc000ff3333 +assert p2.test3 == 0xffeeddccbbaa9988776655 +assert p2.test4 == 309404098707666285700277845 +assert raw(p2) == raw(TestNBytesField(test1=p2.test1, test2=p2.test2, test3=p2.test3, test4=p2.test4)) = StrField ~ field strfield @@ -197,7 +226,7 @@ class TestStrField(Packet): p = TestStrField(s1="cafe", s2="deadbeef") assert raw(p) == b'\x04\x00cafedeadbeef' print(p.sprintf("%s1% %s2%")) -assert p.sprintf("%s1% %s2%") == "'cafe' 'deadbeef'" +assert p.sprintf("%s1% %s2%") == "b'cafe' b'deadbeef'" = StrFieldUtf16 @@ -234,7 +263,6 @@ assert p.sprintf("%s1%") == 'cafe' = Creation of a layer with ActionField ~ field actionfield -from __future__ import print_function class TestAction(Packet): __slots__ = ["_val", "_fld", "_priv1", "_priv2"] @@ -294,29 +322,70 @@ p.len == 4 and p.str == b"ABC" and Raw in p = BitFieldLenField test ~ field class TestBFLenF(Packet): - fields_desc = [ BitFieldLenField("len", None, 4, length_of="str" , adjust=lambda pkt,x:x+1), - BitField("nothing",0xfff, 12), + fields_desc = [ BitFieldLenField("len", None, 4, length_of="str" , adjust=lambda pkt,x:x+1, tot_size=-2), + BitField("nothing",0xfff, 12, end_tot_size=-2), StrLenField("str", "default", length_from=lambda pkt:pkt.len-1, ) ] a=TestBFLenF() r = raw(a) r -assert r == b"\x8f\xffdefault" +assert r == b"\xff\x8fdefault" a.str="" r = raw(a) r -assert r == b"\x1f\xff" +assert r == b"\xff\x1f" -p = TestBFLenF(b"\x1f\xff@@") +p = TestBFLenF(b"\xff\x1f@@") p assert p.len == 1 and p.str == b"" and Raw in p and p[Raw].load == b"@@" -p = TestBFLenF(b"\x6f\xffabcdeFGH") +p = TestBFLenF(b"\xff\x6fabcdeFGH") p assert p.len == 6 and p.str == b"abcde" and Raw in p and p[Raw].load == b"FGH" += Test BitLenField +~ field +SIZES = {0: 6, 1: 6, 2: 14, 3: 22} + +class TestBitLenField(Packet): + fields_desc = [ + BitField("mode", 0, 2), + BitLenField("value", 0, length_from=lambda pkt: SIZES[pkt.mode]) + ] + +p = TestBitLenField(mode=1, value=50) +assert bytes(p) == b"r" + +p = TestBitLenField(mode=2, value=5000) +assert bytes(p) == b'\x93\x88' + +p = TestBitLenField(b'\xc0\x01\xf4') +assert p.mode == 3 +assert p.value == 500 + += Test UTCTimeField +~ field + +class TestUTCTimeField(Packet): + fields_desc = [ + # A Windows time field. See GH#4308 + UTCTimeField( + "Time", + None, + fmt="' True = EnumField with Enum -~ python3_only - -# not available on Python 2... - from enum import Enum class JUICE(Enum): @@ -1141,6 +1295,22 @@ class Breakfast(Packet): 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" ############ ############ @@ -1651,6 +1821,17 @@ 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()) + ######## ######## + ScalingField @@ -1931,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" ############ @@ -2213,3 +2301,47 @@ 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/imports.uts b/test/imports.uts index 635c6aba7ba..ad6ca83598e 100644 --- a/test/imports.uts +++ b/test/imports.uts @@ -1,7 +1,8 @@ % Import tests +~ not_pypy + Import tests -~ python3_only imports +~ imports = Prepare importing all scapy files @@ -11,7 +12,7 @@ import subprocess import re import time import sys -from scapy.consts import WINDOWS +from scapy.consts import WINDOWS, OPENBSD # DEV: to add your file to this list, make sure you have # a GREAT reason. @@ -46,7 +47,7 @@ ALL_FILES = [ x.split(".")[1] not in EXCEPTION_PACKAGES ] -NB_PROC = 1 if WINDOWS else 4 +NB_PROC = 1 if WINDOWS or OPENBSD else 4 def append_processes(processes, filename): processes.append( diff --git a/test/linux.uts b/test/linux.uts index 516eade1ab8..d651fccee3f 100644 --- a/test/linux.uts +++ b/test/linux.uts @@ -11,12 +11,9 @@ = L3RawSocket ~ netaccess IP TCP linux needs_root -old_l3socket = conf.L3socket -conf.L3socket = L3RawSocket with no_debug_dissector(): x = sr1(IP(dst="www.google.com")/TCP(sport=RandShort(), dport=80, flags="S"),timeout=3) -conf.L3socket = old_l3socket x assert x[IP].ottl() in [32, 64, 128, 255] assert 0 <= x[IP].hops() <= 126 @@ -58,16 +55,65 @@ if exit_status == 0: 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) + 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 + += Test scoped interface addresses +~ linux needs_root + +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() + +conf.route6 + +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 missing ~ linux needs_root -from mock import patch +from unittest.mock import patch # can't remove the lo device (or its address without causing trouble) - use some pseudo dummy instead @@ -78,71 +124,71 @@ with patch('scapy.arch.linux.conf.loopback_name', 'scapy_lo_x'): ~ linux needs_root import os, socket -from mock import patch +from unittest.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.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() - -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 - -assert got_lo_device - -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 -IFACES._add_fake_iface("scapy0") +conf.ifaces._add_fake_iface("scapy0", 'e2:39:91:79:19:10') -from mock import patch +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 @@ -275,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): @@ -293,76 +339,75 @@ test_read_routes() ~ linux needs_root from scapy.arch.linux import L3PacketSocket -import scapy.libs.six as six - -if six.PY3: - import mock, 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() + +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 veth += 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, name="linux.uts sniff right0") -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 +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) -veth.destroy() = Reload interfaces & routes diff --git a/test/nmap.uts b/test/nmap.uts index d71c86b136c..abea7137c9a 100644 --- a/test/nmap.uts +++ b/test/nmap.uts @@ -20,7 +20,6 @@ assert len(d) == 5 = Fetch database ~ netaccess -from __future__ import print_function try: from urllib.request import urlopen except ImportError: @@ -91,7 +90,7 @@ assert conf.nmap_kdb.filename == None = Clear temp files try: - os.remove('nmap-os-fingerprints') + os.remove(filename) except: pass diff --git a/test/p0fv2.uts b/test/p0fv2.uts index 594c4e9660e..3933f5ecfd1 100644 --- a/test/p0fv2.uts +++ b/test/p0fv2.uts @@ -13,7 +13,6 @@ load_module('p0fv2') = Fetch database ~ netaccess -from __future__ import print_function try: from urllib.request import urlopen except ImportError: @@ -22,7 +21,8 @@ except ImportError: def _load_database(file): for i in range(10): try: - open(file, 'wb').write(urlopen('https://raw.githubusercontent.com/p0f/p0f/4b4d1f384abebbb9b1b25b8f3c6df5ad7ab365f7/' + file).read()) + with open(file, 'wb') as fd: + fd.write(urlopen('https://raw.githubusercontent.com/p0f/p0f/4b4d1f384abebbb9b1b25b8f3c6df5ad7ab365f7/' + file).read()) break except: raise @@ -119,4 +119,4 @@ def _rem(f): _rem("p0f.fp") _rem("p0fa.fp") _rem("p0fr.fp") -_rem("p0fo.fp") \ No newline at end of file +_rem("p0fo.fp") 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/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_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/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/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/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/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_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 c1673b25f99..f622f385eec 100644 --- a/test/pipetool.uts +++ b/test/pipetool.uts @@ -228,7 +228,7 @@ p.wait_and_stop() = Test SniffSource -import mock +from unittest import mock fd = ObjectPipe("sniffsource") fd.write("test") @@ -295,7 +295,7 @@ else: = Test exhausted AutoSource and SniffSource -import mock +from unittest import mock from scapy.error import Scapy_Exception def _fail(): @@ -323,7 +323,7 @@ except: 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() -import mock +from unittest import mock with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink() sink.start() @@ -345,7 +345,7 @@ 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() -import mock +from unittest import mock with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink(linktype=linktype) sink.start() @@ -363,7 +363,7 @@ 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() -import mock +from unittest import mock with mock.patch("scapy.scapypipes.subprocess.Popen", return_value=Bunch(stdin=q)) as popen: sink = WiresharkSink(args=['-c', '1']) sink.start() @@ -387,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 = [] @@ -720,5 +720,6 @@ s.send(bytes(HTTP()/HTTPRequest(Host="www.google.com"))) result = c.q.get(timeout=10) p.stop() -assert result.startswith(b"HTTP/1.1 200 OK") +result +assert result.startswith(b"HTTP/1.1 200 OK") or result.startswith(b"HTTP/1.1 302 Found") diff --git a/test/random.uts b/test/random.uts index 7a445d2326e..c0cec76c5f0 100644 --- a/test/random.uts +++ b/test/random.uts @@ -7,34 +7,33 @@ = RandomEnumeration ren = RandomEnumeration(0, 7, seed=0x2807, forever=False) -[x for x in ren] == ([3, 4, 2, 5, 1, 6, 0, 7] if six.PY2 else [5, 0, 2, 7, 6, 3, 1, 4]) +[x for x in ren] == [5, 0, 2, 7, 6, 3, 1, 4] = RandIP6 random.seed(0x2807) r6 = RandIP6() -assert(r6 == ("d279:1205:e445:5a9f:db28:efc9:afd7:f594" if six.PY2 else - "240b:238f:b53f:b727:d0f9:bfc4:2007:e265")) +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::e445" if six.PY2 else "2001:0db8::b53f") +assert r6 == "2001:0db8::b53f" assert r6.command() == "RandIP6(ip6template='2001:db8::-')" r6 = RandIP6("2001:db8::*") -assert r6 == ("2001:0db8::efc9" if six.PY2 else "2001:0db8::bfc4") +assert r6 == "2001:0db8::bfc4" assert r6.command() == "RandIP6(ip6template='2001:db8::*')" = RandMAC random.seed(0x2807) rm = RandMAC() -assert rm == ("d2:12:e4:5a:db:ef" if six.PY2 else "24:23:b5:b7:d0:bf") +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:05" if six.PY2 else "00:01:02:03:04:01") +assert rm == "00:01:02:03:04:01" assert rm.command() == "RandMAC(template='00:01:02:03:04:0-7')" @@ -50,7 +49,7 @@ 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.11" if six.PY2 else "1.2.3.12") +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)) @@ -61,7 +60,7 @@ assert rand_obj.command() == "RandOID(fmt='1.2.3.0-28', depth=RandNumExpo(lambd= random.seed(0x2807) rex = RandRegExp("[g-v]* @? [0-9]{3} . (g|v)") -bytes(rex) == ('vmuvr @ 906 \x9e g' if six.PY2 else b'irrtv @ 517 \xc2\xb8 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:]") @@ -84,7 +83,7 @@ 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 == ('c' if six.PY2 else 'a') +assert r == 'a' = RandSingNum random.seed(0x2807) @@ -95,13 +94,13 @@ assert rs.command() == "RandSingNum(mn=-28, mx=7)" = Rand* random.seed(0x2807) rss = RandSingString() -assert rss == ("CON:" if six.PY2 else "foo.exe:") +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=%s'scapy')" % '' if six.PY2 else 'b' +assert rts.command() == "RandTermString(size=4, term=b'scapy')" = RandInt (test __bool__) a = "True" if RandNum(False, True) else "False" @@ -120,7 +119,7 @@ assert rng._fix() == 8 assert rng.command() == "RandNumGauss(mu=1, sigma=42)" renum = RandEnum(1, 42, seed=0x2807) -assert renum == (13 if six.PY2 else 37) +assert renum == 37 assert renum.command() == "RandEnum(min=1, max=42, seed=10247)" rp = RandPool((IncrementalValue(), 42), (IncrementalValue(), 0)) @@ -134,97 +133,3 @@ 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)" - -= CyclicPattern charset 0 - -cs0 = b'AAAaAA0AABAAbAA1AACAAcAA2AADAAdAA3AAEAAeAA4AAFAAfAA5AAGAAgAA6AAHAAhAA7AAIAAiAA8AAJAAjAA9AAKAAkAALAAlAAMAAmAANAAnAAOAAoAAPAApAAQAAqAARAArAASAAsAATAAtAAUAAuAAVAAvAAWAAwAAXAAxAAYAAyAAZAAzAaaAa0AaBAabAa1AaCAacAa2AaDAadAa3AaEAaeAa4AaFAafAa5AaGAagAa6AaHAahAa7AaIAaiAa8AaJ' - -p = Raw(load=CyclicPattern()) -b = bytes(p) -if len(b): - if len(b) > len(cs0): - assert cs0 in b - else: - assert b in cs0 or b == cs0 - -p = Raw(load=CyclicPattern(5)) -b = bytes(p) -assert len(b) == 5 -assert b == b'AAAaA' - -p = Raw(load=CyclicPattern(2, 3)) -b = bytes(p) -print(b) -assert len(b) == 2 -assert b == b'aA' - -= CyclicPattern charset 1 - -cs1 = b'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyAAzA%%A%sA%BA%$A%nA%CA%-A%(A%DA%;A%)A%EA%aA%0A%FA%bA%1A%GA%' - -p = Raw(load=CyclicPattern(None, 0, 1)) -b = bytes(p) -if len(b): - if len(b) > len(cs1): - assert cs1 in b - else: - assert b in cs1 or b == cs1 - -p = Raw(load=CyclicPattern(10, 0, 1)) -b = bytes(p) -assert len(b) == 10 -assert b == b'AAA%AAsAAB' - -p = Raw(load=CyclicPattern(2, 8, 1)) -b = bytes(p) -print(b) -assert len(b) == 2 -assert b == b'AB' - -= CyclicPattern charset 2 - -cs2 = b'AAAaAA0AA!AABAAbAA1AA"AACAAcAA2AA#AADAAdAA3AA$AAEAAeAA4AA%AAFAAfAA5AA&AAGAAgAA6AA\'AAHAAhAA7AA(AAIAAiAA8AA)AAJAAjAA9AA*AAKAAkAA+AALAAlAA,AAMAAmAA-AANAAnAA.AAOAAoAA/AAPAApAA:AAQAAqAA;AARAArAAAAUAAuAA?AAVAAvAA@AAWAAwAA[AAXAAxAA\\AAYAAyAA]AAZAAzAA^AA_AA`AA{AA|AA}AA~AaaAa0Aa!AaBAabAa1Aa"Aa' - -p = Raw(load=CyclicPattern(None, 0, 2)) -b = bytes(p) -if len(b): - if len(b) > len(cs2): - assert cs2 in b - else: - assert b in cs2 or b == cs2 - -p = Raw(load=CyclicPattern(10, 0, 2)) -b = bytes(p) -assert len(b) == 10 -assert b == b'AAAaAA0AA!' - -p = Raw(load=CyclicPattern(2, 8, 2)) -b = bytes(p) -print(b) -assert len(b) == 2 -assert b == b'A!' - -= CyclicPattern command - -p = Raw(load=CyclicPattern(2, 8, 2)) -cmd = p.command() - -assert "charset_type=2" in cmd -assert "start=8" in cmd -assert "size=2" in cmd - -p = Raw(load=CyclicPattern(2)) -cmd = p.command() - -assert "charset_type" not in cmd -assert "start" not in cmd -assert "size=2" in cmd - -p = Raw(load=CyclicPattern()) -cmd = p.command() - -assert "charset_type" not in cmd -assert "start" not in cmd -assert "size" not in cmd - - diff --git a/test/regression.uts b/test/regression.uts index e4a05e0124c..97bba309ca8 100644 --- a/test/regression.uts +++ b/test/regression.uts @@ -46,16 +46,26 @@ assert _version_checker(FakeModule3, (2, 4, 2)) = Check Scapy version -import mock +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) as popen: - class GitModuleScapy(object): - __version__ = _version_from_git_describe() +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.dev261' assert _version_checker(GitModuleScapy, (2, 4, 5)) @@ -100,7 +110,24 @@ def test_list_contrib(): 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") @@ -184,6 +211,33 @@ 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) @@ -215,7 +269,7 @@ except: assert not conf.use_bpf = Configuration conf.use_pcap -~ linux +~ linux libpcap if not conf.use_pcap: assert not conf.iface.provider.libpcap @@ -234,6 +288,9 @@ 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)) @@ -253,17 +310,18 @@ assert p == "127.0.0.1" = Interface related functions -import mock +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""" - IFACES._add_fake_iface("dummy0") + conf.ifaces._add_fake_iface("dummy0") return "dummy0" get_if_raw_addr(get_dummy_interface()) @@ -274,16 +332,6 @@ get_working_if() get_if_raw_addr6(conf.iface) -if conf.use_bpf: - addr = u"lladdr 29:0b:c2:ff:fe:53:21:e9\n" - b = Bunch(returncode=0, communicate=lambda *args, **kargs: (addr, None)) - with mock.patch('scapy.arch.bpf.core.subprocess.Popen', return_value=b) as popen: - try: - scapy.arch.bpf.core.get_if_raw_hwaddr("fw0") - assert False - except Scapy_Exception: - assert True - = More Interfaces related functions # Test name resolution @@ -294,12 +342,12 @@ assert conf.iface == old assert isinstance(conf.iface, NetworkInterface) assert conf.iface.is_valid() -import mock +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() == conf.loopback_name + assert get_working_if() is None assert conf.iface + "a" # left + assert "hey! are you, ready to go ? %s" % conf.iface # format @@ -335,6 +383,95 @@ 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() @@ -386,14 +523,17 @@ pkt.build() = Test read_routes6() - check mandatory routes +import re +ll_route = re.compile(r"fe80:\d{0,2}:") +# match fe80::, fe80:5:, etc. (if scoped) + conf.route6 -# Doesn't pass on Travis Bionic XXX 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 not OPENBSD and len(iflist) >= 2: - assert sum(1 for r in routes6 if r[0] == "fe80::" and r[1] == 64) >= 1 + if len(iflist) >= 2: + assert sum(1 for r in routes6 if ll_route.match(r[0]) and r[1] == 64) >= 1 try: # 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 @@ -416,21 +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 data = b"\xffsweet\xef celestia\xab" -if not six.PY2: - # Only Python 3 has to deal with Str/Bytes conversion, - # as we don't use Python 2's unicode - if sys.version_info[0:2] <= (3, 4): - # Python3.4 can only ignore unknown special characters - assert plain_str(data) == "sweet celestia" - else: - # Python >3.4 can replace them with a backslash representation - assert plain_str(data) == "\\xffsweet\\xef celestia\\xab" -else: - assert plain_str(data) == "\xffsweet\xef celestia\xab" +assert plain_str(data) == "\\xffsweet\\xef celestia\\xab" ############ ############ @@ -443,16 +595,6 @@ 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 return -invalid_pcapngfile_2 = BytesIO(b'\n\r\r\n\x00\x00\x00\x10\x1a+ raise EOFError @@ -2230,6 +2643,19 @@ 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 @@ -2257,7 +2683,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" @@ -2288,8 +2714,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() @@ -2314,7 +2740,7 @@ f.close() del f, pkt = Check tcpdump() command with linktype and args -~ tcpdump +~ tcpdump libpcap f = BytesIO() pkt = Ether()/IP()/ICMP() @@ -2465,7 +2891,7 @@ os.remove(filename) = Check wrpcap() with different packets types -import mock +from unittest import mock import os import tempfile @@ -2475,6 +2901,15 @@ with mock.patch("scapy.utils.warning") as warning: os.remove(filename) assert any("Inconsistent" in arg for arg in warning.call_args[0]) += Check wrpcap() with the Loopback layer +~ tshark + +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 + ############ ############ + ERF Ethernet format support @@ -2525,298 +2960,371 @@ assert pkterf[1][Ether].src == "00:0f:53:3f:ca:c0" ############ ############ -+ Mocked read_routes() calls - -= Truncated netstat -rn output on OS X -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.os") -def test_osx_netstat_truncated(mock_os, mock_get_if_addr): - """Test read_routes() on OS X 10.? with a long interface name""" - # netstat & ifconfig outputs from https://github.com/secdev/scapy/pull/119 - netstat_output = u""" -Routing tables - -Internet: -Destination Gateway Flags Refs Use Netif Expire -default 192.168.1.1 UGSc 460 0 en1 -default link#11 UCSI 1 0 bridge1 -127 127.0.0.1 UCS 1 0 lo0 -127.0.0.1 127.0.0.1 UH 10 2012351 lo0 -""" - ifconfig_output = u"lo0 en1 bridge10\n" - # Mocked file descriptors - def se_popen(command): - """Perform specific side effects""" - if command.startswith("netstat -rn"): - return StringIO(netstat_output) - elif command == "ifconfig -l": - ret = StringIO(ifconfig_output) - def unit(): - return ret - ret.__call__ = unit - ret.__enter__ = unit - ret.__exit__ = lambda x,y,z: None - return ret - raise Exception("Command not mocked: %s" % command) - mock_os.popen.side_effect = se_popen - # Mocked get_if_addr() behavior - def se_get_if_addr(iface): - """Perform specific side effects""" - if iface == "bridge1": - return "0.0.0.0" - return "1.2.3.4" - mock_get_if_addr.side_effect = se_get_if_addr - # Test the function - from scapy.arch.unix import read_routes - scapy.arch.unix.DARWIN = True - scapy.arch.unix.FREEBSD = False - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - routes = read_routes() - assert len(routes) == 4 - assert [r for r in routes if r[3] == "bridge10"] - - -test_osx_netstat_truncated() - - -= macOS 10.13 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.os") -def test_osx_10_13_ipv4(mock_os, mock_get_if_addr): - """Test read_routes() on OS X 10.13""" - # 'netstat -rn -f inet' output - netstat_output = u""" -Routing tables - -Internet: -Destination Gateway Flags Refs Use Netif Expire -default 192.168.28.1 UGSc 82 0 en0 -127 127.0.0.1 UCS 0 0 lo0 -127.0.0.1 127.0.0.1 UH 1 878 lo0 -169.254 link#5 UCS 0 0 en0 -192.168.28 link#5 UCS 4 0 en0 -192.168.28.1/32 link#5 UCS 2 0 en0 -192.168.28.1 88:32:9c:f5:4e:ea UHLWIir 40 37 en0 1177 -192.168.28.2 62:aa:56:4b:51:54 UHLWI 0 0 en0 619 -192.168.28.4 38:17:ed:9a:58:28 UHLWIi 1 6 en0 428 -192.168.28.18/32 link#5 UCS 1 0 en0 -192.168.28.18 88:32:9c:f5:4e:eb UHLWI 0 1 lo0 -192.168.28.28 04:0e:eb:11:74:a7 UHLWI 0 0 en0 576 -224.0.0/4 link#5 UmCS 1 0 en0 -224.0.0.251 1:0:5e:0:0:fb UHmLWI 0 0 en0 -255.255.255.255/32 link#5 UCS 0 0 en0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked get_if_addr() output - def se_get_if_addr(iface): - """Perform specific side effects""" - import socket - if iface == "en0": - return "192.168.28.18" - return "127.0.0.1" - mock_get_if_addr.side_effect = se_get_if_addr - # Test the function - from scapy.arch.unix import read_routes - scapy.arch.unix.DARWIN = False - scapy.arch.unix.FREEBSD = True - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - routes = read_routes() - for r in routes: - print(r) - assert len(routes) == 15 - default_route = [r for r in routes if r[0] == 0][0] - assert default_route[3] == "en0" and default_route[4] == "192.168.28.18" - -test_osx_10_13_ipv4() - - -= macOS 10.15 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.os") -def test_osx_10_15_ipv4(mock_os, mock_get_if_addr): - """Test read_routes() on OS X 10.15""" - # 'netstat -rn -f inet' output - netstat_output = u""" -Routing tables - -Internet: -Destination Gateway Flags Netif Expire -default 192.168.122.1 UGSc en0 -127 127.0.0.1 UCS lo0 -127.0.0.1 127.0.0.1 UH lo0 -169.254 link#8 UCS en0 ! -192.168.122 link#8 UCS en0 ! -192.168.122.1/32 link#8 UCS en0 ! -192.168.122.1 52:54:0:c0:b7:af UHLWIir en0 1169 -192.168.122.63/32 link#8 UCS en0 ! -224.0.0/4 link#8 UmCS en0 ! -224.0.0.251 1:0:5e:0:0:fb UHmLWI en0 -255.255.255.255/32 link#8 UCS en0 ! -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked get_if_addr() output - def se_get_if_addr(iface): - """Perform specific side effects""" - import socket - if iface == "en0": - return "192.168.122.42" - return "127.0.0.1" - mock_get_if_addr.side_effect = se_get_if_addr - # Test the function - from scapy.arch.unix import read_routes - scapy.arch.unix.DARWIN = False - scapy.arch.unix.FREEBSD = True - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - routes = read_routes() - for r in routes: - print(r) - assert len(routes) == 11 - default_route = [r for r in routes if r[0] == 0][0] - assert default_route[3] == "en0" and default_route[4] == "192.168.122.42" - -test_osx_10_15_ipv4() - - -= OpenBSD 6.3 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.OPENBSD") -@mock.patch("scapy.arch.unix.os") -def test_openbsd_6_3(mock_os, mock_openbsd, mock_get_if_addr): - """Test read_routes() on OpenBSD 6.3""" - # 'netstat -rn -f inet' output - netstat_output = u""" -Routing tables - -Internet: -Destination Gateway Flags Refs Use Mtu Prio Iface -default 10.0.1.254 UGS 0 0 - 8 bge0 -224/4 127.0.0.1 URS 0 23 32768 8 lo0 -10.0.1/24 10.0.1.26 UCn 4 192 - 4 bge0 -10.0.1.1 00:30:48:57:ed:0b UHLc 2 338 - 3 bge0 -10.0.1.2 00:03:ba:0c:0b:52 UHLc 1 186 - 3 bge0 -10.0.1.26 00:30:48:62:b3:f4 UHLl 0 47877 - 1 bge0 -10.0.1.135 link#1 UHLch 1 194 - 3 bge0 -10.0.1.254 link#1 UHLch 1 190 - 3 bge0 -10.0.1.255 10.0.1.26 UHb 0 0 - 1 bge0 -10.188.6/24 10.188.6.17 Cn 0 0 - 4 tap3 -10.188.6.17 fe:e1:ba:d7:ff:32 UHLl 0 25 - 1 tap3 -10.188.6.255 10.188.6.17 Hb 0 0 - 1 tap3 -10.188.135/24 10.0.1.135 UGS 0 0 1350 L 8 bge0 -127/8 127.0.0.1 UGRS 0 0 32768 8 lo0 -127.0.0.1 127.0.0.1 UHhl 1 3835230 32768 1 lo0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - - # Mocked OpenBSD parsing behavior - mock_openbsd = True - - # Mocked get_if_addr() output - def se_get_if_addr(iface): - """Perform specific side effects""" - import socket - if iface == "bge0": - return "192.168.122.42" - return "10.0.1.26" - mock_get_if_addr.side_effect = se_get_if_addr - - # Test the function - from scapy.arch.unix import read_routes - return read_routes() - -routes = test_openbsd_6_3() - -for r in routes: - print(ltoa(r[0]), ltoa(r[1]), r) - # check that default route exists in parsed data structure - if ltoa(r[0]) == "0.0.0.0": - default = r - # check that route with locked mtu exists in parsed data structure - if ltoa(r[0]) == "10.188.135.0": - locked = r - -assert len(routes) == 11 -assert default[2] == "10.0.1.254" -assert default[3] == "bge0" -assert locked[2] == "10.0.1.135" -assert locked[3] == "bge0" - -= Solaris 11.1 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -# Mocked Solaris 11.1 parsing behavior - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.SOLARIS", True) -@mock.patch("scapy.arch.unix.os") -def test_solaris_111(mock_os, mock_get_if_addr): - """Test read_routes() on Solaris 11.1""" - # 'netstat -rvn -f inet' output - netstat_output = u""" -IRE Table: IPv4 - Destination Mask Gateway Device MTU Ref Flg Out In/Fwd --------------------- --------------- -------------------- ------ ----- --- --- ----- ------ -default 0.0.0.0 10.0.2.2 net0 1500 2 UG 5 0 -10.0.2.0 255.255.255.0 10.0.2.15 net0 1500 3 U 0 0 -127.0.0.1 255.255.255.255 127.0.0.1 lo0 8232 2 UH 1517 1517 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - print(scapy.arch.unix.SOLARIS) - - # Mocked get_if_addr() output - def se_get_if_addr(iface): - """Perform specific side effects""" - import socket - if iface == "net0": - return "10.0.2.15" - return "127.0.0.1" - mock_get_if_addr.side_effect = se_get_if_addr - - # Test the function - from scapy.arch.unix import read_routes - return read_routes() ++ Mocked read_routes() and read_routes6() calls + += Create patcher util +~ mock_read_routes_bsd little_endian_only + +# mock the random to get consistency +from unittest import mock + +from scapy.pton_ntop import inet_pton +import scapy.arch.bpf.pfroute + +og_afinet6 = socket.AF_INET6 +og_inet_pton = socket.inet_pton +def mock_inet_pton(af, data): + if af in [24, 28, 30]: + return og_inet_pton(og_afinet6, data) + return og_inet_pton(af, data) + + +og_inet_ntop = socket.inet_ntop +def mock_inet_ntop(af, data): + if af in [24, 28, 30]: + return og_inet_ntop(og_afinet6, data) + return og_inet_ntop(af, data) + + +class BSDLoader: + def __init__(self, OPENBSD=False, FREEBSD=False, NETBSD=False, DARWIN=False, sysctldata=None, ifaces={}, AF_INET6=socket.AF_INET6, IS_64BITS=True): + self.sysctldata = sysctldata + self.ifaces = ifaces + socket.AF_LINK = 18 + self.loadpatches = [ + mock.patch('socket.AF_INET6', AF_INET6), + mock.patch('socket.inet_pton', side_effect=mock_inet_pton), + mock.patch('socket.inet_ntop', side_effect=mock_inet_ntop), + mock.patch('scapy.consts.OPENBSD', OPENBSD), + # mock.patch('scapy.consts.FREEBSD', FREEBSD), + mock.patch('scapy.consts.NETBSD', NETBSD), + mock.patch('scapy.consts.DARWIN', DARWIN), + mock.patch('scapy.consts.IS_64BITS', IS_64BITS), + ] + def __enter__(self): + # Apply patches that only occur when loading + for p in self.loadpatches: + p.start() + # Reload module + pfroute = importlib.reload(scapy.arch.bpf.pfroute) + # Now apply post-load patches + self.patches = [ + mock.patch.object( + pfroute, + '_sr1_bsdsysctl', + return_value=pfroute.pfmsghdrs(self.sysctldata) + ), + mock.patch.object( + pfroute, + '_get_if_list', + return_value=self.ifaces, + ), + ] + for p in self.patches: + p.start() + return pfroute + def __exit__(self, *args, **kwargs): + for p in self.loadpatches: + p.stop() + for p in self.patches: + p.stop() + + += OpenBSD 7.5 amd64 - read_routes() +~ mock_read_routes_bsd little_endian_only + +import zlib + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789c7bc1c0ca92c0c0c8c0c0c0c160cec2c0e0ccc10006de0f1850003f0376c08a431c06049830f96bc40f30e2925710626460636163c828cb3360108d6548e5c4aabf0bc6576248c9482ec8494d2c4e4dc1e78e03607f323380fd09243d71f853109be6067c2623dcf5008d5fcfc080e2cf0f48f20a42cc0c1240e7e4e41be0340f593fbafbbd71b81f2b20d2fdf504dcff9f2aee6704bbdf151873d8dc8f961ce0ee67c4264ec0bd18ee0702cadc0fe2b280ddefc8d880d5fdb80031ee07a66b747e1732ffff7f440a22359f5c80bb9f1912fe2ccc58ddcf03a5cd675f494316c71a2f98f6c1bd09761f1002dd26f4b900bb7ad4f820d73fd0f4c4a280d53f5c04dc4dc03f70fb88711f25fe3980ee1f0607acfe61a7c83fe7ffa3f2d1d317f9ee1f05a360148c8251300a46c128180543030000bd836967')) + + +with BSDLoader(OPENBSD=True, sysctldata=_PFROUTE_DATA, AF_INET6=24) as pfroute: + routes = pfroute.read_routes() + + +assert routes == [ + (0, 0, '172.23.192.1', 'hvn0', '172.23.192.138', 1), + (3758096384, 4026531840, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706432, 4278190080, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706433, 4294967295, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2887237632, 4294963200, '172.23.192.138', 'hvn0', '172.23.192.138', 1), + (2887237633, 4294967295, '0.0.0.0', 'hvn0', '172.23.192.138', 1), + (2887237770, 4294967295, '0.0.0.0', 'hvn0', '172.23.192.138', 1), + (2887241727, 4294967295, '172.23.192.138', 'hvn0', '172.23.192.138', 1) +] + += OpenBSD 7.6 GENERIC#531 i386 - read_routes() +~ mock_read_routes_bsd little_endian_only + +import zlib +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789c7bc2c0ca92c0c0c8c0c0c0c160cec2c0e0ccc10006fa760c28c08089012b60c52e0c07024c98fc35120f1871c92b083132b031b331a4a41a3088c632a47262316f8dc4ab1330be12434a4672414e6a62716a0a2e371c00fb919901ec4720e989c38f584103612520373d40e3d73330a0f8f10392bc8210338304d03939f90638cd43d68fee7e6f1ab8bf9e80fbff53c5fd8c60f7bb02630d9bfbb126b1062483f0bb9f111fff3f1050e67e109705ec7e47c606aceec70588713f304fa0f111691ce27e440a22358f5c80bb9f1912fe2ccc58ddbf1c4a478aec4a4716c791f5d1dd0ff726d87d4008749cd0e702ecea51e3835cff40d3138b0256ff781370377eff20ec23c67d94f8e700ba7f181cb0fa673a45fe79ff1f958f9ebec877ff281805a360148c8251300a46c128183a0000223e6527')) + +with BSDLoader(OPENBSD=True, sysctldata=_PFROUTE_DATA, AF_INET6=24, IS_64BITS=False) as pfroute: + routes = pfroute.read_routes() + +assert routes == [ + (0, 0, '172.24.224.1', 'de0', '172.24.234.200', 1), + (3758096384, 4026531840, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706432, 4278190080, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706433, 4294967295, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2887311360, 4294963200, '172.24.234.200', 'de0', '172.24.234.200', 1), + (2887311361, 4294967295, '0.0.0.0', 'de0', '172.24.234.200', 1), + (2887314120, 4294967295, '0.0.0.0', 'de0', '172.24.234.200', 1), + (2887315455, 4294967295, '172.24.234.200', 'de0', '172.24.234.200', 1) +] + + += OpenBSD 7.5 amd64 - read_routes6() +~ mock_read_routes_bsd little_endian_only + +import zlib + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789ced96bb0dc2301086cf38481125a248411131011d3505a2600906406204325a4661040a6a4264f130d6c5b19c87e2f07f458adcd9be2f7fe190984647924414d3a67c1e6252dcf7544f56dfb24cbceac2ac171a7a633a979494e39fce6baffde9e32f94ff8e56eab5e9bfe076c98866ecf6eee7fbf8ebdfa03dffbef2ffcd6f38f977eb9f4eecf5aaf9347fb6311cff8bd77ce3f1bf7acdf7f5bfb18de1f8f3f9fd4bfe8f8a5e67ff9c5f1f8c7f6eaf57cdd79ffffbfe4fd5eb0ef297dcf9aef5a6f77fd5fe7de55f087bdd80f1e7d7b7977fa4fcb7af7bdaf467c7cff899b8f34b7f69abbbe4cfad0f26ffc6ff3ffcfa60f29f0c347f00000000000000000000306a9e0a72ae83')) + + +with BSDLoader(OPENBSD=True, sysctldata=_PFROUTE_DATA, AF_INET6=24) as pfroute: + routes = pfroute.read_routes6() + + +assert routes == [ + ('::', 96, '::1', 'lo0', ['::1'], 1), + ('::1', 128, '::1', 'lo0', ['::1'], 1), + ('::ffff:0.0.0.0', 96, '::1', 'lo0', ['::1'], 1), + ('2002::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:7f00::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:e000::', 20, '::1', 'lo0', ['::1'], 1), + ('2002:ff00::', 24, '::1', 'lo0', ['::1'], 1), + ('fe80::', 10, '::1', 'lo0', ['::1'], 1), + ('fec0::', 10, '::1', 'lo0', ['::1'], 1), + ('fe80:3::1', 128, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1), + ('ff01::', 16, '::1', 'lo0', ['::1'], 1), + ('ff01:3::', 32, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1), + ('ff02::', 16, '::1', 'lo0', ['::1'], 1), + ('ff02:3::', 32, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1) +] + += OpenBSD 7.6 GENERIC#531 i386 - read_routes6() +~ mock_read_routes_bsd little_endian_only + +import zlib + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789ced96310e824010450717136269456141ac2cedac2dacaced3d808947905b781d8ee21138814836226477083b0ae89aff0a9a1966e7e5872c394dc32329228a68533ef71169ae87803a49bb5b16b1b816346b4583aa21992b8acb954fe7b5786efef20db4ef8e96ba68faaeb80929d1ac4dc6e16ca96fe5dc8fef58f9d6397d37df617d93896caf86afd5e087ef45b497ffbe37d15eb56f6e35f8e16be7f4cff9de995e27dfcc6ef0c23793ed35bc6f75ff26ba3840beca3cdba5f6c9fdcbcd1d2bdf8219e7f6fdda0dfde41b6adfedf39e347d59fb949dcb9e5dfaaab65a57bee67b5ee4fbf6ff86dde045be93dfc81700000000000000000000009f7900a834b765')) + + +with BSDLoader(OPENBSD=True, sysctldata=_PFROUTE_DATA, AF_INET6=24, IS_64BITS=False) as pfroute: + routes = pfroute.read_routes6() + +assert routes == [ + ('::', 96, '::1', 'lo0', ['::1'], 1), + ('::1', 128, '::1', 'lo0', ['::1'], 1), + ('::ffff:0.0.0.0', 96, '::1', 'lo0', ['::1'], 1), + ('2002::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:7f00::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:e000::', 20, '::1', 'lo0', ['::1'], 1), + ('2002:ff00::', 24, '::1', 'lo0', ['::1'], 1), + ('fe80::', 10, '::1', 'lo0', ['::1'], 1), + ('fec0::', 10, '::1', 'lo0', ['::1'], 1), + ('fe80:3::1', 128, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1), + ('ff01::', 16, '::1', 'lo0', ['::1'], 1), + ('ff01:3::', 32, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1), + ('ff02::', 16, '::1', 'lo0', ['::1'], 1), + ('ff02:3::', 32, 'fe80:3::1', 'lo0', ['fe80:3::1'], 1) +] + += FreeBSD 14.1 amd64 - read_routes() +~ mock_read_routes_bsd little_endian_only + +import zlib + +from scapy.arch.bpf.pfroute import _bsd_iff_flags +_FREEBSD_IFACES = {1: {'name': 'lo0', 'index': 1, 'flags': FlagValue(32841, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 28, 'index': 1, 'address': '::1', 'scope': 16}, {'af_family': 28, 'index': 1, 'address': 'fe80::1', 'scope': 32}, {'af_family': 2, 'index': 1, 'address': '127.0.0.1'}]}, 2: {'name': 'hn0', 'index': 2, 'flags': FlagValue(34883, _bsd_iff_flags), 'mac': '00:15:5d:00:65:07', 'ips': [{'af_family': 28, 'index': 2, 'address': 'fe80::215:5dff:fe00:6507', 'scope': 32}, {'af_family': 2, 'index': 2, 'address': '172.23.198.182'}]}} + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789c136064656162606060e660603067200ceeb012a18808c008a55970c80b3061f2d7881f60c4256f21c4c4c0c6ccc6909167c0201acb90ca4ea43b20e61edb8670182b0bc8125606010663620c7020d2220280118d46072077d623490b0831324820c95b80f8cc0c0c39f90624d98b612e343d3002fd3f10e98109873c34fe117c507ca3c9ffffff01cea77a7ae01898f4c08c431edd9de8e141adf40000e8611aa8')) + + +with BSDLoader(FREEBSD=True, sysctldata=_PFROUTE_DATA, ifaces=_FREEBSD_IFACES, AF_INET6=28) as pfroute: + routes = pfroute.read_routes() + +assert routes == [ + (0, 0, '172.23.192.1', 'hn0', '172.23.198.182', 1), + (2130706433, 4294967295, '0.0.0.0', 'lo0', '127.0.0.1', 1), + (2887237632, 4294963200, '0.0.0.0', 'hn0', '172.23.198.182', 1), + (2887239350, 4294967295, '0.0.0.0', 'lo0', '127.0.0.1', 1), + (3758096384, 4026531840, '0.0.0.0', 'lo0', '127.0.0.1', 250), + (3758096384, 4026531840, '0.0.0.0', 'hn0', '172.23.198.182', 250) +] + += FreeBSD 14.1 amd64 - read_routes6() +~ mock_read_routes_bsd little_endian_only + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789ce5553b0e8240109d593e62b72121b1a0e00824165a72042e40696261f40a1ecd837816101236c28461872801e36bb678f3797979bb9ba3e722006c0380030890498aecc0f6f4193e8ec7fb191e295f75d02d3c86083b07e0724b457aa57b93d64f2fd0b0970ccc26ad6781e4a4b0e9d68d1f1d622e7ff29fc95b3f2f6bcddbdafd2cefe33c33f6ede763b87f2e3fb3d64f04bd889f0ec3737e9a3e7a7f691ee9bc4ffd233ad0e858fafd530c6fd3fdedf78fdbd3e44b813c5f4f6fd27a16663f378ecb97f15387aa77d7edf9aaeb1d1fced714a2024e1ba14eaa43454555d6ed46c7d2f97219dea69bfaf7afff41c55c50f9ff3adc3f979f2f44725d78')) + + +with BSDLoader(FREEBSD=True, sysctldata=_PFROUTE_DATA, ifaces=_FREEBSD_IFACES, AF_INET6=28) as pfroute: + routes = pfroute.read_routes6() + +assert routes == [ + ('::', 96, '::1', 'lo0', ['::1'], 1), + ('::1', 128, '::', 'lo0', ['::1'], 1), + ('::ffff:0.0.0.0', 96, '::1', 'lo0', ['::1'], 1), + ('fe80::', 10, '::1', 'lo0', ['::1'], 1), + ('fe80::', 64, '::', 'lo0', ['fe80::1'], 1), + ('fe80::1', 128, '::', 'lo0', ['fe80::1'], 1), + ('fe80::', 64, '::', 'hn0', ['fe80::215:5dff:fe00:6507'], 1), + ('fe80::215:5dff:fe00:6507', 128, '::', 'lo0', ['::1'], 1), + ('ff02::', 16, '::1', 'lo0', ['::1'], 1), + ('ff00::', 8, '::', 'lo0', ['::1', 'fe80::1'], 250), + ('ff00::', 8, '::', 'hn0', ['fe80::215:5dff:fe00:6507'], 250) +] + += NetBSD 10.0 amd64 - read_routes() +~ mock_read_routes_bsd little_endian_only + +import zlib + +from scapy.arch.bpf.pfroute import _bsd_iff_flags +_NETBSD_IFACES = {1: {'name': 'hvn0', 'index': 1, 'flags': FlagValue(34883, _bsd_iff_flags), 'mac': '00:15:5d:00:65:0a', 'ips': [{'af_family': 24, 'index': 1, 'address': 'fe80:1::7184:2b50:9fbe:e337', 'scope': 32}, {'af_family': 2, 'index': 1, 'address': '172.23.207.191'}]}, 2: {'name': 'lo0', 'index': 2, 'flags': FlagValue(32841, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 2, 'index': 2, 'address': '127.0.0.1'}, {'af_family': 24, 'index': 2, 'address': '::1', 'scope': 16}, {'af_family': 24, 'index': 2, 'address': 'fe80:2::1', 'scope': 32}]}} + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789c3bc1c0c2c2c8c0c0c00cc4e60ca8c08a91816640800993bf46fc00868d42428c0c6c2c6c0c196579060ca2b10ca95cc8eacfef87a93b00f407c8486e0e4c7f600311cd94b81ed5ddf5987cb83f58ff0301c85d424c0c12c040cec937c0aa6e07d4fdac0c2c0cc6f4773fdc1de8ee24e4ee0bd0f4c3c88819ee2c2cd471232e7703d30b9c0f4e2758d4b183c2ffff0792d311b1f1402d80ee0e5cfec1161fc8fa6640e383550592a769058cef5d4943e6a3e75f3eb0fb813e108d15fa5c404387e000001e173214')) + + +with BSDLoader(NETBSD=True, sysctldata=_PFROUTE_DATA, ifaces=_NETBSD_IFACES, AF_INET6=24) as pfroute: + routes = pfroute.read_routes() + +assert routes == [ + (0, 0, '172.23.192.1', 'hvn0', '172.23.207.191', 1), + (2130706432, 4294967040, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706433, 4294967295, '0.0.0.0', 'lo0', '127.0.0.1', 1), + (2887237632, 4294967295, '0.0.0.0', 'hvn0', '172.23.207.191', 1), + (2887241663, 4294967295, '0.0.0.0', 'lo0', '172.23.207.191', 1), + (2887237633, 4294967295, '0.0.0.0', 'hvn0', '', 1), + (3758096384, 4026531840, '0.0.0.0', 'hvn0', '172.23.207.191', 250), + (3758096384, 4026531840, '0.0.0.0', 'lo0', '127.0.0.1', 250) +] + += NetBSD 10.0 amd64 - read_routes6() +~ mock_read_routes_bsd little_endian_only + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789ced97b14ec3301445af4da8aa964a5544a50e1dd8592a31f01b8c2c1d2b31205017d65682ffe86f30213e8331123fd09109d3368a8127fbd9898c2c87de29ce4dac77749f1d0722cb24807e17b8845bd78f1e0f7968326ee48bea62a40cdadeefe712e323e0f67eea350f12e53f35e3d7e67f43c97f8c0c171e75ff31bfae8b72a49cebd2e1a3e57d5d387c38f837489b5f397cb43a7fa5787fafe0fbda07e2f29f89c133e713e9ba4f52e796bc4ff4bddfffc64e907bc9fa442de22e589fc8c0bd29c7c9712bd6276a4dde9f2bde27d275f72aead772dc847b3710c28f3b947e700b939fe7021dc3fd21f986ed9fcb3ab879b89b6234c3bc679e7ff1747eb57e79d78845cdf37928b9eab271db72b5cd53f5f3ce8c94abf18b65f1750fd07c196ee3fb75ffbb42c95597ef7f97edfdd8eb54897aeb949eb79aae53ddc79edca1f7e52d37dbc74441cf9b51f396ff346f1927ef830efa028ffb7c47')) + + +with BSDLoader(NETBSD=True, sysctldata=_PFROUTE_DATA, ifaces=_NETBSD_IFACES, AF_INET6=24) as pfroute: + routes = pfroute.read_routes6() + +assert routes == [ + ('::', 104, '::1', 'lo0', ['::1'], 1), + ('::', 96, '::1', 'lo0', ['::1'], 1), + ('::1', 128, '::', 'lo0', ['::1'], 1), + ('::127.0.0.0', 104, '::1', 'lo0', ['::1'], 1), + ('::224.0.0.0', 100, '::1', 'lo0', ['::1'], 1), + ('::255.0.0.0', 104, '::1', 'lo0', ['::1'], 1), + ('::ffff:0.0.0.0', 96, '::1', 'lo0', ['::1'], 1), + ('2001:db8::', 32, '::1', 'lo0', ['::1'], 1), + ('2002::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:7f00::', 24, '::1', 'lo0', ['::1'], 1), + ('2002:e000::', 20, '::1', 'lo0', ['::1'], 1), + ('2002:ff00::', 24, '::1', 'lo0', ['::1'], 1), + ('fe80::', 10, '::1', 'lo0', ['::1'], 1), + ('fe80:1::', 64, '::', 'hvn0', ['fe80:1::7184:2b50:9fbe:e337'], 1), + ('fe80:1::7184:2b50:9fbe:e337', + 128, + '::', + 'lo0', + ['fe80:1::7184:2b50:9fbe:e337'], + 1), + ('fe80:2::', 64, 'fe80:2::1', 'lo0', ['fe80:2::1'], 1), + ('fe80:2::1', 128, '::', 'lo0', ['fe80:2::1'], 1), + ('ff01:1::', 32, '::', 'hvn0', ['fe80:1::7184:2b50:9fbe:e337'], 1), + ('ff01:2::', 32, '::1', 'lo0', ['::1'], 1), + ('ff02:1::', 32, '::', 'hvn0', ['fe80:1::7184:2b50:9fbe:e337'], 1), + ('ff02:2::', 32, '::1', 'lo0', ['::1'], 1), + ('ff00::', 8, '::', 'hvn0', ['fe80:1::7184:2b50:9fbe:e337'], 250), + ('ff00::', 8, '::', 'lo0', ['::1', 'fe80:2::1'], 250) +] + += Darwin 23.6 (MacOS 14.5) x86_64 - read_routes() +~ mock_read_routes_bsd little_endian_only + +import zlib + +from scapy.arch.bpf.pfroute import _bsd_iff_flags +_DARWIN_IFACES = {1: {'name': 'lo0', 'index': 1, 'flags': FlagValue(32841, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 2, 'index': 1, 'address': '127.0.0.1'}, {'af_family': 30, 'index': 1, 'address': '::1', 'scope': 16}, {'af_family': 30, 'index': 1, 'address': 'fe80:1::1', 'scope': 32}]}, 2: {'name': 'gif0', 'index': 2, 'flags': FlagValue(32784, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': []}, 3: {'name': 'stf0', 'index': 3, 'flags': FlagValue(0, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': []}, 4: {'name': 'XHC2', 'index': 4, 'flags': FlagValue(0, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': []}, 5: {'name': 'en0', 'index': 5, 'flags': FlagValue(34915, _bsd_iff_flags), 'mac': '52:54:00:09:49:17', 'ips': [{'af_family': 30, 'index': 5, 'address': 'fe80:5::409:eec9:f06c:50ab', 'scope': 32}, {'af_family': 30, 'index': 5, 'address': 'fec0::89e:daf7:5cb1:f1f0', 'scope': 64}, {'af_family': 30, 'index': 5, 'address': 'fec0::c0c4:1f0b:61ba:ea8', 'scope': 64}, {'af_family': 2, 'index': 5, 'address': '10.0.2.15'}]}, 6: {'name': 'utun0', 'index': 6, 'flags': FlagValue(32849, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 30, 'index': 6, 'address': 'fe80:6::d36e:82de:94dc:84fc', 'scope': 32}]}, 7: {'name': 'utun1', 'index': 7, 'flags': FlagValue(32849, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 30, 'index': 7, 'address': 'fe80:7::7ce2:1f7b:2c29:a5ee', 'scope': 32}]}, 8: {'name': 'utun2', 'index': 8, 'flags': FlagValue(32849, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 30, 'index': 8, 'address': 'fe80:8::e4e0:bef:bf56:2605', 'scope': 32}]}, 9: {'name': 'utun3', 'index': 9, 'flags': FlagValue(32849, _bsd_iff_flags), 'mac': '00:00:00:00:00:00', 'ips': [{'af_family': 30, 'index': 9, 'address': 'fe80:9::ce81:b1c:bd2c:69e', 'scope': 32}]}} -routes = test_solaris_111() -print(routes) -assert len(routes) == 3 -assert routes[0][:4] == (0, 0, '10.0.2.2', 'net0') -assert routes[1][:4] == (167772672, 4294967040, '0.0.0.0', 'net0') -assert routes[2][:4] == (2130706433, 4294967295, '0.0.0.0', 'lo0') +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789ccdd94d6813411400e0d9ddcc36e6608d4472a950a8e0d183e8a1a7da5a503008012948b11eaa78f0e6498b180a45c1802d08164f7ab3f4208817b1e84a0ff647ea0fd2832815a5e021018f518931dbce6c5e76df6cb2e36c3a0325bce936fbf5bdd96176669ad00c2584584963e060fd7382e0ed3315fc22a4ed3183718a98260df675f3b8c83c5dc41ceeab7f1acca6ca6366922f451ebf6596598c5d84b8b9f1fd3b01cb8bc58f17a35852e01b337b29b17dd774d5b65a4b97a1dee5c1305772db55f3bba6998b26cc7d6eedf2cc2872ed5ffe5f974df2671abd211eea7a4ce0d9403c59816703e963f7b2508f857b3a5037ef5e72751b34fa581fcf9305fe9ebb4a029785f4b17bd59a5d3661431bb5fbe700b76e2ae780eef2d4619f4f3807c43d1fa5b3e753da587ad7e7b4b11c583aad8d65fe50561bcbc29de7fa589e7c93b5a87ea6d30bcf6eca5a12c082cd77a2269aefd295d280ac45798d2aa541598b052c70edd3ca82ad93986548d612435e8ecb5a605e145986652deaf3f23ba19185c2b83d8b7d8caf61c2c6eedbf7f81a463876ab3d7fa25b62ca4bf5c81b8d2c6b1a59dee96339b9aa8f25b72c6b513ed755732bd12dc1671abe3b71cb9ae099c6deb3b62d97af47b7c455a3196dde03b2fd4e8f4696c72a2cd8781135d178a95bd655586093cfcbab823616e7fb2f590b7c0f505223a72cfd1e10836556d6a2be46e5872a2c2af27234f76053850536d9bc8c54c61fe96219bdf6e9be2e96b1f1a913ba582e5d397bbb5dcbddbac5bd3fdf631536a873d84f1b961bc1d81be6946d69fafb8bcc44492fe1f38cc7680ac24d4dd70a0cade2b035d529f0bdbc56b73ee06b2af7daa7be7abaf72ae6af5a30dec93da17b17266c184739eb1135d9bdf9b9bf8d18db9bb7d97e78a773e46c7e1983f14e3ee7af7fcc27dbb534ea5588e52ce52b88b17ab9cffa4fc4c573441393e02ca520744569cce5ed43ec6667290639b7d5dbe931dd38c18976def40fb15043e2')) +with BSDLoader(DARWIN=True, sysctldata=_PFROUTE_DATA, ifaces=_DARWIN_IFACES, AF_INET6=30) as pfroute: + routes = pfroute.read_routes() + +assert routes == [ + (0, 0, '10.0.2.2', 'en0', '10.0.2.15', 1), + (167772672, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (167772674, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (167772674, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (167772675, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (167772687, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (167772927, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (2130706432, 4294967040, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2130706433, 4294967295, '127.0.0.1', 'lo0', '127.0.0.1', 1), + (2851995648, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (3758096384, 4043308800, '0.0.0.0', 'en0', '10.0.2.15', 1), + (3758096635, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1), + (4294967295, 4294967295, '0.0.0.0', 'en0', '10.0.2.15', 1) +] + += Darwin 23.6 (MacOS 14.5) x86_64 - read_routes6() +~ mock_read_routes_bsd little_endian_only + +_PFROUTE_DATA = zlib.decompress(bytes.fromhex('789cd5dd5f8c13451800f0d96dbbdb5e8fbbdaebfd114eaf08148fdc830124045f0a1581981062f48c8698c32022313c1925c2c381f8a0a28290887f728644e0d0aa60ce80e60897887fe08120f060cc051030218af74713088a60b73b5bda6e77f73a33fbcd7dfbc27667d2fefaddb7dfcc6cb7a58f84122142488028e9e9b97f8f90cadb60c8a1c1656bbddbbbed6637297e6635e4d01e8c0c1d1b797ed9a7c67e5fceac99e6f9d35d5edf47b3567c5c73683fbd76d3d91d839b6f58667d0ce695fe99f5e2e3ba43fb860b6deb3bda770f59e6f018cc277597463e73b8f878d8a1fdd2f9e8f091ce54c83247c660be1cf0cd1c293e1e71683fb131da7ab843eb31f6f7e7cc4aeedf503049a6b801d2c2cc0a4fdb7e5a3374a2cdb7bc46fd28ef6c9d7f43a7ceacaad69b5416772d56c94c7a784e715ba59ae1562faaf5fec9ed55d6417a39e23b9b1ec6125fea858d2f87770e32ef5c19de1106efd4e06a920e8669894f0620bd2cf14d91ad64f2dbf308894df8f9a9f4f65d38bcab9079d722f36e42e67d1f99770099f72832ef6554ded4d3130999747c70188db70399772632ef1a64deadc8bcfdc8bcc79179cf48f18eb278833db96585d26e8a6a4aae31f8edbdc4e2d5ee25eaac7f546dfd475757e8e159905e96f57c4a5b5438b63a9eb880cbdb08eabdc2ea8d516f22f0072e6f10d4cb9c0f96b729b810973704ea1de6f64eedc2e59d06ea651a8f4bbcb33b7179ef17e455171a5ec5c35bcd56d93bef075cde07047961c68b6ca6a1458c1726bed94ce345315e98f1229b69fe1dd2cb5b1fb299d41a482fef7891cdcc3826c6eb73fe26cdfdd513b5cd62bcfe7dde52ead541bdccf95bf086f7e1f24640bdcce75bc15bb71797b71ed4cb7bbe65338bbe87f4f2c6379b79380de9e53ddf72de51482ff3fc61b2b9ffe633eb6a9079a390deeb2c5efdef652430ed3962ce226a21bd4ce75ba977022e6f12597c93a0f13df5138337a92c21d763f4110922f14e43e64d21f35ab7c0a2f0aae180e5ba0b99f76e245eeb56752cf1b5ee2c9f24c66baee7831ede6ab6626fcd68e1700c85f7aa68af8f9f1f27952bd17385c30b20bdd718bc334862595a0fd36748aa905e96f90ef5d2af582441c70b96f531f5d6522fe8fd041cf1ad93e165591f9779c3905e8ef8d22bc0b0e71b4b3da3de06195e8ef85a5e0dd2cb91bf2dd45b87c43b917ab1d433cb0b9abf2cd7a3cabca0d7cf583e8f2df382e6ef632c5ee560ba462d4c8041f3e1257e2fe87c6711bf17743c6e64f56a9657d929c6ebdfe7b1796fb8105fd0f1ed04c3fa38ef6db29e5201bd7f5280f72f48efbfacf9db34629140c76301de00a49769be53eabd538cd7e7fad06279c9f85fbf957a41f3e11cb357cefc813dbe72e60fccf537791f3d2aaafefa7cbea5909d6fb7bd3aa497793e999233dfb9c9ea9d5fd76d8a60f3418017b49eb5f27b41c70b01f105adbf02bc2164def17fbd2fe76de832eb426e038daf002fb6f8828e6f02bca0d74b047823c8bca0d753057841ef4714e005bd5f4e8017f47e39015ed0ebebcff27beb21bd02e20b7a7f8900ef1d905ea6fb4bd4d45c73964e9f1ed0cbf47961a917b49e317d7ea1a646b46e53bcfce53def427a99ee8729f57e8bccfb1ba497e97e8d9c771bad0b5db1eba0eb0b015ed0f585002fe8fa428017747d21c00bbabe6862f72e34f69b17c37ae3fcde6648af80f80ababf1a2cbe6d90de667e6f1299773232ef3dc8bc539079a7427a1f62f686bb2909f4f7f038bcafcbf03eceed6d7e02d2cb11df2d9484251fb6c9f072e4c376f3119a7cf89a9240e39b64f6d6bf623e8a7f07e97d92dd4bebc383a0df2fe4f06e91e1e5c887f7cc47b0f9c03e5f6fd8489f612912ef6b32bcacd727735e29f3878becde3764787f65f7be23c3cb910fdb917977caf0b2cf771a3e301fc1ce7738ceb70f2909cbf9d623c3cb91bfbb647839f2618f0c2fc7f946bdb0e71b47feeea5242cf9db2bc3cb11df7d32bc1cf3c94fe83380ce27c9695e6f3be8fd041cde2c32ef67c8bc9fcbf0728c6f5f5012687de0882ff5a2c9870332bc1ce3c5979484251ffacc4768f2e12b64de8332bc1cf5ec102561c95fea858def7fccf16da4d74b26fc88c4db23c3cb743fad9aba463e26247b7e45a8bc6d9c7bf5f2b671eead41e6ad45e6ad47e68d61f12a4d86f72d34f940bd75d0de192cdee0d205e6afbc1a9bfe22a497e9f7b90cef1aeb68f055486f1bb7577f81c73b90f31a5f3c5188a24c27a4f02514db667b0763f7e65ed7f6b40e6df9fdd8add2cdad6f2ff5878249650a8c3fbf9f882ba4a58afe87685e28b9401b71561d5e93e7772bcafef6c47486cc2ff8166d2ef1b5e5472f7587826aa311dfe3f43df8e8566fbb35f248272904cbcb599c078e5b9adf59fcba05e7a324b2a4d9bbbf71be197f0feb7cf3290fcaffe4b6b6d36b379ddd31b8f986b1ef920fb6be4071b6bd6e22aed992ceadbf11676332ed15e7957c71d6bdda365c685bdfd1be7bc8d87789b3ad2f509c6daf9b88eb6e71b6f537e26c7c01c52bce276d91aaca19f66abb743e3a7ca43395ff6bbac4d9d61728ceb6d74dc4c36e71b6f537e26c7c11c52bce97035cce8857db898dd1d6c31d5afe5a804b9c6d7d81e26c7bdd443ce216675bffa2719af8569f07ec6d02c7e9427c15fac68bdf8397bbd2fb7570f38ed3c4b73ca0ce70cf2fd7961f181d2971561aa72bf487740e1c6d8baef8a6ae77accee2fefdd6fc5de956acff7045b4f3964b5bd996cfb88895b01efdfa0ae79abb9de75cab64af74ae553257cadf7e6bfe066c769beb38d86dfdfaad3991879d674ee461b7cd1f1cecb67efdd63cc3c3ce33cff0b0dbc66407bbad5fbf35767bd879c66e0fbb6d9c73b0dbfa81d4970aeb49b7ba515b614cacd40fa4be28635b7357324bab2f4a75eb4307bb9cfaa254b7e672b0cba92f4a75eb1807bb9cfaa254b73670b0cba92f2ae2faa222ac2f2ae2faa222ae2f2ae2faa2fa535ffe079dfe8806')) + + +with BSDLoader(DARWIN=True, sysctldata=_PFROUTE_DATA, ifaces=_DARWIN_IFACES, AF_INET6=30) as pfroute: + routes = pfroute.read_routes6() + +assert routes == [ + ('::', 0, 'fe80:5::2', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('::', 0, 'fe80:6::', 'utun0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('::', 0, 'fe80:7::', 'utun1', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('::', 0, 'fe80:8::', 'utun2', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('::', 0, 'fe80:9::', 'utun3', ['fe80:9::ce81:b1c:bd2c:69e'], 1), + ('::1', 128, '::1', 'lo0', ['::1'], 1), + ('fe80:1::', 64, 'fe80:1::1', 'lo0', ['fe80:1::1'], 1), + ('fe80:1::1', 128, '::', 'lo0', ['fe80:1::1'], 1), + ('fe80:5::', 64, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('fe80:5::2', 128, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('fe80:5::409:eec9:f06c:50ab', 128, '::', 'lo0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('fe80:6::', 64, 'fe80:6::d36e:82de:94dc:84fc', 'utun0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('fe80:6::d36e:82de:94dc:84fc', 128, '::', 'lo0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('fe80:7::', 64, 'fe80:7::7ce2:1f7b:2c29:a5ee', 'utun1', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('fe80:7::7ce2:1f7b:2c29:a5ee', 128, '::', 'lo0', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('fe80:8::', 64, 'fe80:8::e4e0:bef:bf56:2605', 'utun2', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('fe80:8::e4e0:bef:bf56:2605', 128, '::', 'lo0', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('fe80:9::', 64, 'fe80:9::ce81:b1c:bd2c:69e', 'utun3', ['fe80:9::ce81:b1c:bd2c:69e'], 1), + ('fe80:9::ce81:b1c:bd2c:69e', 128, '::', 'lo0', ['fe80:9::ce81:b1c:bd2c:69e'], 1), + ('fec0::', 64, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('fec0::2', 128, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('fec0::89e:daf7:5cb1:f1f0', 128, '::', 'lo0', ['fec0::89e:daf7:5cb1:f1f0'], 1), + ('fec0::c0c4:1f0b:61ba:ea8', 128, '::', 'lo0', ['fec0::c0c4:1f0b:61ba:ea8'], 1), + ('ff00::', 8, '::1', 'lo0', ['::1'], 1), + ('ff00::', 8, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('ff00::', 8, 'fe80:6::d36e:82de:94dc:84fc', 'utun0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('ff00::', 8, 'fe80:7::7ce2:1f7b:2c29:a5ee', 'utun1', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('ff00::', 8, 'fe80:8::e4e0:bef:bf56:2605', 'utun2', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('ff00::', 8, 'fe80:9::ce81:b1c:bd2c:69e', 'utun3', ['fe80:9::ce81:b1c:bd2c:69e'], 1), + ('ff01:1::', 32, '::1', 'lo0', ['::1'], 1), + ('ff01:5::', 32, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('ff01:6::', 32, 'fe80:6::d36e:82de:94dc:84fc', 'utun0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('ff01:7::', 32, 'fe80:7::7ce2:1f7b:2c29:a5ee', 'utun1', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('ff01:8::', 32, 'fe80:8::e4e0:bef:bf56:2605', 'utun2', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('ff01:9::', 32, 'fe80:9::ce81:b1c:bd2c:69e', 'utun3', ['fe80:9::ce81:b1c:bd2c:69e'], 1), + ('ff02:1::', 32, '::1', 'lo0', ['::1'], 1), + ('ff02:5::', 32, '::', 'en0', ['fe80:5::409:eec9:f06c:50ab'], 1), + ('ff02:6::', 32, 'fe80:6::d36e:82de:94dc:84fc', 'utun0', ['fe80:6::d36e:82de:94dc:84fc'], 1), + ('ff02:7::', 32, 'fe80:7::7ce2:1f7b:2c29:a5ee', 'utun1', ['fe80:7::7ce2:1f7b:2c29:a5ee'], 1), + ('ff02:8::', 32, 'fe80:8::e4e0:bef:bf56:2605', 'utun2', ['fe80:8::e4e0:bef:bf56:2605'], 1), + ('ff02:9::', 32, 'fe80:9::ce81:b1c:bd2c:69e', 'utun3', ['fe80:9::ce81:b1c:bd2c:69e'], 1) +] + ############ ############ + Mocked _parse_tcpreplay_result(stdout, stderr, argv, results_dict) @@ -2897,390 +3405,6 @@ expected = { assert results_dict == expected -############ -############ -+ Mocked read_routes6() calls - -= Preliminary definitions -~ mock_read_routes_bsd - -import mock -from io import StringIO - -def valid_output_read_routes6(routes): - """"Return True if 'routes' contains correctly formatted entries, False otherwise""" - for destination, plen, next_hop, dev, cset, me in routes: - if not in6_isvalid(destination) or not type(plen) == int: - return False - if not in6_isvalid(next_hop) or not isinstance(dev, six.string_types): - return False - for address in cset: - if not in6_isvalid(address): - return False - return True - -def check_mandatory_ipv6_routes(routes6): - """Ensure that mandatory IPv6 routes are present""" - if sum(1 for r in routes6 if r[0] == "::1" and r[4] == ["::1"]) < 1: - return False - if sum(1 for r in routes6 if r[0] == "fe80::" and r[1] == 64) < 1: - return False - if sum(1 for r in routes6 if in6_islladdr(r[0]) and r[1] == 128 and \ - r[4] == ["::1"]) < 1: - return False - return True - - -= Mac OS X 10.9.5 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_osx_10_9_5(mock_os, mock_in6_getifaddr): - """Test read_routes6() on OS X 10.9.5""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Netif Expire -::1 ::1 UHL lo0 -fe80::%lo0/64 fe80::1%lo0 UcI lo0 -fe80::1%lo0 link#1 UHLI lo0 -fe80::%en0/64 link#4 UCI en0 -fe80::ba26:6cff:fe5f:4eee%en0 b8:26:6c:5f:4e:ee UHLWIi en0 -fe80::bae8:56ff:fe45:8ce6%en0 b8:e8:56:45:8c:e6 UHLI lo0 -ff01::%lo0/32 ::1 UmCI lo0 -ff01::%en0/32 link#4 UmCI en0 -ff02::%lo0/32 ::1 UmCI lo0 -ff02::%en0/32 link#4 UmCI en0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0"), - ("fe80::ba26:6cff:fe5f:4eee", IPV6_ADDR_LINKLOCAL, "en0")] - # Test the function - from scapy.arch.unix import read_routes6 - scapy.arch.unix.DARWIN = False - scapy.arch.unix.FREEBSD = True - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - routes = read_routes6() - for r in routes: - print(r) - assert len(routes) == 6 - assert check_mandatory_ipv6_routes(routes) - -test_osx_10_9_5() - - -= Mac OS X 10.9.5 with global IPv6 connectivity -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_osx_10_9_5_global(mock_os, mock_in6_getifaddr): - """Test read_routes6() on OS X 10.9.5 with an IPv6 connectivity""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Netif Expire -default fe80::ba26:8aff:fe5f:4eef%en0 UGc en0 -::1 ::1 UHL lo0 -2a01:ab09:7d:1f01::/64 link#4 UC en0 -2a01:ab09:7d:1f01:420:205c:9fab:5be7 b8:e9:55:44:7c:e5 UHL lo0 -2a01:ab09:7d:1f01:ba26:8aff:fe5f:4eef b8:26:8a:5f:4e:ef UHLWI en0 -2a01:ab09:7d:1f01:bae9:55ff:fe44:7ce5 b8:e9:55:44:7c:e5 UHL lo0 -fe80::%lo0/64 fe80::1%lo0 UcI lo0 -fe80::1%lo0 link#1 UHLI lo0 -fe80::%en0/64 link#4 UCI en0 -fe80::5664:d9ff:fe79:4e00%en0 54:64:d9:79:4e:0 UHLWI en0 -fe80::6ead:f8ff:fe74:945a%en0 6c:ad:f8:74:94:5a UHLWI en0 -fe80::a2f3:c1ff:fec4:5b50%en0 a0:f3:c1:c4:5b:50 UHLWI en0 -fe80::ba26:8aff:fe5f:4eef%en0 b8:26:8a:5f:4e:ef UHLWIir en0 -fe80::bae9:55ff:fe44:7ce5%en0 b8:e9:55:44:7c:e5 UHLI lo0 -ff01::%lo0/32 ::1 UmCI lo0 -ff01::%en0/32 link#4 UmCI en0 -ff02::%lo0/32 ::1 UmCI lo -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0"), - ("fe80::ba26:6cff:fe5f:4eee", IPV6_ADDR_LINKLOCAL, "en0")] - # Test the function - from scapy.arch.unix import read_routes6 - routes = read_routes6() - print(routes) - assert valid_output_read_routes6(routes) - for r in routes: - print(r) - assert len(routes) == 11 - assert check_mandatory_ipv6_routes(routes) - -test_osx_10_9_5_global() - - -= Mac OS X 10.10.4 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_osx_10_10_4(mock_os, mock_in6_getifaddr): - """Test read_routes6() on OS X 10.10.4""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Netif Expire -::1 ::1 UHL lo0 -fe80::%lo0/64 fe80::1%lo0 UcI lo0 -fe80::1%lo0 link#1 UHLI lo0 -fe80::%en0/64 link#4 UCI en0 -fe80::a00:27ff:fe9b:c965%en0 8:0:27:9b:c9:65 UHLI lo0 -ff01::%lo0/32 ::1 UmCI lo0 -ff01::%en0/32 link#4 UmCI en0 -ff02::%lo0/32 ::1 UmCI lo0 -ff02::%en0/32 link#4 UmCI en0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0"), - ("fe80::a00:27ff:fe9b:c965", IPV6_ADDR_LINKLOCAL, "en0")] - # Test the function - from scapy.arch.unix import read_routes6 - routes = read_routes6() - for r in routes: - print(r) - assert len(routes) == 5 - assert check_mandatory_ipv6_routes(routes) - -test_osx_10_10_4() - - -= FreeBSD 10.2 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_freebsd_10_2(mock_os, mock_in6_getifaddr): - """Test read_routes6() on FreeBSD 10.2""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Netif Expire -::/96 ::1 UGRS lo0 -::1 link#2 UH lo0 -::ffff:0.0.0.0/96 ::1 UGRS lo0 -fe80::/10 ::1 UGRS lo0 -fe80::%lo0/64 link#2 U lo0 -fe80::1%lo0 link#2 UHS lo0 -ff01::%lo0/32 ::1 U lo0 -ff02::/16 ::1 UGRS lo0 -ff02::%lo0/32 ::1 U lo0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0")] - # Test the function - from scapy.arch.unix import read_routes6 - routes = read_routes6() - scapy.arch.unix.DARWIN = False - scapy.arch.unix.FREEBSD = True - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - for r in routes: - print(r) - assert len(routes) == 3 - assert check_mandatory_ipv6_routes(routes) - -test_freebsd_10_2() - - -= FreeBSD 13.0 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.get_if_addr") -@mock.patch("scapy.arch.unix.os") -def test_freebsd_13(mock_os, mock_get_if_addr): - """Test read_routes() on FreeBSD 13""" - # 'netstat -rnW -f inet' output - netstat_output = u""" -Routing tables - -Internet: -Destination Gateway Flags Nhop# Mtu Netif Expire -default 10.0.0.1 UGS 3 1500 vtnet0 -10.0.0.0/24 link#1 U 2 1500 vtnet0 -10.0.0.8 link#2 UHS 1 16384 lo0 -127.0.0.1 link#2 UH 1 16384 lo0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked get_if_addr() behavior - def se_get_if_addr(iface): - """Perform specific side effects""" - if iface == "vtnet0": - return "10.0.0.1" - return "1.2.3.4" - mock_get_if_addr.side_effect = se_get_if_addr - # Test the function - from scapy.arch.unix import read_routes - routes = read_routes() - scapy.arch.unix.DARWIN = False - scapy.arch.unix.FREEBSD = True - scapy.arch.unix.NETBSD = False - scapy.arch.unix.OPENBSD = False - for r in routes: - print(r) - assert r[3] in ["vtnet0", "lo0"] - assert len(routes) == 4 - -test_freebsd_13() - - -= OpenBSD 5.5 -~ mock_read_routes_bsd - -import mock -from io import StringIO - -@mock.patch("scapy.arch.unix.OPENBSD") -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_openbsd_5_5(mock_os, mock_in6_getifaddr, mock_openbsd): - """Test read_routes6() on OpenBSD 5.5""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Refs Use Mtu Prio Iface -::/104 ::1 UGRS 0 0 - 8 lo0 -::/96 ::1 UGRS 0 0 - 8 lo0 -::1 ::1 UH 14 0 33144 4 lo0 -::127.0.0.0/104 ::1 UGRS 0 0 - 8 lo0 -::224.0.0.0/100 ::1 UGRS 0 0 - 8 lo0 -::255.0.0.0/104 ::1 UGRS 0 0 - 8 lo0 -::ffff:0.0.0.0/96 ::1 UGRS 0 0 - 8 lo0 -2002::/24 ::1 UGRS 0 0 - 8 lo0 -2002:7f00::/24 ::1 UGRS 0 0 - 8 lo0 -2002:e000::/20 ::1 UGRS 0 0 - 8 lo0 -2002:ff00::/24 ::1 UGRS 0 0 - 8 lo0 -fe80::/10 ::1 UGRS 0 0 - 8 lo0 -fe80::%em0/64 link#1 UC 0 0 - 4 em0 -fe80::a00:27ff:fe04:59bf%em0 08:00:27:04:59:bf UHL 0 0 - 4 lo0 -fe80::%lo0/64 fe80::1%lo0 U 0 0 - 4 lo0 -fe80::1%lo0 link#3 UHL 0 0 - 4 lo0 -fec0::/10 ::1 UGRS 0 0 - 8 lo0 -ff01::/16 ::1 UGRS 0 0 - 8 lo0 -ff01::%em0/32 link#1 UC 0 0 - 4 em0 -ff01::%lo0/32 fe80::1%lo0 UC 0 0 - 4 lo0 -ff02::/16 ::1 UGRS 0 0 - 8 lo0 -ff02::%em0/32 link#1 UC 0 0 - 4 em0 -ff02::%lo0/32 fe80::1%lo0 UC 0 0 - 4 lo0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0"), - ("fe80::a00:27ff:fe04:59bf", IPV6_ADDR_LINKLOCAL, "em0")] - # Mocked OpenBSD parsing behavior - mock_openbsd = True - # Test the function - from scapy.arch.unix import read_routes6 - routes = read_routes6() - for r in routes: - print(r) - assert len(routes) == 5 - assert check_mandatory_ipv6_routes(routes) - -test_openbsd_5_5() - - -= NetBSD 7.0 -~ mock_read_routes_bsd - -@mock.patch("scapy.arch.unix.NETBSD") -@mock.patch("scapy.arch.unix.in6_getifaddr") -@mock.patch("scapy.arch.unix.os") -def test_netbsd_7_0(mock_os, mock_in6_getifaddr, mock_netbsd): - """Test read_routes6() on NetBSD 7.0""" - # 'netstat -rn -f inet6' output - netstat_output = u""" -Routing tables - -Internet6: -Destination Gateway Flags Refs Use Mtu Interface -::/104 ::1 UGRS - - - lo0 -::/96 ::1 UGRS - - - lo0 -::1 ::1 UH - - 33648 lo0 -::127.0.0.0/104 ::1 UGRS - - - lo0 -::224.0.0.0/100 ::1 UGRS - - - lo0 -::255.0.0.0/104 ::1 UGRS - - - lo0 -::ffff:0.0.0.0/96 ::1 UGRS - - - lo0 -2001:db8::/32 ::1 UGRS - - - lo0 -2002::/24 ::1 UGRS - - - lo0 -2002:7f00::/24 ::1 UGRS - - - lo0 -2002:e000::/20 ::1 UGRS - - - lo0 -2002:ff00::/24 ::1 UGRS - - - lo0 -fe80::/10 ::1 UGRS - - - lo0 -fe80::%wm0/64 link#1 UC - - - wm0 -fe80::acd1:3989:180e:fde0 08:00:27:a1:64:d8 UHL - - - lo0 -fe80::%lo0/64 fe80::1 U - - - lo0 -fe80::1 link#2 UHL - - - lo0 -ff01:1::/32 link#1 UC - - - wm0 -ff01:2::/32 ::1 UC - - - lo0 -ff02::%wm0/32 link#1 UC - - - wm0 -ff02::%lo0/32 ::1 UC - - - lo0 -""" - # Mocked file descriptor - strio = StringIO(netstat_output) - mock_os.popen = mock.MagicMock(return_value=strio) - # Mocked in6_getifaddr() output - mock_in6_getifaddr.return_value = [("::1", IPV6_ADDR_LOOPBACK, "lo0"), - ("fe80::acd1:3989:180e:fde0", IPV6_ADDR_LINKLOCAL, "wm0")] - # Test the function - from scapy.arch.unix import read_routes6 - routes = read_routes6() - for r in routes: - print(r) - assert len(routes) == 5 - assert check_mandatory_ipv6_routes(routes) - -test_netbsd_7_0() - - ############ ############ + Mocked route() calls @@ -3289,10 +3413,9 @@ test_netbsd_7_0() import scapy -IFACES._add_fake_iface("enp3s0") -IFACES._add_fake_iface("lo") +conf.ifaces._add_fake_iface("enp3s0") +conf.ifaces._add_fake_iface("lo") -old_routes = conf.route.routes old_iface = conf.iface old_loopback = conf.loopback_name try: @@ -3309,27 +3432,25 @@ try: (3232235775, 4294967295, '0.0.0.0', 'enp3s0', '2.2.2.2', 281), (3232235639, 4294967295, '0.0.0.0', 'enp3s0', '3.3.3.3', 281), (3232235520, 4294967040, '0.0.0.0', 'enp3s0', '4.4.4.4', 281), - (0, 0, '192.168.0.254', 'enp3s0', '192.168.0.119', 25) + (0, 0, '192.168.0.254', 'enp3s0', '0.0.0.0', 25) ] assert conf.route.route("192.168.0.0-10") == ('enp3s0', '4.4.4.4', '0.0.0.0') assert conf.route.route("192.168.0.119") == ('lo', '192.168.0.119', '0.0.0.0') assert conf.route.route("224.0.0.0") == ('enp3s0', '1.1.1.1', '0.0.0.0') assert conf.route.route("255.255.255.255") == ('enp3s0', '192.168.0.119', '0.0.0.0') - assert conf.route.route("*") == ('enp3s0', '192.168.0.119', '192.168.0.254') + assert conf.route.route("*") == ('enp3s0', '4.4.4.4', '192.168.0.254') finally: conf.loopback_name = old_loopback conf.iface = old_iface - conf.route.routes = old_routes - conf.route.invalidate_cache() - IFACES.reload() + conf.route.resync() + conf.ifaces.reload() = Mocked IPv6 routes calls -IFACES._add_fake_iface("enp3s0") -IFACES._add_fake_iface("lo") +conf.ifaces._add_fake_iface("enp3s0") +conf.ifaces._add_fake_iface("lo") -old_routes = conf.route6.routes old_iface = conf.iface old_loopback = conf.loopback_name try: @@ -3358,16 +3479,7 @@ finally: conf.loopback_name = old_loopback conf.iface = old_iface conf.route6.resync() - -= Find a link-local address when conf.iface does not support IPv6 - -old_iface = conf.iface -conf.route6.ipv6_ifaces = set(['eth1', 'lo']) -conf.iface = "eth0" -conf.route6.routes = [("fe80::", 64, "::", "eth1", ["fe80::a00:28ff:fe07:1980"], 256), ("::1", 128, "::", "lo", ["::1"], 0), ("fe80::a00:28ff:fe07:1980", 128, "::", "lo", ["::1"], 0)] -assert conf.route6.route("fe80::2807") == ("eth1", "fe80::a00:28ff:fe07:1980", "::") -conf.iface = old_iface -conf.route6.resync() + conf.ifaces.reload() = Windows: reset routes properly @@ -3388,17 +3500,11 @@ import socket sck = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sck.connect(("8.8.8.8", 53)) -class DNSTCP(Packet): - name = "DNS over TCP" - fields_desc = [ FieldLenField("len", None, fmt="!H", length_of="dns"), - PacketLenField("dns", 0, DNS, length_from=lambda p: p.len)] - -ssck = StreamSocket(sck) -ssck.basecls = DNSTCP +ssck = StreamSocket(sck, DNSTCP) -r = ssck.sr1(DNSTCP(dns=DNS(rd=1, qd=DNSQR(qname="www.example.com"))), timeout=3) +r = ssck.sr1(DNSTCP(rd=1, qd=DNSQR(qname="www.example.com")), timeout=3) sck.close() -assert DNSTCP in r and len(r.dns.an) +assert DNSTCP in r and len(r.an) ############ + Tests of SSLStreamContext @@ -3412,7 +3518,7 @@ class MockSocket(object): self.l = [ b'\x00\x00\x00\x01', b'\x00\x00\x00\x02', b'\x00\x00\x00\x03' ] def recv(self, x): if len(self.l) == 0: - raise socket.error(100, 'EOF') + return b"" return self.l.pop(0) def fileno(self): return -1 @@ -3440,7 +3546,7 @@ assert p.data == 3 try: ss.recv() ret = False -except socket.error: +except EOFError: ret = True assert ret @@ -3454,7 +3560,7 @@ class MockSocket(object): self.l = [ b'\x00\x00\x00\x01\x00\x00\x00\x02', b'\x00\x00\x00\x03\x00\x00\x00\x04' ] def recv(self, x): if len(self.l) == 0: - raise socket.error(100, 'EOF') + return b"" return self.l.pop(0) def fileno(self): return -1 @@ -3484,7 +3590,7 @@ assert p.data == 4 try: ss.recv() ret = False -except socket.error: +except EOFError: ret = True assert ret @@ -3498,7 +3604,7 @@ class MockSocket(object): self.l = [ b'\x00\x00', b'\x00\x01', b'\x00\x00\x00', b'\x02', b'\x00\x00', b'\x00', b'\x03' ] def recv(self, x): if len(self.l) == 0: - raise socket.error(100, 'EOF') + return b"" return self.l.pop(0) def fileno(self): return -1 @@ -3517,40 +3623,22 @@ class TestPacket(Packet): s = MockSocket() ss = SSLStreamSocket(s, basecls=TestPacket) -try: - p = ss.recv() - ret = False -except: - ret = True - -assert ret p = ss.recv() assert p.data == 1 -try: - p = ss.recv() - ret = False -except: - ret = True -assert ret p = ss.recv() assert p.data == 2 -try: - p = ss.recv() - ret = False -except: - ret = True -assert ret +p = ss.recv() +assert p.data == 3 + try: - p = ss.recv() + ss.recv() ret = False -except: +except EOFError: ret = True assert ret -p = ss.recv() -assert p.data == 3 ############ @@ -3639,6 +3727,10 @@ n1 = ip.dst assert isinstance(n1, Net) ip.show() += Net using implicit format in IP + +assert len(list(IP(dst=("192.168.0.100", "192.168.0.199")))) == 100 + = Multiple IP addresses test ~ netaccess @@ -3694,6 +3786,10 @@ ip.show() ip = IPv6(dst="www.yahoo.com") assert IPv6(raw(ip)).dst == [p.dst for p in ip][0] += Net6 using implicit format in IPv6 + +assert len(list(IPv6(dst=("fe80::1", "fe80::1f")))) == 31 + = Multiple IPv6 addresses test ~ netaccess ipv6 @@ -3883,6 +3979,9 @@ conf.route.routes = [ assert sorted(conf.route.get_if_bcast(dummy_interface)) == sorted(['169.254.255.255', '172.21.230.255', '239.255.255.255']) conf.route.routes = bck_conf_route_routes += Remove dummy interface + +conf.ifaces.reload() ############ ############ @@ -4038,14 +4137,14 @@ os.write(fd, b"-- MIB test\nscapy OBJECT IDENTIFIER ::= {test 2807}\n") os.close(fd) load_mib(fname) -assert sum(1 for k in six.itervalues(conf.mib.d) if "scapy" in k) == 1 +assert sum(1 for k in conf.mib.d.values() if "scapy" in k) == 1 assert sum(1 for oid in conf.mib) > 100 = MIB - graph ~ mib -import mock +from unittest import mock @mock.patch("scapy.asn1.mib.do_graph") def get_mib_graph(do_graph): @@ -4094,6 +4193,18 @@ except DeprecationWarning: # -Werror is used pass += MIB - Check that MIB OIDs are not duplicated +~ mib + +from scapy.asn1.mib import x509_oids_sets + +_dct = {} +for d in x509_oids_sets: + for elt in d: + if elt in _dct: + raise ValueError("OID %s already exists" % elt) + _dct.update(d) + = BER tests BER_id_enc(42) == '*' @@ -4508,7 +4619,7 @@ assert all(bytes(a[0]) == bytes(b[0]) for a, b in zip(unp, srl)) = plot() -import mock +from unittest import mock import scapy.libs.matplot @mock.patch("scapy.libs.matplot.plt") @@ -4524,7 +4635,7 @@ test_plot() = diffplot() -import mock +from unittest import mock import scapy.libs.matplot @mock.patch("scapy.libs.matplot.plt") @@ -4540,7 +4651,7 @@ test_diffplot() = multiplot() -import mock +from unittest import mock import scapy.libs.matplot @mock.patch("scapy.libs.matplot.plt") @@ -4550,7 +4661,7 @@ def test_multiplot(mock_plt): mock_plt.plot = fake_plot tmp = [IP(id=i)/TCP() for i in range(10)] plist = PacketList([tuple(tmp[i-2:i]) for i in range(2, 10, 2)]) - lines = plist.multiplot(lambda x: (x[1][IP].src, (x[1].time, x[1][IP].id))) + lines = plist.multiplot(lambda x, y: (y[IP].src, (y.time, y[IP].id))) assert len(lines) == 1 assert len(lines[0]) == 4 @@ -4639,17 +4750,17 @@ test_padding() def test_nzpadding(): with ContextManagerCaptureOutput() as cmco: - p = PacketList([IP()/conf.padding_layer("A%s" % i) for i in range(2)]) + p = PacketList([IP()/conf.padding_layer("AB"), IP()/conf.padding_layer("\x00\x00")]) p.nzpadding() result_pl_nzpadding = cmco.get_output() - assert len(result_pl_nzpadding.split('\n')) == 5 - assert "0000 41 30" in result_pl_nzpadding + assert len(result_pl_nzpadding.split('\n')) == 3 + assert "0000 41 42" in result_pl_nzpadding test_nzpadding() = conversations() -import mock +from unittest import mock @mock.patch("scapy.plist.do_graph") def test_conversations(mock_do_graph): def fake_do_graph(graph, **kwargs): @@ -4677,7 +4788,7 @@ assert len(pl.sessions().keys()) == 5 = afterglow() -import mock +from unittest import mock @mock.patch("scapy.plist.do_graph") def test_afterglow(mock_do_graph): def fake_do_graph(graph, **kwargs): @@ -4718,7 +4829,7 @@ if PYX: = svgdump() print("PYX: %d" % PYX) -if PYX and not six.PY2: +if PYX: import tempfile import os filename = tempfile.mktemp(suffix=".svg") @@ -4746,6 +4857,16 @@ assert pl[0].wirelen == 1 assert pl[0][Ether].src == '00:11:22:33:44:55' assert pl[1][Ether].dst == '00:22:33:44:55:66' += EDecimal + +# GH4488 +p1, p2 = EDecimal('1722417787.778435252'), EDecimal('1722417787.778435216') +assert p1 != p2 +assert p1 > p2 +assert not (p1 < p2) +assert p1 == 1722417787.778435252 # float test +assert p2 == 1722417787.778435216 +assert (p1, 0) > (p2, 1) ############ ############ @@ -4754,18 +4875,34 @@ assert pl[1][Ether].dst == '00:22:33:44:55:66' = _version() import os -version_filename = os.path.join(scapy._SCAPY_PKG_DIR, "VERSION") +from datetime import datetime, timezone +version_filename = os.path.join(scapy._SCAPY_PKG_DIR, "VERSION") +mtime = datetime.fromtimestamp(os.path.getmtime(scapy.__file__), timezone.utc) version = "2.0.0" with open(version_filename, "w") as fd: fd.write(version) -import mock -with mock.patch("scapy._version_from_git_describe") as version_mocked: - version_mocked.side_effect = Exception() - assert scapy._version() == version - os.unlink(version_filename) - assert scapy._version() == "unknown.version" +os.environ["SCAPY_VERSION"] = "9.9.9" +assert scapy._version() == "9.9.9" +del os.environ["SCAPY_VERSION"] + +assert scapy._version() == version +os.unlink(version_filename) + +from unittest import mock +with mock.patch("scapy._version_from_git_archive") as archive: + archive.return_value = "4.4.4" + assert scapy._version() == "4.4.4" + archive.side_effect = ValueError() + with mock.patch("scapy._version_from_git_describe") as git: + git.return_value = "3.3.3" + assert scapy._version() == "3.3.3" + git.side_effect = Exception() + assert scapy._version() == mtime.strftime("%Y.%m.%d") + with mock.patch("os.path.getmtime") as getmtime: + getmtime.side_effect = Exception() + assert scapy._version() == "0.0.0" = UTscapy HTML output @@ -4791,7 +4928,7 @@ assert os.path.isdir(dname) = test fragleak functions ~ netaccess linux fragleak -import mock +from unittest import mock @mock.patch("scapy.layers.inet.conf.L3socket") @mock.patch("scapy.layers.inet.select.select") @@ -4810,3 +4947,34 @@ def _test_fragleak(func, sr1, select, L3socket): assert _test_fragleak(fragleak) assert _test_fragleak(fragleak2) + ++ CLIUtil +~ cliutil + += CLIUtil: define and check overlap + +from scapy.layers.smbclient import smbclient + +class CLI1(CLIUtil): + @CLIUtil.addcommand() + def shares(self): + return 1 + @CLIUtil.addoutput(shares) + def shares_output(self, results): + print(results) + + +class CLI2(CLIUtil): + @CLIUtil.addcommand() + def shares(self): + return 2 + @CLIUtil.addoutput(shares) + def shares_output(self, results): + print(results) + + +c1 = CLI1(cli=False) +c2 = CLI2(cli=False) + +assert c1.shares() == 1 +assert c2.shares() == 2 diff --git a/test/run_tests b/test/run_tests index 7304855b0b6..a44808dfc56 100755 --- a/test/run_tests +++ b/test/run_tests @@ -21,8 +21,8 @@ then do case $arg in - -2) PYTHON=python2;; -3) PYTHON=python3;; + -W) PYTHONWARNINGS="-W error";; *) ARGS="$ARGS $arg";; esac done @@ -54,10 +54,12 @@ then fi # Run tox - export UT_FLAGS="-K tcpdump -K manufdb -K wireshark -K ci_only -K vcan_socket -K automotive_comm -K imports -K scanner" + export UT_FLAGS="-K tcpdump -K wireshark -K tshark -K ci_only -K vcan_socket -K automotive_comm -K imports -K scanner" export SIMPLE_TESTS="true" + export PYTHON + export DISABLE_COVERAGE=" " PYVER=$($PYTHON -c "import sys; print('.'.join(sys.version.split('.')[:2]))") - ${DIR}/.config/ci/test.sh $PYVER non_root + bash ${DIR}/.config/ci/test.sh $PYVER non_root exit $? fi -PYTHONPATH=$DIR exec "$PYTHON" ${DIR}/scapy/tools/UTscapy.py $ARGS +PYTHONPATH=$DIR exec "$PYTHON" $PYTHONWARNINGS ${DIR}/scapy/tools/UTscapy.py $ARGS diff --git a/test/run_tests.bat b/test/run_tests.bat index f3812ea359d..b66ddcd7030 100644 --- a/test/run_tests.bat +++ b/test/run_tests.bat @@ -5,10 +5,7 @@ set PYTHONPATH=%MYDIR% REM Note: shift will not work with %* REM ### Get args, Handle Python version ### set "_args=%*" -IF "%1" == "-2" ( - set PYTHON=python - set "_args=%_args:~3%" -) ELSE IF "%1" == "-3" ( +IF "%1" == "-3" ( set PYTHON=python3 set "_args=%_args:~3%" ) diff --git a/test/scapy/automaton.uts b/test/scapy/automaton.uts index bc103f3126d..ab107b8bc74 100644 --- a/test/scapy/automaton.uts +++ b/test/scapy/automaton.uts @@ -446,6 +446,27 @@ graph = HelloWorld.build_graph() assert graph.startswith("digraph") assert '"BEGIN" -> "END"' in graph += Automaton graph - with indirection +~ automaton + +class HelloWorld(Automaton): + @ATMT.state(initial=1) + def BEGIN(self): + self.count1 = 0 + self.count2 = 0 + @ATMT.condition(BEGIN) + def cnd_1(self): + self.cnd_generic() + def cnd_generic(self): + raise END + @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 diff --git a/test/scapy/layers/bluetooth.uts b/test/scapy/layers/bluetooth.uts index 76d1f959422..9df33f0126b 100644 --- a/test/scapy/layers/bluetooth.uts +++ b/test/scapy/layers/bluetooth.uts @@ -3,56 +3,242 @@ + 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() + +# HCI_Command_Hdr +# default construction +hci_cmd_hdr = HCI_Command_Hdr() +assert hci_cmd_hdr.ogf == 0 +assert hci_cmd_hdr.ocf == 0 +assert hci_cmd_hdr.len == None +assert raw(hci_cmd_hdr) == b'\x00\x00\x00' + +# parsing +hci_cmd_hdr = HCI_Command_Hdr(raw(hci_cmd_hdr)) +assert hci_cmd_hdr.ogf == 0 +assert hci_cmd_hdr.ocf == 0 +assert hci_cmd_hdr.len == 0 + +# HCI_Cmd_Inquiry default construction +hci_cmd_inquiry = HCI_Command_Hdr() / HCI_Cmd_Inquiry() +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == None +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# parsing +hci_cmd_inquiry = HCI_Command_Hdr(raw(hci_cmd_inquiry)) +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 5 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# HCI_Cmd_Inquiry constructing an invalid packet +hci_cmd_inquiry = HCI_Command_Hdr(len = 10) / HCI_Cmd_Inquiry() +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 10 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +assert raw(hci_cmd_inquiry)[2] == 10 + +# parse the invalid packet +hci_cmd_inquiry = HCI_Command_Hdr(raw(hci_cmd_inquiry)) +assert hci_cmd_inquiry.ogf == 0x01 +assert hci_cmd_inquiry.ocf == 0x01 +assert hci_cmd_inquiry.len == 10 +assert hci_cmd_inquiry.lap == 0x9e8b33 +assert hci_cmd_inquiry.inquiry_length == 0 +assert hci_cmd_inquiry.num_responses == 0 + +# HCI_Cmd_Inquiry_Cancel default construction +hci_cmd_inquiry_cancel = HCI_Command_Hdr() / HCI_Cmd_Inquiry_Cancel() +assert hci_cmd_inquiry_cancel.ogf == 0x01 +assert hci_cmd_inquiry_cancel.ocf == 0x02 +assert hci_cmd_inquiry_cancel.len == None + +# hci_cmd_inquiry_cancel parsing +hci_cmd_inquiry_cancel = HCI_Command_Hdr(raw(hci_cmd_inquiry_cancel)) +assert hci_cmd_inquiry_cancel.ogf == 0x01 +assert hci_cmd_inquiry_cancel.ocf == 0x02 +assert hci_cmd_inquiry_cancel.len == 0 + + +# Hci_Cmd_Hold_Mode +hci_cmd_hold_mode = HCI_Command_Hdr() / HCI_Cmd_Hold_Mode() +assert hci_cmd_hold_mode.ogf == 0x02 +assert hci_cmd_hold_mode.ocf == 0x01 +assert hci_cmd_hold_mode.len == None + +# parsing +hci_cmd_hold_mode = HCI_Command_Hdr(raw(hci_cmd_hold_mode)) +assert hci_cmd_hold_mode.ogf == 0x02 +assert hci_cmd_hold_mode.ocf == 0x01 +assert hci_cmd_hold_mode.len == 6 + +# HCI_Cmd_Set_Event_Mask +hci_cmd_set_event_mask = HCI_Command_Hdr() / HCI_Cmd_Set_Event_Mask() +assert hci_cmd_set_event_mask.ogf == 0x03 +assert hci_cmd_set_event_mask.ocf == 0x01 +assert hci_cmd_set_event_mask.len == None + +# parsing +hci_cmd_set_event_mask = HCI_Command_Hdr(raw(hci_cmd_set_event_mask)) +assert hci_cmd_set_event_mask.ogf == 0x03 +assert hci_cmd_set_event_mask.ocf == 0x01 +assert hci_cmd_set_event_mask.len == 8 + +# HCI_Cmd_Read_BD_Addr +hci_cmd_read_bd_addr = HCI_Command_Hdr() / HCI_Cmd_Read_BD_Addr() +assert hci_cmd_read_bd_addr.ogf == 0x04 +assert hci_cmd_read_bd_addr.ocf == 0x09 +assert hci_cmd_read_bd_addr.len == None + +# parsing +hci_cmd_read_bd_addr = HCI_Command_Hdr(raw(hci_cmd_read_bd_addr)) +assert hci_cmd_read_bd_addr.ogf == 0x04 +assert hci_cmd_read_bd_addr.ocf == 0x09 +assert hci_cmd_read_bd_addr.len == 0 + + +# HCI_Cmd_Read_Link_Quality +hci_cmd_read_link_quality = HCI_Command_Hdr() / HCI_Cmd_Read_Link_Quality() +assert hci_cmd_read_link_quality.ogf == 0x05 +assert hci_cmd_read_link_quality.ocf == 0x03 +assert hci_cmd_read_link_quality.len == None + +# parsing +hci_cmd_read_link_quality = HCI_Command_Hdr(raw(hci_cmd_read_link_quality)) +assert hci_cmd_read_link_quality.ogf == 0x05 +assert hci_cmd_read_link_quality.ocf == 0x03 +assert hci_cmd_read_link_quality.len == 2 + + +# HCI_Cmd_Read_Loopback_Mode +hci_cmd_read_loopback_mode = HCI_Command_Hdr() / HCI_Cmd_Read_Loopback_Mode() +assert hci_cmd_read_loopback_mode.ogf == 0x06 +assert hci_cmd_read_loopback_mode.ocf == 0x01 +assert hci_cmd_read_loopback_mode.len == None + +# parsing +hci_cmd_read_loopback_mode = HCI_Command_Hdr(raw(hci_cmd_read_loopback_mode)) +assert hci_cmd_read_loopback_mode.ogf == 0x06 +assert hci_cmd_read_loopback_mode.ocf == 0x01 +assert hci_cmd_read_loopback_mode.len == 0 + + +# HCI_Cmd_LE_Read_Buffer_Size_V1 +hci_cmd_le_read_buffer_size_v1 = HCI_Command_Hdr() / HCI_Cmd_LE_Read_Buffer_Size_V1() +assert hci_cmd_le_read_buffer_size_v1.ogf == 0x08 +assert hci_cmd_le_read_buffer_size_v1.ocf == 0x02 +assert hci_cmd_le_read_buffer_size_v1.len == None + +# parsing +hci_cmd_le_read_buffer_size_v1 = HCI_Command_Hdr(raw(hci_cmd_le_read_buffer_size_v1)) +assert hci_cmd_le_read_buffer_size_v1.ogf == 0x08 +assert hci_cmd_le_read_buffer_size_v1.ocf == 0x02 +assert hci_cmd_le_read_buffer_size_v1.len == 0 + Bluetooth Transport Layers -= Test HCI_PHDR_Hdr + += Test HCI_PHDR_Hdr piling up 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' +assert raw(pkt) == b'\x00\x00\x00\x00\x02\x00\x00\n\x00\x06\x00\x05\x00\n\x01\x02\x00\x00\x00' pkt = HCI_PHDR_Hdr(raw(pkt)) assert HCI_Hdr in pkt assert L2CAP_InfoReq in pkt +assert pkt[L2CAP_Hdr].len == 6 +assert pkt[L2CAP_Hdr].cid == 5 +assert pkt[L2CAP_CmdHdr].code == 10 +assert pkt[L2CAP_CmdHdr].id == 1 +assert pkt[L2CAP_CmdHdr].len == 2 +assert len(pkt[L2CAP_InfoReq]) == 2 + HCI Commands += Create Connection + +cmd = HCI_Hdr(hex_bytes("0105040d76d56f95010018cc0200000001")) +assert HCI_Cmd_Create_Connection in cmd +assert cmd[HCI_Cmd_Create_Connection].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Create_Connection].packet_type == 52248 +assert cmd[HCI_Cmd_Create_Connection].page_scan_repetition_mode == 2 +assert cmd[HCI_Cmd_Create_Connection].reserved == 0 +assert cmd[HCI_Cmd_Create_Connection].clock_offset == 0 +assert cmd[HCI_Cmd_Create_Connection].allow_role_switch == 1 + += Authentication Requested + +cmd = HCI_Hdr(hex_bytes("011104020001")) +assert HCI_Cmd_Authentication_Requested in cmd +assert cmd[HCI_Cmd_Authentication_Requested].handle == 256 + += Link Key Request Reply + +cmd = HCI_Hdr(hex_bytes("010b041676d56f9501006c9016a48a009180086a39200f03d3dd")) +assert HCI_Cmd_Link_Key_Request_Reply in cmd +assert cmd[HCI_Cmd_Link_Key_Request_Reply].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Link_Key_Request_Reply].link_key == 0x6c9016a48a009180086a39200f03d3dd + += Set Connection Encryption + +cmd = HCI_Hdr(hex_bytes("01130403000101")) +assert HCI_Cmd_Set_Connection_Encryption in cmd +assert cmd[HCI_Cmd_Set_Connection_Encryption].handle == 256 +assert cmd[HCI_Cmd_Set_Connection_Encryption].encryption_enable == 1 + += Remote Name Request + +cmd = HCI_Hdr(hex_bytes("0119040a76d56f95010002000000")) +assert HCI_Cmd_Remote_Name_Request in cmd +assert cmd[HCI_Cmd_Remote_Name_Request].bd_addr == "00:01:95:6f:d5:76" +assert cmd[HCI_Cmd_Remote_Name_Request].page_scan_repetition_mode == 2 +assert cmd[HCI_Cmd_Remote_Name_Request].reserved == 0 +assert cmd[HCI_Cmd_Remote_Name_Request].clock_offset == 0 + += 7.3.12 Read Local Name +cmd = HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_Read_Local_Name() +assert raw(cmd) == hex_bytes("01140c00") + +# Response +response = HCI_Hdr(hex_bytes("040efc01140c00546865726d6973746f7200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000")) +assert HCI_Cmd_Complete_Read_Local_Name in response +assert response[HCI_Cmd_Complete_Read_Local_Name].local_name.decode('utf-8').rstrip('\x00') == 'Thermistor' +assert response.answers(cmd) + += 7.4.1 Read Local Version Information +cmd = HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_Read_Local_Version_Information() +assert raw(cmd) == hex_bytes("01011000") + +# Response +response = HCI_Hdr(hex_bytes("040e0c010110000900100931010c22")) +assert HCI_Cmd_Complete_Read_Local_Version_Information in response +assert response[HCI_Cmd_Complete_Read_Local_Version_Information].hci_version == 9 +assert response[HCI_Cmd_Complete_Read_Local_Version_Information].hci_subversion == 4096 +assert response[HCI_Cmd_Complete_Read_Local_Version_Information].lmp_version == 9 +assert response[HCI_Cmd_Complete_Read_Local_Version_Information].company_identifier == 0x0131 +assert response[HCI_Cmd_Complete_Read_Local_Version_Information].lmp_subversion == 8716 +assert response.answers(cmd) + += 7.4.4 Read Local Extended Features +cmd = HCI_Hdr() / HCI_Command_Hdr() / HCI_Cmd_Read_Local_Extended_Features(page_number=1) +assert raw(cmd) == hex_bytes("0104100101") + +# Response +response = HCI_Hdr(hex_bytes("040e0e0104100001020000000000000000")) +assert HCI_Cmd_Complete_Read_Local_Extended_Features in response +assert response[HCI_Cmd_Complete_Read_Local_Extended_Features].page == 1 +assert response[HCI_Cmd_Complete_Read_Local_Extended_Features].max_page == 2 +assert response[HCI_Cmd_Complete_Read_Local_Extended_Features].extended_features == 0 +assert response.answers(cmd) + = LE Create Connection # Request data @@ -120,6 +306,147 @@ assert expected_cmd_raw_data == cmd_raw_data + HCI Events + += Inquiry Complete +evt_raw_data = hex_bytes("04010100") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Inquiry_Complete in evt_pkt +assert evt_pkt[HCI_Event_Inquiry_Complete].status == 0 + += Inquiry Result +evt_pkt = HCI_Event_Inquiry_Result(b'\x01\xcb\x7f\xdbn\x8c\x9c\x01\x00\x00<\x04\x08\x8di') +assert HCI_Event_Inquiry_Result in evt_pkt +assert evt_pkt[HCI_Event_Inquiry_Result].num_response == 1 +assert evt_pkt[HCI_Event_Inquiry_Result].addr[0] == '9c:8c:6e:db:7f:cb' +assert evt_pkt[HCI_Event_Inquiry_Result].page_scan_repetition_mode[0] == 1 +assert evt_pkt[HCI_Event_Inquiry_Result].device_class[0] == 0x8043c +assert evt_pkt[HCI_Event_Inquiry_Result].clock_offset[0] == 27021 + + += Connection Complete +evt_raw_data = hex_bytes("04030b000b00093491e5b7540100") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Connection_Complete in evt_pkt +assert evt_pkt[HCI_Event_Connection_Complete].status == 0 +assert evt_pkt[HCI_Event_Connection_Complete].handle == 0x000b +assert evt_pkt[HCI_Event_Connection_Complete].bd_addr == "54:b7:e5:91:34:09" +assert evt_pkt[HCI_Event_Connection_Complete].link_type == 1 +assert evt_pkt[HCI_Event_Connection_Complete].encryption_enabled == 0 + += Disconnection Complete +evt_raw_data = hex_bytes("04050400400016") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Disconnection_Complete in evt_pkt +assert evt_pkt[HCI_Event_Disconnection_Complete].status == 0 +assert evt_pkt[HCI_Event_Disconnection_Complete].handle == 0x0040 +assert evt_pkt[HCI_Event_Disconnection_Complete].reason == 0x16 + += Remote Name Request Complete +evt_raw_data = hex_bytes("0407ff0076d56f950100746573742d6c6170746f70000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Remote_Name_Request_Complete in evt_pkt +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].status == 0 +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].bd_addr == "00:01:95:6f:d5:76" +assert evt_pkt[HCI_Event_Remote_Name_Request_Complete].remote_name == b"test-laptop".ljust(248, b"\x00") + += Encryption Change +evt_raw_data = hex_bytes("040804000b0001") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Encryption_Change in evt_pkt +assert evt_pkt[HCI_Event_Encryption_Change].status == 0 +assert evt_pkt[HCI_Event_Encryption_Change].handle == 0x000b +assert evt_pkt[HCI_Event_Encryption_Change].enabled == 1 + += Read Remote Supported Features Complete +evt_raw_data = hex_bytes("040b0b000b00fffe8ffedbff5b87") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Read_Remote_Supported_Features_Complete in evt_pkt +assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].status == 0 +assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].handle == 0x000b +assert evt_pkt[HCI_Event_Read_Remote_Supported_Features_Complete].lmp_features == 0x875bffdbfe8ffeff + += Read Remote Version Information Complete +evt_raw_data = hex_bytes("040c080002000bb0022c04") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Read_Remote_Version_Information_Complete in evt_pkt +assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].status == 0 +assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].handle == 0x0002 +assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].version == 0x0b +assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].manufacturer_name == 0x02b0 +assert evt_pkt[HCI_Event_Read_Remote_Version_Information_Complete].subversion == 1068 + += Command Complete +evt_raw_data = hex_bytes("040e0a010b04002587ceedd668") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Command_Complete in evt_pkt +assert evt_pkt[HCI_Event_Command_Complete].number == 1 +assert evt_pkt[HCI_Event_Command_Complete].opcode == 0x040b +assert evt_pkt[HCI_Event_Command_Complete].status == 0 + += Command Status +evt_raw_data = hex_bytes("040f0400011904") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Command_Status in evt_pkt +assert evt_pkt[HCI_Event_Command_Status].status == 0 +assert evt_pkt[HCI_Event_Command_Status].number == 1 +assert evt_pkt[HCI_Event_Command_Status].opcode == 0x0419 + += Number Of Completed Packets +evt_raw_data = hex_bytes("0413050103000300") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Number_Of_Completed_Packets in evt_pkt +assert evt_pkt[HCI_Event_Number_Of_Completed_Packets].num_handles == 1 +assert evt_pkt[HCI_Event_Number_Of_Completed_Packets].connection_handle_list[0] == 0x0003 +assert evt_pkt[HCI_Event_Number_Of_Completed_Packets].num_completed_packets_list[0] == 3 + += Link Key Request +evt_raw_data = hex_bytes("041706093491e5b754") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Link_Key_Request in evt_pkt +assert evt_pkt[HCI_Event_Link_Key_Request].bd_addr == '54:b7:e5:91:34:09' + += Inquiry Result with RSSI +# TODO + += Read Remote Extended Features Complete +evt_raw_data = hex_bytes("04230d000b0001020300000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Read_Remote_Extended_Features_Complete in evt_pkt +assert evt_pkt[HCI_Event_Read_Remote_Extended_Features_Complete].status == 0 +assert evt_pkt[HCI_Event_Read_Remote_Extended_Features_Complete].handle == 0x000b +assert evt_pkt[HCI_Event_Read_Remote_Extended_Features_Complete].page == 1 +assert evt_pkt[HCI_Event_Read_Remote_Extended_Features_Complete].max_page == 2 +assert evt_pkt[HCI_Event_Read_Remote_Extended_Features_Complete].extended_features == 0x0000000000000003 + += Extended Inquiry Result +evt_raw_data = hex_bytes("042fff01093491e5b75401001404247c37c2091001000a00ffffffff020a040b020d110b110a110e110f110c095354414e4d4f524520494900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_Extended_Inquiry_Result in evt_pkt +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].num_response == 1 +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].bd_addr == '54:b7:e5:91:34:09' +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].page_scan_repetition_mode == 1 +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].device_class == 0x240414 +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].clock_offset == 0x377c +assert evt_pkt[HCI_Event_Extended_Inquiry_Result].rssi == -62 +assert EIR_Hdr in evt_pkt[HCI_Event_Extended_Inquiry_Result].eir_data[0] +assert Raw in evt_pkt[HCI_Event_Extended_Inquiry_Result].eir_data[-1] +assert len(evt_pkt[HCI_Event_Extended_Inquiry_Result].eir_data[-1].load) == 200 + += IO Capability Response +evt_raw_data = hex_bytes("043209093491e5b754030002") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_IO_Capability_Response in evt_pkt +assert evt_pkt[HCI_Event_IO_Capability_Response].bd_addr == '54:b7:e5:91:34:09' +assert evt_pkt[HCI_Event_IO_Capability_Response].io_capability == 0x03 +assert evt_pkt[HCI_Event_IO_Capability_Response].oob_data_present == 0 +assert evt_pkt[HCI_Event_IO_Capability_Response].authentication_requirements == 0x02 + += LE Meta +evt_raw_data = hex_bytes("043e0414400000") +evt_pkt = HCI_Hdr(evt_raw_data) +assert HCI_Event_LE_Meta in evt_pkt +assert evt_pkt[HCI_Event_LE_Meta].event == 0x14 + = LE Connection Update Event evt_raw_data = hex_bytes("043e0a03004800140001003c00") evt_pkt = HCI_Hdr(evt_raw_data) @@ -130,6 +457,67 @@ assert evt_pkt[HCI_LE_Meta_Connection_Update_Complete].timeout == 60 + Bluetooth LE Advertising / Scan Response Data Parsing += Parse EIR_IncompleteList32BitServiceUUIDs + +p = HCI_Hdr(hex_bytes('042fff019cc888f640c401000c025af32cb09904f6dc73222396f640c40c025a40dbca09000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) +assert EIR_IncompleteList32BitServiceUUIDs in p +assert len(p[EIR_IncompleteList32BitServiceUUIDs].svc_uuids) == 38 + += Parse EIR_CompleteList32BitServiceUUIDs + +p = HCI_Hdr(hex_bytes('042fff0106ec883aef1801003c04285758b30e0954562064656c2073616cc3b36e09030a110c110e1100120105810700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) +assert EIR_CompleteList32BitServiceUUIDs in p +assert p[EIR_CompleteList32BitServiceUUIDs].svc_uuids == [] + += Parse EIR_ClassOfDevice + +p = HCI_Hdr(hex_bytes('043e2b020100000a1bb44ce0001f02010503ff000106084d4920524303021218040d040500020a0004fe06ec88a2')) +assert EIR_ClassOfDevice in p +assert p[EIR_ClassOfDevice].major_service_classes == 0 +assert p[EIR_ClassOfDevice].major_device_class == 5 +assert p[EIR_ClassOfDevice].minor_device_class == 1 + += Parse EIR_PublicTargetAddress +p = HCI_Hdr(hex_bytes('043e1402010001554433221100080717ffeeddccbbaaaa')) +assert EIR_PublicTargetAddress in p +assert p[EIR_PublicTargetAddress].bd_addr == 'aa:bb:cc:dd:ee:ff' + += Parse EIR_AdvertisingInterval +p = HCI_Event_Hdr(hex_bytes('3e23020100002e4961121110170201060f0954656c6553617420283432453229031a9001a3')) +assert EIR_AdvertisingInterval in p +assert p[EIR_AdvertisingInterval].advertising_interval == 400 + +p = BTLE(hex_bytes('d6be898e20234fe761e5b754021a1803030a18020ace12fffa07104a2b010000000054b7e561e74f00000000')) +assert EIR_AdvertisingInterval in p +assert p[EIR_AdvertisingInterval].advertising_interval == 24 + += Parse EIR_LEBluetoothDeviceAddress +p = HCI_Event_Hdr(hex_bytes("3e2a02010000d93519d7ba4c1e0201020affc4000734151317fd80081b00d93519d7ba4c0303b9fe020ad4ad")) +assert EIR_LEBluetoothDeviceAddress in p +assert p[EIR_LEBluetoothDeviceAddress].addr_type == 0x0 +assert p[EIR_LEBluetoothDeviceAddress].bd_addr == '4c:ba:d7:19:35:d9' + += Parse EIR_Appearance +p = BTLE(hex_bytes("d6be898e201660d4d3cebffb0201050319420c0303e7fe040948393850c27c")) +assert EIR_Appearance in p +assert p[EIR_Appearance].appearance == 0x0c42 +assert p[EIR_Appearance].category == 0x31 #'Pulse Oximeter' +assert p[EIR_Appearance].subcategory == 0x02 # Wrist Worn Pulse Oximeter + += Parse EIR_ServiceData32BitUUID + +p = HCI_Hdr(hex_bytes('042fff01c47c80894df801000c0128a269a30c4a125d13f30196894df80c012820f61a1a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) +assert EIR_ServiceData32BitUUID in p +assert p[EIR_ServiceData32BitUUID].svc_uuid == 0x001a1af6 + += Parse EIR_URI + +p = HCI_Event_Hdr(hex_bytes('3e2902010301f3c1dad728031d1c24172f2f6669726d776172652e73696c766169722e636f6d2f6f6f62ac')) +assert EIR_URI in p +assert p[EIR_URI].scheme == 0x17 +assert p[EIR_URI].uri_hier_part == b'//firmware.silvair.com/oob' +assert p[EIR_URI].uri == 'https://firmware.silvair.com/oob' + = Parse EIR_Flags, EIR_CompleteList16BitServiceUUIDs, EIR_CompleteLocalName and EIR_TX_Power_Level ad_report_raw_data = \ @@ -150,7 +538,8 @@ scan_resp_raw_data = \ 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 +assert scapy_packet[EIR_Manufacturer_Specific_Data].company_identifier == 0x154 +assert scapy_packet[EIR_Manufacturer_Specific_Data].sprintf("%company_identifier%") == "Pebble Technology" = Parse EIR_Manufacturer_Specific_Data with magic @@ -179,13 +568,13 @@ EIR_Manufacturer_Specific_Data.register_magic_payload(ScapyManufacturerPacket2) p = EIR_Hdr(b'\x0b\xff\xff\xffSCAPY!\xab\x12') p.show() -assert p[EIR_Manufacturer_Specific_Data].company_id == 0xffff +assert p[EIR_Manufacturer_Specific_Data].company_identifier == 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[EIR_Manufacturer_Specific_Data].company_identifier == 0xffff assert p[ScapyManufacturerPacket2].y == 0x1234 # Test encode @@ -200,6 +589,13 @@ except TypeError: else: assert False, "expected exception" += Parse EIR_ServiceSolicitation16BitUUID and EIR_ServiceSolicitation128BitUUID + +d = hex_bytes("043e29020100013d1ef10747d81d0319000002010603140d181115d0002d121e4b0fa4994eceb531f40579aa") +p = HCI_Hdr(d) +assert p[EIR_ServiceSolicitation16BitUUID].svc_uuid == 0x180d +assert p[EIR_ServiceSolicitation128BitUUID].svc_uuid == UUID('7905f431-b5ce-4e99-a40f-4b1e122d00d0') + = Parse EIR_ServiceData16BitUUID d = hex_bytes("043e1902010001abcdef7da97f0d020102030350fe051650fee6c2ac") @@ -217,14 +613,14 @@ 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' +a = HCI_Hdr()/HCI_ACL_Hdr(handle=0xf4c, PB=2, BC=2, len=20)/L2CAP_Hdr(len=16)/L2CAP_CmdHdr(code=8, len=12)/L2CAP_EchoReq(data="AAAAAAAAAAAA") +assert raw(a) == b'\x02L\xaf\x14\x00\x10\x00\x05\x00\x08\x01\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' +assert raw(a) == b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\x02\x01\x04\x00\x00\x00\x01\x00' a.show() = Complex HCI - L2CAP dissect @@ -232,7 +628,25 @@ a = HCI_Hdr(b'\x02\x00\x00\x11\x00\r\x00\x05\x00\x0b\x00\t\x00\x01\x00\x00\x00de assert a[L2CAP_InfoResp].result == 0 assert a[L2CAP_InfoResp].data == b"debug" -= Answers += HCI - L2CAP Echo test + +rq = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/L2CAP_CmdHdr()/L2CAP_EchoReq(data=b"data") +assert bytes(rq) == b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\x08\x01\x04\x00data' + +rsp = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/L2CAP_CmdHdr()/L2CAP_EchoResp(data=b"data") +assert bytes(rsp) == b'\x02\x00\x00\x0c\x00\x08\x00\x05\x00\t\x01\x04\x00data' +assert rsp.answers(rq) + += HCI - L2CAP Create Channel request + +p = HCI_Hdr()/HCI_ACL_Hdr()/L2CAP_Hdr()/L2CAP_CmdHdr()/L2CAP_Create_Channel_Request(psm="SDP") +assert bytes(p) == b'\x02\x00\x00\r\x00\t\x00\x05\x00\x0c\x01\x05\x00\x01\x00\x00\x00\x00' + +p = HCI_Hdr(bytes(p)) +assert p[L2CAP_Create_Channel_Request].psm == 1 +assert p[L2CAP_Create_Channel_Request].scid == 0 + += L2CAP Conn 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) @@ -296,13 +710,66 @@ 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 b[EIR_Manufacturer_Specific_Data].company_identifier == 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" += EIR_Hdr - HCI_LE_Meta_Extended_Advertising_Report +a = HCI_Hdr()/HCI_Event_Hdr()/HCI_Event_LE_Meta()/HCI_LE_Meta_Extended_Advertising_Reports(reports=[ + HCI_LE_Meta_Extended_Advertising_Report( + #event_type = 0x0012, + scannable = 1, + legacy = 1, + address_type = 0x01, + address="a1:b2:c3:d4:e5:f6", + primary_phy = 1, + rssi = -85, + data=[ + EIR_Hdr()/EIR_CompleteList16BitServiceUUIDs( + svc_uuids = [0xffff], + ), + EIR_Hdr()/EIR_ServiceData16BitUUID( + svc_uuid = 0xffff + )/Raw(b"scapy\x00\x00\x00") + ] + ), + HCI_LE_Meta_Extended_Advertising_Report( + #event_type = 0x001a, + scannable = 1, + scan_response = 1, + legacy = 1, + address_type = 0x01, + address="a1:b2:c3:d4:e5:f6", + primary_phy = 1, + rssi = -85, + data=[ + EIR_Hdr()/EIR_Manufacturer_Specific_Data( + company_identifier = 0xffff, + ) / Raw(b"scapy\x00\x01\x02\x03\x04") + ] + ), +]) + +assert raw(a) == b"\x04\x3e\x50\x0d\x02\x12\x00\x01\xf6\xe5\xd4\xc3\xb2\xa1\x01\x00\xff\x7f\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x03\x03\xff\xff\x0b\x16\xff\xffscapy\x00\x00\x00\x1a\x00\x01\xf6\xe5\xd4\xc3\xb2\xa1\x01\x00\xff\x7f\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0e\x0d\xff\xff\xffscapy\x00\x01\x02\x03\x04" + +b = HCI_Hdr(raw(a)) +b.show() +assert b[HCI_Event_Hdr].len > 0 +assert b[HCI_LE_Meta_Extended_Advertising_Reports].num_reports == 2 +assert b[HCI_LE_Meta_Extended_Advertising_Report][0].address == "a1:b2:c3:d4:e5:f6" +assert b[HCI_LE_Meta_Extended_Advertising_Report][0].tx_power == 0x7f +assert b[HCI_LE_Meta_Extended_Advertising_Report][0].rssi == -85 +assert b[HCI_LE_Meta_Extended_Advertising_Report][0].data_length > 0 +assert b[EIR_CompleteList16BitServiceUUIDs].svc_uuids == [0xffff] +assert b[EIR_ServiceData16BitUUID].svc_uuid == 0xffff +assert raw(b[EIR_ServiceData16BitUUID].payload) == b"scapy\x00\x00\x00" +assert b[EIR_Manufacturer_Specific_Data].company_identifier == 0xffff +assert raw(b[EIR_Manufacturer_Specific_Data].payload) == b"scapy\x00\x01\x02\x03\x04" + + = 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)) @@ -332,23 +799,11 @@ 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_Security_Request +pkt = HCI_Hdr(hex_bytes('0200260600020006000b0d')) +assert SM_Security_Request in pkt +assert pkt[SM_Security_Request].auth_req == 0x0d + = SM_Public_Key() tests @@ -365,3 +820,41 @@ 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" + + ++ HCIMon tests + += HCI_Mon - Bluetooth Monitor Pcap Header + +p = HCI_Mon_Pcap_Hdr(hex_bytes("00000008")) +assert HCI_Mon_Pcap_Hdr in p +assert p[HCI_Mon_Pcap_Hdr].adapter_id == 0 +assert p[HCI_Mon_Pcap_Hdr].opcode == 8 + += HCI_Mon - Bluetooth Monitor HCI_Mon_New_Index + +p = HCI_Mon_Pcap_Hdr(hex_bytes("0000000000030000109a81206863693000000000")) +assert HCI_Mon_New_Index in p +assert p[HCI_Mon_New_Index].bus == 0 +assert p[HCI_Mon_New_Index].type == 3 +assert p[HCI_Mon_New_Index].addr == '20:81:9a:10:00:00' +assert p[HCI_Mon_New_Index].devname.decode('utf-8').rstrip('\x00') == 'hci0' + += HCI_Mon - Bluetooth Monitor HCI_Mon_Delete_Index + +p = HCI_Mon_Pcap_Hdr(hex_bytes("00000001")) +assert HCI_Mon_Pcap_Hdr in p +assert p[HCI_Mon_Pcap_Hdr].opcode == 1 + += HCI_Mon - Bluetooth Monitor HCI_Mon_Index_Info + +p = HCI_Mon_Pcap_Hdr(hex_bytes("0000000a0000109a81203101")) +assert HCI_Mon_Index_Info in p +assert p[HCI_Mon_Index_Info].addr == '20:81:9a:10:00:00' +assert p[HCI_Mon_Index_Info].manufacturer == 0x131 + += HCI_Mon - Bluetooth Monitor HCI_Mon_System_Note + +p = HCI_Mon_Pcap_Hdr(hex_bytes("ffff000c426c7565746f6f74682073756273797374656d2076657273696f6e20322e323200")) +assert HCI_Mon_System_Note in p +assert p[HCI_Mon_System_Note].note == b'Bluetooth subsystem version 2.22' diff --git a/test/scapy/layers/bluetooth4LE.uts b/test/scapy/layers/bluetooth4LE.uts index 33a0ee694b8..b671a0c347f 100644 --- a/test/scapy/layers/bluetooth4LE.uts +++ b/test/scapy/layers/bluetooth4LE.uts @@ -103,17 +103,18 @@ assert test[LL_UNKNOWN_RSP].code == 4 = LL_FEATURE_REQ -test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / LL_FEATURE_REQ(feature_set=0x1234) +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / LL_FEATURE_REQ(feature_set=0x011234) test = BTLE(raw(test)) assert test[LL_FEATURE_REQ].feature_set == \ - "ext_reject_ind+le_ping+le_data_len_ext+tx_mod_idx+le_ext_adv" + "ext_reject_ind+le_ping+le_data_len_ext+tx_mod_idx+le_ext_adv+conn_cte_req" = LL_FEATURE_RSP -test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / LL_FEATURE_RSP(feature_set=0x4321) +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / LL_FEATURE_RSP(feature_set=0x104321) test = BTLE(raw(test)) +print(test[LL_FEATURE_RSP].feature_set) assert test[LL_FEATURE_RSP].feature_set == \ - "le_encryption+le_data_len_ext+le_2m_phy+tx_mod_idx+ch_sel_alg" + "le_encryption+le_data_len_ext+le_2m_phy+tx_mod_idx+ch_sel_alg+antenna_switching_cte_aod_tx" = LL_PAUSE_ENC_REQ @@ -262,6 +263,210 @@ test = BTLE(raw(test)) assert test[LL_MIN_USED_CHANNELS_IND].phys == "phy_1m+phy_2m" assert test[LL_MIN_USED_CHANNELS_IND].min_used_channels == 3 +# LL_CTE_REQ + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CTE_REQ(min_cte_len_req=20, rfu=1, cte_type_req=2) +test = BTLE(raw(test)) +assert test[LL_CTE_REQ].min_cte_len_req == 20 +assert test[LL_CTE_REQ].rfu == 1 +assert test[LL_CTE_REQ].cte_type_req == 2 + + +# LL_CTE_RSP + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CTE_RSP() +test = BTLE(raw(test)) + + +# LL_PERIODIC_SYNC_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_PERIODIC_SYNC_IND(id=2, + sync_info=12345, + conn_event_count=0x4321, + last_pa_event_counter=0xabcd, sid=0xF, + a_type=1, sca=3, phy=2, AdvA="cc:bb:bb:bb:bb:bb", + sync_conn_event_count=32) +test = BTLE(raw(test)) +assert test[LL_PERIODIC_SYNC_IND].id == 2 +assert test[LL_PERIODIC_SYNC_IND].sync_info == 12345 +assert test[LL_PERIODIC_SYNC_IND].conn_event_count == 0x4321 +assert test[LL_PERIODIC_SYNC_IND].last_pa_event_counter == 0xabcd +assert test[LL_PERIODIC_SYNC_IND].sid == 0xF +assert test[LL_PERIODIC_SYNC_IND].a_type == 1 +assert test[LL_PERIODIC_SYNC_IND].sca == 3 +assert test[LL_PERIODIC_SYNC_IND].phy == 2 +assert test[LL_PERIODIC_SYNC_IND].AdvA == "cc:bb:bb:bb:bb:bb" +assert test[LL_PERIODIC_SYNC_IND].sync_conn_event_count == 32 + + +# LL_CLOCK_ACCURACY_REQ + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CLOCK_ACCURACY_REQ(sca=2) +test = BTLE(raw(test)) +assert test[LL_CLOCK_ACCURACY_REQ].sca == 2 + + +# LL_CLOCK_ACCURACY_RSP + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CLOCK_ACCURACY_RSP(sca=3) +test = BTLE(raw(test)) +assert test[LL_CLOCK_ACCURACY_RSP].sca == 3 + + +# LL_CIS_REQ + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CIS_REQ(cig_id=3, cis_id=2, phy_c_to_p=1, phy_p_to_c=2, + max_sdu_c_to_p=123, max_sdu_p_to_c=321, + sdu_interval_c_to_p=234, framed=1, sdu_interval_p_to_c=432, + max_pdu_c_to_p=123, max_pdu_p_to_c=234, + nse=10, subinterval=4567, + bn_c_to_p=3, bn_p_to_c=2, + ft_c_to_p=15, ft_p_to_c=16, + iso_interval=12345, + cis_offset_min=1, cis_offset_max=999, + conn_event_count=2) +test = BTLE(raw(test)) +assert test[LL_CIS_REQ].cig_id == 3 +assert test[LL_CIS_REQ].cis_id == 2 +assert test[LL_CIS_REQ].phy_c_to_p == 1 +assert test[LL_CIS_REQ].phy_p_to_c == 2 +assert test[LL_CIS_REQ].max_sdu_c_to_p == 123 +assert test[LL_CIS_REQ].framed == 1 +assert test[LL_CIS_REQ].max_sdu_p_to_c == 321 +assert test[LL_CIS_REQ].sdu_interval_c_to_p == 234 +assert test[LL_CIS_REQ].sdu_interval_p_to_c == 432 +assert test[LL_CIS_REQ].max_pdu_c_to_p == 123 +assert test[LL_CIS_REQ].max_pdu_p_to_c == 234 +assert test[LL_CIS_REQ].nse == 10 +assert test[LL_CIS_REQ].subinterval == 4567 +assert test[LL_CIS_REQ].bn_c_to_p == 3 +assert test[LL_CIS_REQ].bn_p_to_c == 2 +assert test[LL_CIS_REQ].ft_c_to_p == 15 +assert test[LL_CIS_REQ].ft_p_to_c == 16 +assert test[LL_CIS_REQ].iso_interval == 12345 +assert test[LL_CIS_REQ].cis_offset_min == 1 +assert test[LL_CIS_REQ].cis_offset_max == 999 +assert test[LL_CIS_REQ].conn_event_count == 2 + + +# LL_CIS_RSP + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CIS_RSP(cis_offset_min=1, cis_offset_max=999, conn_event_count=400) +test = BTLE(raw(test)) +assert test[LL_CIS_RSP].cis_offset_min == 1 +assert test[LL_CIS_RSP].cis_offset_max == 999 +assert test[LL_CIS_RSP].conn_event_count == 400 + + +# LL_CIS_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CIS_IND(AA=0x12345678, cis_offset=1, + cig_sync_delay=999, cis_sync_delay=400, conn_event_count=300) +test = BTLE(raw(test)) +assert test[LL_CIS_IND].AA == 0x12345678 +assert test[LL_CIS_IND].cis_offset == 1 +assert test[LL_CIS_IND].cig_sync_delay == 999 +assert test[LL_CIS_IND].cis_sync_delay == 400 +assert test[LL_CIS_IND].conn_event_count == 300 + + +# LL_CIS_TERMINATE_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CIS_TERMINATE_IND(cig_id=33, cis_id=44, error_code=55) +test = BTLE(raw(test)) +assert test[LL_CIS_TERMINATE_IND].cig_id == 33 +assert test[LL_CIS_TERMINATE_IND].cis_id == 44 +assert test[LL_CIS_TERMINATE_IND].error_code == 55 + + +# LL_POWER_CONTROL_REQ + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_POWER_CONTROL_REQ(phy=3, delta=-34, tx_power=55) +test = BTLE(raw(test)) +assert test[LL_POWER_CONTROL_REQ].phy == 3 +assert test[LL_POWER_CONTROL_REQ].delta == -34 +assert test[LL_POWER_CONTROL_REQ].tx_power == 55 + + +# LL_POWER_CONTROL_RSP + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_POWER_CONTROL_RSP(min=0, max=1, delta=-34, tx_power=55, apr=4) +test = BTLE(raw(test)) +assert test[LL_POWER_CONTROL_RSP].min == 0 +assert test[LL_POWER_CONTROL_RSP].max == 1 +assert test[LL_POWER_CONTROL_RSP].delta == -34 +assert test[LL_POWER_CONTROL_RSP].tx_power == 55 +assert test[LL_POWER_CONTROL_RSP].apr == 4 + + +# LL_POWER_CHANGE_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_POWER_CHANGE_IND(phy=3, min=0, max=1, delta=-34, tx_power=55) +test = BTLE(raw(test)) +assert test[LL_POWER_CHANGE_IND].phy == 3 +assert test[LL_POWER_CHANGE_IND].min == 0 +assert test[LL_POWER_CHANGE_IND].max == 1 +assert test[LL_POWER_CHANGE_IND].delta == -34 +assert test[LL_POWER_CHANGE_IND].tx_power == 55 + + + +# LL_SUBRATE_REQ + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_SUBRATE_REQ(subrate_factor_min=3, subrate_factor_max=0, + max_latency=1, continuation_number=123, timeout=55) +test = BTLE(raw(test)) +assert test[LL_SUBRATE_REQ].subrate_factor_min == 3 +assert test[LL_SUBRATE_REQ].subrate_factor_max == 0 +assert test[LL_SUBRATE_REQ].max_latency == 1 +assert test[LL_SUBRATE_REQ].continuation_number == 123 +assert test[LL_SUBRATE_REQ].timeout == 55 + + +# LL_SUBRATE_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_SUBRATE_IND(subrate_factor=3, subrate_base_event=0, + latency=1, continuation_number=123, timeout=55) +test = BTLE(raw(test)) +assert test[LL_SUBRATE_IND].subrate_factor == 3 +assert test[LL_SUBRATE_IND].subrate_base_event == 0 +assert test[LL_SUBRATE_IND].latency == 1 +assert test[LL_SUBRATE_IND].continuation_number == 123 +assert test[LL_SUBRATE_IND].timeout == 55 + + +# LL_CHANNEL_REPORTING_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CHANNEL_REPORTING_IND(enable=1, min_spacing=123, max_delay=124) +test = BTLE(raw(test)) +assert test[LL_CHANNEL_REPORTING_IND].enable == 1 +assert test[LL_CHANNEL_REPORTING_IND].min_spacing == 123 +assert test[LL_CHANNEL_REPORTING_IND].max_delay == 124 + + +# LL_CHANNEL_STATUS_IND + +test = BTLE(access_addr=1) / BTLE_DATA() / BTLE_CTRL() / \ + LL_CHANNEL_STATUS_IND(channel_classification=123456789012345) +test = BTLE(raw(test)) +assert test[LL_CHANNEL_STATUS_IND].channel_classification == 123456789012345 + + = BTLE_DATA + BTLE_EMPTY_PDU test = BTLE(access_addr=1)/BTLE_DATA(LLID=1, len=0)/BTLE_EMPTY_PDU() diff --git a/test/scapy/layers/can.uts b/test/scapy/layers/can.uts index 04e19bf68c5..8e4a2652c53 100644 --- a/test/scapy/layers/can.uts +++ b/test/scapy/layers/can.uts @@ -67,6 +67,19 @@ assert set(pkt.flags for pkt in packets) == {0} set(pkt.length for pkt in packets) == {1, 2, 8} += read PCAP of a CookedLinux/SocketCAN capture with CANFD frames + +conf.contribs['CAN']['swap-bytes'] = True + +packets = rdpcap(scapy_path("/test/pcaps/canfd.pcap.gz")) + += Check if parsing worked: each packet has a CANFD layer + +assert all(CANFD in pkt[1] for pkt in packets) + +assert all(pkt.identifier == 0x123 for pkt in packets) +assert len(packets) == 4 + ############ ############ @@ -134,9 +147,11 @@ pcap_fd = BytesIO(b'''(1539191392.761779) vcan0 123#11223344 (1539191494.084177) vcan0 1F334455#1122334455667788 (1539191494.724228) vcan0 1F334455#1122334455667788 (1539191495.148182) vcan0 1F334455#1122334455667788 - (1539191495.563320) vcan0 1F334455#1122334455667788''') + (1539191495.563320) vcan0 1F334455#1122334455667788 + (1539191470.820239) vcan0 123##1112233445566778899aabbccddeeff + (1539191495.563320) vcan0 1F334455##1112233445566778899aabbccddeeff''') packets = rdcandump(pcap_fd) -assert len(packets) == 9 +assert len(packets) == 11 assert packets[0].identifier == 0x123 assert packets[8].identifier == 0x1F334455 assert packets[8].flags == 0b100 @@ -144,6 +159,10 @@ assert packets[0].length == 4 assert packets[8].length == 8 assert packets[0].data == b'\x11\x22\x33\x44' assert packets[8].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' +assert packets[9].identifier == 0x123 +assert packets[10].identifier == 0x1F334455 +assert packets[9].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' +assert packets[10].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' = Check rdcandump_iterable default * default reading @@ -155,9 +174,11 @@ pcap_fd = BytesIO(b'''(1539191392.761779) vcan0 123#11223344 (1539191494.084177) vcan0 1F334455#1122334455667788 (1539191494.724228) vcan0 1F334455#1122334455667788 (1539191495.148182) vcan0 1F334455#1122334455667788 - (1539191495.563320) vcan0 1F334455#1122334455667788''') + (1539191495.563320) vcan0 1F334455#1122334455667788 + (1539191470.820239) vcan0 123##1112233445566778899aabbccddeeff + (1539191495.563320) vcan0 1F334455##1112233445566778899aabbccddeeff''') packets = [x for x in CandumpReader(pcap_fd)] -assert len(packets) == 9 +assert len(packets) == 11 assert packets[0].identifier == 0x123 assert packets[8].identifier == 0x1F334455 assert packets[8].flags == 0b100 @@ -165,6 +186,10 @@ assert packets[0].length == 4 assert packets[8].length == 8 assert packets[0].data == b'\x11\x22\x33\x44' assert packets[8].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' +assert packets[9].identifier == 0x123 +assert packets[10].identifier == 0x1F334455 +assert packets[9].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' +assert packets[10].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff' = Check rdcandump filter * interface filter 1 @@ -249,20 +274,27 @@ pcap_fd = BytesIO(b''' vcan0 1F334455 [8] 11 22 33 44 55 66 77 88 vcan0 1F3 [8] 11 22 33 44 55 66 77 88 vcan0 1F334455 [8] 11 22 33 44 55 66 77 88 vcan0 1F334455 [4] 11 22 33 44 - vcan0 1F3 [4] 11 22 33 44''') + vcan0 1F3 [4] 11 22 33 44 + vcan0 1F334455 [09] 11 22 33 44 55 66 77 88 99 + vcan0 1F3 [09] 11 22 33 44 55 66 77 88 99 + ''') packets = rdcandump(pcap_fd) -assert len(packets) == 8 +assert len(packets) == 10 packets[-1].show() -assert packets[-1].identifier == 0x1F3 +assert packets[-3].identifier == 0x1F3 assert packets[1].identifier == 0x1F3 assert packets[0].identifier == 0x1F334455 assert packets[0].flags == 0b100 -assert packets[-1].length == 4 +assert packets[-3].length == 4 assert packets[0].length == 8 assert packets[1].length == 8 -assert packets[-1].data == b'\x11\x22\x33\x44' +assert packets[-1].length == 9 +assert packets[8].length == 9 +assert packets[-3].data == b'\x11\x22\x33\x44' assert packets[0].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' assert packets[1].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' +assert packets[8].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99' +assert packets[-1].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99' = interface not log file format filtered 1 pcap_fd = BytesIO(b''' vcan0 1F334455 [8] 11 22 33 44 55 66 77 88 @@ -273,19 +305,24 @@ pcap_fd = BytesIO(b''' vcan0 1F334455 [8] 11 22 33 44 55 66 77 88 vcan1 1F334455 [8] 11 22 33 44 55 66 77 88 vcan1 1F334455 [4] 11 22 33 44 vcan0 1F3 [4] 11 22 33 44 + vcan0 1F334455 [09] 11 22 33 44 55 66 77 88 99 + vcan1 1F3 [09] 11 22 33 44 55 66 77 88 99 ''') packets = rdcandump(pcap_fd, interface="vcan0") -assert len(packets) == 4 -assert packets[-1].identifier == 0x1F3 +assert len(packets) == 5 +assert packets[-2].identifier == 0x1F3 assert packets[2].identifier == 0x1F3 assert packets[0].identifier == 0x1F334455 +assert packets[-1].identifier == 0x1F334455 assert packets[0].flags == 0b100 -assert packets[-1].length == 4 +assert packets[-2].length == 4 assert packets[0].length == 8 assert packets[2].length == 8 -assert packets[-1].data == b'\x11\x22\x33\x44' +assert packets[-1].length == 9 +assert packets[-2].data == b'\x11\x22\x33\x44' assert packets[0].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' assert packets[2].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' +assert packets[-1].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99' = interface not log file format filtered 2 @@ -385,9 +422,10 @@ pcap_fd = BytesIO(b'''(1539191392.761779) vcan0 123#11223344 (1539191494.084177) vcan0 00000055#1122334455667788 (1539191494.724228) vcan0 00000055#1122334455667788 (1539191495.148182) vcan0 00000055#1122334455667788 - (1539191495.563320) vcan0 00000055#1122334455667788''') + (1539191495.563320) vcan0 00000055#1122334455667788 + (1539191494.724228) vcan0 00000055##1112233445566778899''') packets = rdcandump(pcap_fd) -assert len(packets) == 9 +assert len(packets) == 10 assert packets[0].identifier == 0x123 assert packets[8].identifier == 0x55 assert packets[8].flags == 0b100 @@ -395,6 +433,10 @@ assert packets[0].length == 4 assert packets[8].length == 8 assert packets[0].data == b'\x11\x22\x33\x44' assert packets[8].data == b'\x11\x22\x33\x44\x55\x66\x77\x88' +assert packets[8].identifier == 0x55 +assert packets[8].flags == 0b100 +assert packets[9].length == 9 +assert packets[9].data == b'\x11\x22\x33\x44\x55\x66\x77\x88\x99' = interface not log file format @@ -1442,3 +1484,51 @@ nan = [x for x in li if math.isnan(x)] assert len(nan) >= 0 assert abs(len(gz) - len(lz)) < (testlen // 10) + ++ SECOC CANFD + += Load SecOC_CANFD + +load_contrib("automotive.autosar.secoc_canfd", globals_dict=globals()) + + += Test SecOC_CANFD build + +#SecOC_CANFD.register_secoc_protected_pdu(0x123) + +pkt = SecOC_CANFD(identifier=0x123, pdu_payload=bytes.fromhex("1122334455667788AABBCCDDEEFF0011")) +pkt.show2() +canfd = CANFD(bytes(pkt)) +canfd.show2() +pkt = SecOC_CANFD(bytes(pkt)) + +assert pkt.identifier == canfd.identifier +assert pkt.data == canfd.data +assert pkt.length == canfd.length + +SecOC_CANFD.register_secoc_protected_pdu(0x123) + +pkt = CANFD(identifier=0x123, data=bytes.fromhex("1122334455667788AABBCCDDEEFF001122334455")) +canfd = CANFD(bytes(pkt)) +canfd.show2() +pkt = SecOC_CANFD(bytes(pkt)) +pkt.show2() + +assert pkt.identifier == canfd.identifier +assert bytes(pkt.pdu_payload) == bytes(canfd.data)[:-4] +assert pkt.length == canfd.length +assert pkt.tfv == 0x22 +assert pkt.tmac == b"\x33\x44\x55" + +pkt.secoc_authenticate() + +assert pkt.tfv == 0 +assert pkt.tmac != b"\x33\x44\x55" + +if conf.crypto_valid: + from cryptography.hazmat.primitives import cmac + from cryptography.hazmat.primitives.ciphers import algorithms + c = cmac.CMAC(algorithms.AES128(b"\x00" * 16)) + c.update(bytes.fromhex("1122334455667788AABBCCDDEEFF0011") + bytes.fromhex("00000000")) + mac = c.finalize() + assert pkt.tmac == mac[:3] \ No newline at end of file diff --git a/test/scapy/layers/dcerpc.uts b/test/scapy/layers/dcerpc.uts index 984ec665455..e496b68719c 100644 --- a/test/scapy/layers/dcerpc.uts +++ b/test/scapy/layers/dcerpc.uts @@ -6,6 +6,9 @@ import re from scapy.layers.dcerpc import * from uuid import UUID +old_debug_dissector = conf.debug_dissector +conf.debug_dissector = 2 +True + Check EField @@ -66,8 +69,9 @@ f.addfield(None, b'', f.default) == hex_bytes('0123456789abcdef0123456789abcdef' pkt = DceRpc(b"\x05\x00\x00\x03\x10\x00\x00\x00\xcd\x00-\x00\x01\x00\x00\x00x\x00\x00\x00\x00\x00\x00\x00j\x87\xb4\xa8DrE3\xfa\xc1\x1d\x9e\xb7\x8a_\xffr\xbe\x13\xc4<\x85\xf0\xf2'y\x84t%u|e\xef/\x04\xb0m\x98\xb1\xd2\x00KwW#P\x8f2\xecB\x81\x19\xf3g\xd2o[\x07L-\xb8\x89\x05\xcf?\xcf\t\xeb\xb3&&6\xb7\x84\xb6\xcd8Ao\x8c\x94\xca\x03\xe3\x0e\x86'-\xfaHj\xcez\xf0A\x83\x9dX\r\xe8\x96\x07Bs\xaf\x9c[=2\x9eS\xb1\x18\x84 \xb4y\n9\xdf\x92\x1c\xd8\xe2e\xd3^,\t\x06\x08\x00pj\x8f\x04`+\x06\t*\x86H\x86\xf7\x12\x01\x02\x02\x02\x01\x11\x00\x10\x00\xff\xffp\xc0\\m\xfe\xa4\xe1!\xf7\xdf\xbf\xa4\xad\xdf\xcb\x16\x1e\xb5+{\x97\xaf\xd5~") assert pkt.auth_verifier.auth_type == 9 +pkt.show() assert pkt.auth_verifier.auth_value.MechType.oidname == 'Kerberos 5' -assert isinstance(pkt.auth_verifier.auth_value.innerContextToken, KRB5_GSS_Wrap_RFC1964) +assert isinstance(pkt.auth_verifier.auth_value.innerToken, KRB_InnerToken) assert DceRpc5Request in pkt assert pkt[DceRpc5Request].alloc_hint == 120 assert pkt[DceRpc5Request].opnum == 0 @@ -81,10 +85,10 @@ assert pkt[DceRpc5Request].opnum == 3 = Dissect DCE/RPC v5 Bind request with NETLOGON secure channel -pkt = DceRpc(b'\x05\x00\x0b\x07\x10\x00\x00\x00\xe4\x00<\x00\x02\x00\x00\x00\xd0\x16\xd0\x16\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x00\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x00\x00\x01\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x003\x05qq\xba\xbe7I\x83\x19\xb5\xdb\xef\x9c\xcc6\x01\x00\x00\x00\x02\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x00,\x1c\xb7l\x12\x98@E\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00D\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00APPS2019\x00APPS2019-RODC\x00\x08apps2019\x03lab\x00\rAPPS2019-RODC\x00') +pkt = DceRpc(b'\x05\x00\x0b\x07\x10\x00\x00\x00\xe4\x00(\x00\x02\x00\x00\x00\xd0\x16\xd0\x16\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x00\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x00\x00\x01\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x003\x05qq\xba\xbe7I\x83\x19\xb5\xdb\xef\x9c\xcc6\x01\x00\x00\x00\x02\x00\x01\x00xV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x00\x00,\x1c\xb7l\x12\x98@E\x03\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00D\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00DOMAIN\x00WIN1\x00\x06domain\x05local\x00\x04WIN1\x00') -assert pkt.auth_verifier.auth_value.NetbiosDomainName == b"APPS2019" -assert pkt.auth_verifier.auth_value.DnsDomainName == b"apps2019.lab." +assert pkt.auth_verifier.auth_value.NetbiosDomainName == b"DOMAIN" +assert pkt.auth_verifier.auth_value.DnsDomainName == b"domain.local." assert pkt.n_context_elem == 3 assert pkt[DceRpc5Bind].context_elem[0].transfer_syntaxes[0].sprintf("%if_uuid%") == 'NDR 2.0' @@ -102,35 +106,68 @@ assert pkt[DceRpc5BindAck].results[1].transfer_syntax.sprintf("%if_uuid%") == 'N pkt = DceRpc(b'\x05\x00\x02\x03\x10\x00\x00\x00\x98\x038\x00\x02\x00\x00\x004\x03\x00\x00\x01\x00\x00\x00\x88\xd6k\xac\xab^\xafqA^\xee\x8e\xce\x16\x86i\xe5A\xafK#\xeb%\'l\x88\xd4A\x0f\xa6>\xaf\xed\xf65\xf0\xf9\xf25\x89\xf5\xc5r\xe6;t\xf5\x80 \x80~\xf6\x0cRQ\x0b\xea\xc2}\x8a>\x08\xc9\x04\x9c\xdcOj\xa3\x0c\x82~\xfe\xa6\xa3\x01^ \xee\xd3\xd2yf\xfa\xfbL\xec&\x8b60\xb9\x83j\x84\xa0\xbc*G\xe25\x1a\r\xf3\xc8\xa6ib9\x87\xcbt%\x17\xf8g\x17\x1cIR\xd5\'wW\xbedZbXv\xb7\xe5?#$(\xae\x06\x9e\xce\xe1K\xd9\'\x9fG\xde\xff\xc9j\xd7\xa4\x04\xcb]-\xbcr\xb9+\xdax\xee\xa3\xce\x9c\x15\x0c/\xb2\xcb\xaaF\t\x07/AQM\x18t\xdc\xea\x019\x11TOy\xf7\x7f\xd1\x87\xc7m\xea>\x84Y\xc3\xef\xd0\xa6e\xb0g\xc3\x12\xd9\xc4~$\xb8\xfc/0\x86\x0e0\x8c`5lU\xd1\xbf8\xd2\xcb\xb1%\xfa\xfabr\x10\x9a\xf8\xb7\xb1\x01$wU\x17r\x03Z\xdc\xdd^\xecU\xc1\xf1\x87\xad\xa1\xea\xd8\xf2\x82\xa8\x95\xd4\xd2\xc6\x8e\xf1\xcfN1k\xdc\xc3\xf7o]q\'a\xa3Y\r97\xfe.8O\xf9\xa7\x93\xd3\x99?K\x8bv.\xac=t\r\xba\xca\xd0\x82\xd8\x81\xaf\xe6cv\xbe\xcbN\x93\x9d\x0e\xd4\x119d\x83/u\xc8\xb2\x1c/q\xf0"\xc4\x04\xadB\xe3N\xed\xbbR\xc4yO\x1fQ\xdd}\xd2\xe3c\x1e\xec\xc7\xc4\xf8\xf6OV\xe5\x00*\xb0\t\xbd\xf0\xe5j\xbf\xa3\xe0\x85\xa0\x81\xc6\xb96\xb9\xec\xd7I\x16_\xe7K\xb2D\xad\xb5\x7fG\xb9\x9by\xe2\xd9\xcf\xe7J\x83Y-\xa7:\xa3\x16\xe7\xce\xf9\xf5\xeb\x88z&Je\xcb\x94\'\xdc?\xbf\xed!\x1a\xb3sI\xb5o\x00\x8dJ\xd9\xed\x160+\x11nD\xd0QIo]A\xc0\x89\xa8\xb2\xc9\xb6\xc7,\xf0V\x8a\xae\xa6\x97\x8e\x91tO\x8c\x94\x08\xf1ru\x87e\x0bq6\x8aZ\xb9\xf3\xb7\xbb\xaf;\x89\xdf\x8b\xbf\tA\xef\xe3\x07\x0fT\xed\xbb\x072\x8eQ\xf4\xce\x194A\\w\xb4\x88\xff[\xcf\x91N\x1b\xfb\xe3\xcb~\xe9\xfc\x195\x0f&96\x05\x9a\xe4\xc0~\xd9\x0b\xfd\xbc\xc9\x8fTXY\x9f\xe4\x87e!\x93$$\x0b\xfc\xe7Jm8\x18\xb5\xad\xff\x85\xc3\xe2%\xd5{\x8bs\xa7\xb0\x1e\x0ei\xfc\xc2\x9d\x95\xd4\x83\xba"\x80\xee7^\xda\x02\x8b\x01\'\xe5e\x18\xa9}i\xbe\x86\xf4\x93\x9c\xe6\xe5\xf3\xd2\xa8\x8dH\\\x14\x89+yc\xa7kZ\x80\xe0\xb1\xc3\xd1\xa5\x8a9\xd9\xe7\x8d\xfd\x90\x04B\xce0\xeaK\xa1\xbc\xc1*\x8a\xfd*oX\xa0\x8b\x04D\xbc\x87\xacH\x97\x89\x85\xb2b\xf4F\xa2\xf1m\x06\xfe\x01\xd2\xcbT\x01+\x89<\x05q0ibL\x99[C\xeb\xcfx#i4\x8b\xbb\xb5ZP\x12?\x8b\xa5\x0e\x91"@aJ\t\t\x86\xa5*\t\xbf\x01Q\xa5\x85y\xad\xc0\xa7\xb2l5R\xd4\x85\xf4\xab\n\t\rJb\xf2\x875\xfcL\x16\xb0e\x17\xe1\xdc<\xd1\xee\x86\x01\xefHD\x1eb\xd1\xd1\xbby\xd41\xb7#\xef$DN\xda)\x8f\xb9\xffEa\xfe\xd8C\xb9\xff}\x85ra\xca\xec\xe1\xf6\x99\t\xa1\xc9H\x97\xd7\xc2\xa7\xbbW_\x1a\x92\xed\xb7\xde\xba*\r\x1e%h\xbdu)/\xd8m\xc0\xa9\xfb\xa1\xb5\xa3\xc3\x81\x18\xcd6\xd8t\x06\xa7\xd8\x84\xf5\x80\xb3\xaaX&\x8a\x7fPZ\x04\xcbsn.,b\xdfW\xd0\x7f\xc5\xc90 \x95S\x13*42R\x16fY\xeb\xd2\x05\xbd\x18Wm\xc0\xa1\x9dpYk\xaa\xd9\xd9+\x030\x9a\xe4IMlbfL\x81\xef[H]\xc6:\x88\x9cjE\x11\xce%\xd6\xe2<\x7f\xaaDO\x06\xaf\x13g&FX\x05\x90\xefl\x14\x12P;\xdc\xe7N\x0fU1C\xd1u#\xca\xf9\x12\xe6\xf7\x1bT\x17z\x97\xf2\xf5GH\xe3e\xbe\xe0\xeb?\xc2u\x9e#\x1c\xed\xcf7\x04c\x14\x90\xfc\x07\x1b\xedX\x1a\xd4\xbf\x96T\xee\xe7\x01^@\xcfSG\xd5\x899\x01\xf9\xc3\xf3(\xc2?^\xcd[,\xd85*\xdd\xab\xb6t\xc7p\xc4\xd3\x95\x9d\x02 \x9a^\x81\xb1.y\x9d\xc8\xe7\xb46\xfc\xc7,\x9fI\x03\\R\x83Y3+\xa7\x1f\x00\xd0\x16J\x10\x9a\xc5\'9)\xab\x93\x05\xd7\xb6\x12\xde \r\xc5b\x8bKo36\xfej\xa7\t\xd1{}a\x7f\xa4\xc3\xdc\xaaA\xe5\xe3\x91Uzw\xb2w\xee^\xcd\xd0i\xb7\xc0\xff`D\x06\x04\x00\x00\x00\x00\x00\x13\x00\x1a\x00\xff\xff\x00\x00\xb6\xb0D"\x11h\x92_\xe2 +\x06b%\x7f\xf5\x87O\x00\x08\x81\ro\xcd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') -assert len(pkt.auth_pad) == 4 assert pkt.auth_verifier.auth_pad_length == 4 -pkt.auth_pad = None pkt.auth_verifier.auth_pad_length = None +pkt.auth_padding = None pkt = DceRpc(bytes(pkt)) -assert len(pkt.auth_pad) == 4 assert pkt.auth_verifier.auth_pad_length == 4 += Build and dissect DCE/RPC with vt_trailer + +pkt = DceRpc(b'\x05\x00\x00\x83\x10\x00\x00\x00\x80\x00\x10\x00\x02\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00t\xc0\xd8\xcc\xe5\xd0@J\x92\xb4\xd0t\xfa\xa6\xba(\x8a\xe3\x13q\x02\xf46q\x01\x00\x04\x00\x01\x00\x00\x00\x02@(\x00t\xc0\xd8\xcc\xe5\xd0@J\x92\xb4\xd0t\xfa\xa6\xba(\x01\x00\x01\x00\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x00\x00\x00\x00\x00\x00\n\x05\x04\x00\x00\x00\x00\x00\x01\x00\x00\x00\xbe\x1a\xfd*\x9c\xd3R \x00\x00\x00\x00') + +assert pkt.auth_padding == b"\x00\x00\x00\x00" +assert len(pkt.vt_trailer.commands) == 2 +assert pkt.vt_trailer.commands[0].sprintf("%Command%") == "SEC_VT_COMMAND_BITMASK_1" +assert pkt.vt_trailer.commands[0].bits == 1 +assert pkt.vt_trailer.commands[1].sprintf("%Command%") == "SEC_VT_COMMAND_PCONTEXT" +assert pkt.vt_trailer.commands[1].InterfaceId == pkt[DceRpc5Request].object +assert pkt.vt_trailer.commands[1].Version == 0x10001 +assert DCE_RPC_TRANSFER_SYNTAXES[pkt.vt_trailer.commands[1].TransferSyntax] == "NDR 2.0" +assert pkt.vt_trailer.commands[1].TransferVersion == 2 + +pkt.auth_padding = None +pkt.auth_verifier.auth_pad_length = None +pkt = DceRpc(bytes(pkt)) +assert pkt.auth_padding == b"\x00\x00\x00\x00" +assert pkt.auth_verifier.auth_pad_length == 4 +assert pkt.vt_trailer.commands[1].TransferVersion == 2 + += Dissect DCE/RPC containing two fragments: Auth3 and a Request + +pkt = DceRpc(b'\x05\x00\x10\x07\x10\x00\x00\x00\xe2\x01\xc6\x01\x02\x00\x00\x00\xd0\x16\xd0\x16\n\x05\x00\x00\x00\x00\x00\x00NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00z\x00\x00\x00$\x01$\x01\x92\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x0c\x00\x0c\x00d\x00\x00\x00\n\x00\n\x00p\x00\x00\x00\x10\x00\x10\x00\xb6\x01\x00\x00\x15\x82\x88\xe2\n\x00aJ\x00\x00\x00\x0f\x857\xcfG\xcc\x98\x029\x01\n\xedc\x18\xea\xec\xc3D\x00O\x00M\x00A\x00I\x00N\x00W\x00I\x00N\x001\x000\x00$\x00W\x00I\x00N\x001\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\x00.\xa4\x829p_\xa8\xdc\x15+7+\xb4\x8d\x97~\x01\x01\x00\x00\x00\x00\x00\x00\xe0\x91\xd8\xa5\x91\x82\xd9\x01\xb8/\xcf\xac\t\x1c$\xb3\x00\x00\x00\x00\x02\x00\x0c\x00D\x00O\x00M\x00A\x00I\x00N\x00\x01\x00\n\x00W\x00I\x00N\x001\x000\x00\x04\x00\x18\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x03\x00$\x00W\x00I\x00N\x001\x000\x00.\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x05\x00\x18\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x07\x00\x08\x00\xe0\x91\xd8\xa5\x91\x82\xd9\x01\x06\x00\x04\x00\x06\x00\x00\x00\x08\x000\x000\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\x00\x00Z3!\xf8xx\x02\xa0\xcc\xcb\xa0\xbb|\xa5\x0c\xd3\x93Ib_\x8f\xa6j\xe1\x82\xd3\xec?\xaa\xae\x0e\x8a\n\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x12\x00C\x00I\x00F\x00S\x00/\x00t\x00r\x00u\x00c\x00\x00\x00\x00\x00\x00\x00\x00\x00!\xdc\xa8\xa5\x96\xd0k7\xdd\x84\xdb\x029\x1e+\x97\x05\x00\x00\x83\x10\x00\x00\x00\x80\x00\x10\x00\x02\x00\x00\x00<\x00\x00\x00\x00\x00\x00\x00t\xc0\xd8\xcc\xe5\xd0@J\x92\xb4\xd0t\xfa\xa6\xba(\x8a\xe3\x13q\x02\xf46q\x01\x00\x04\x00\x01\x00\x00\x00\x02@(\x00t\xc0\xd8\xcc\xe5\xd0@J\x92\xb4\xd0t\xfa\xa6\xba(\x01\x00\x01\x00\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x00\x00\x00\x00\x00\x00\n\x05\x04\x00\x00\x00\x00\x00\x01\x00\x00\x00/L\xb5\\\xfc\x83\xecF\x00\x00\x00\x00') +assert DceRpc5Auth3 in pkt +assert pkt.pad == b'\xd0\x16\xd0\x16' +assert pkt.auth_verifier.auth_value.UserName == "WIN10$" +assert pkt.auth_verifier.auth_value.NtChallengeResponse.getAv(9).Value == 'CIFS/truc' + +pkt2 = DceRpc(pkt[conf.padding_layer].load) +assert DceRpc5Request in pkt2 +assert conf.padding_layer not in pkt2 +assert pkt2.vt_trailer.commands[1].InterfaceId == pkt2.object + + Check DCE/RPC 4 layer -= DCE/RPC default values += DCE/RPC 4 default values assert bytes(DceRpc4()) == b'\x04\x00\x00\x00\x10\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\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00' -= DCE/RPC payload length computation += DCE/RPC 4: payload length computation assert bytes(DceRpc4() / b'\x00\x01\x02\x03') == b'\x04\x00\x00\x00\x10\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\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\x04\x00\x00\x00\x00\x00\x00\x01\x02\x03' -= DCE/RPC Guess payload class fallback with no possible payload += DCE/RPC 4: 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 += DCE/RPC 4: 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.layers.dcerpc import *; import binascii, re class DummyPayload(Packet): @@ -146,20 +183,20 @@ DceRpc4Payload.register_possible_payload(DummyPayload) p = DceRpc(hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000ffffffff00040000000001020304')) p.payload.__class__ == DummyPayload -= DCE/RPC Guess payload class fallback with possible payload classes += DCE/RPC 4: Guess payload class fallback with possible payload classes p = DceRpc(hex_bytes('04000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000ffffffff00040000000000010203')) p.payload.__class__ == conf.raw_layer -= DCE/RPC little-endian build += DCE/RPC 4: little-endian build bytes(DceRpc4(ptype='response', endian='little', opnum=3) / b'\x00\x01\x02\x03') == hex_bytes('04020000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000300ffffffff04000000000000010203') -= DCE/RPC little-endian dissection += DCE/RPC 4: little-endian dissection p = DceRpc(hex_bytes('04020000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000300ffffffff04000000000000010203')) p.ptype == 2 and p.opnum == 3 and p.len == 4 + NDR tests -= Create NDR Packet += DCE/RPC 5 NDR: Create NDR Packet # from [MS-SRVS] class LPSHARE_INFO_1(NDRPacket): @@ -174,29 +211,31 @@ class LPSHARE_INFO_1(NDRPacket): ), ] -= Check user friendliness += DCE/RPC 5 NDR: Check user friendliness -pkt = LPSHARE_INFO_1(shi1_netname=b"ADMIN1$") +pkt = LPSHARE_INFO_1(shi1_netname="ADMIN1$", ndr64=True) val = pkt.fields['shi1_netname'] assert isinstance(val, NDRPointer) assert isinstance(val.value, NDRConformantArray) assert isinstance(val.value.value[0], NDRVaryingArray) assert val.value.value[0].value == b"ADMIN1$" +assert pkt.valueof("shi1_netname") == b"ADMIN1$" -= Try building it += DCE/RPC 5 NDR: Try building it assert bytes(pkt) == b'\x00\x00\x02\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00A\x00D\x00M\x00I\x00N\x001\x00$\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' -= Re-dissect -z = LPSHARE_INFO_1(bytes(pkt)) += DCE/RPC 5 NDR: Re-dissect +z = LPSHARE_INFO_1(bytes(pkt), ndr64=True) val = z.fields['shi1_netname'] assert val.value.max_count == 8 assert val.value.value[0].actual_count == 8 assert val.value.value[0].value == b"ADMIN1$" +assert z.valueof("shi1_netname") == b"ADMIN1$" -= Same thing with NDR32 += DCE/RPC 5 NDR: Same thing with NDR32 -pkt = LPSHARE_INFO_1(shi1_netname=b"ADMIN1$", ndr64=False) +pkt = LPSHARE_INFO_1(shi1_netname="ADMIN1$", ndr64=False) assert bytes(pkt) == b'\x00\x00\x02\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00A\x00D\x00M\x00I\x00N\x001\x00$\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' z = LPSHARE_INFO_1(bytes(pkt), ndr64=False) @@ -204,18 +243,19 @@ val = z.fields['shi1_netname'] assert val.value.max_count == 8 assert val.value.value[0].actual_count == 8 assert val.value.value[0].value == b"ADMIN1$" +assert z.valueof("shi1_netname") == b"ADMIN1$" + Real tests on complex packets -= Define structs += DCE/RPC 5 NDR: Define structs # From [MS-WKST] class LPWKSTA_USER_INFO_0(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullPointerField( - NDRConfVarStrNullFieldUtf16("wkui0_username", ""), deferred=True + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("wkui0_username", "") ) ] @@ -224,14 +264,13 @@ class LPWKSTA_USER_INFO_0_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", 0), - NDRFullPointerField( + NDRFullEmbPointerField( NDRConfPacketListField( "Buffer", [LPWKSTA_USER_INFO_0()], LPWKSTA_USER_INFO_0, count_from=lambda pkt: pkt.EntriesRead, ), - deferred=True, ), ] @@ -239,17 +278,17 @@ class LPWKSTA_USER_INFO_0_CONTAINER(NDRPacket): class LPWKSTA_USER_INFO_1(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ - NDRFullPointerField( - NDRConfVarStrNullFieldUtf16("wkui1_username", ""), deferred=True + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("wkui1_username", "") ), - NDRFullPointerField( - NDRConfVarStrNullFieldUtf16("wkui1_logon_domain", ""), deferred=True + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("wkui1_logon_domain", "") ), - NDRFullPointerField( - NDRConfVarStrNullFieldUtf16("wkui1_oth_domains", ""), deferred=True + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("wkui1_oth_domains", "") ), - NDRFullPointerField( - NDRConfVarStrNullFieldUtf16("wkui1_logon_server", ""), deferred=True + NDRFullEmbPointerField( + NDRConfVarStrNullFieldUtf16("wkui1_logon_server", "") ), ] @@ -258,14 +297,13 @@ class LPWKSTA_USER_INFO_1_CONTAINER(NDRPacket): ALIGNMENT = (4, 8) fields_desc = [ NDRIntField("EntriesRead", 0), - NDRFullPointerField( + NDRFullEmbPointerField( NDRConfPacketListField( "Buffer", [LPWKSTA_USER_INFO_1()], LPWKSTA_USER_INFO_1, count_from=lambda pkt: pkt.EntriesRead, - ), - deferred=True, + ) ), ] @@ -277,13 +315,12 @@ class LPWKSTA_USER_ENUM_STRUCT(NDRPacket): NDRUnionField( [ ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( "WkstaUserInfo", LPWKSTA_USER_INFO_0_CONTAINER(), LPWKSTA_USER_INFO_0_CONTAINER, ), - deferred=True, ), ( (lambda pkt: getattr(pkt, "Level", None) == 0), @@ -291,13 +328,12 @@ class LPWKSTA_USER_ENUM_STRUCT(NDRPacket): ), ), ( - NDRFullPointerField( + NDRFullEmbPointerField( NDRPacketField( "WkstaUserInfo", LPWKSTA_USER_INFO_1_CONTAINER(), LPWKSTA_USER_INFO_1_CONTAINER, ), - deferred=True, ), ( (lambda pkt: getattr(pkt, "Level", None) == 1), @@ -307,7 +343,7 @@ class LPWKSTA_USER_ENUM_STRUCT(NDRPacket): ], StrFixedLenField("WkstaUserInfo", "", length=0), align=(4, 8), - switch_fmt=("\x8e ~\x8d\xbb\xfb\xef\x9d3\xb3\x8bv\xf7\xdf\xdei\xfa\xa5\xf1\xb7\x86\x14-\x9a\x16\x92\x88;\xcbH\xbd\x0c\xa0\x97\x05\xf4r\x11\xfae\xd7\xd8\x8d\xdb&\xad\xb4,\xadS\xf2W\xf0\xbf\xcb\x03z\x05@\xaf\x18\xa1\x1f<\xd2}1\xf3]\xaey\xd4\xab\x7f\xaf:\xfd\xcd\x8d\xb1\x95\x00=\x1e\xd0+\x03z\x15@\xaf\n\xe8\xd5\x00\xbd:\xa0\'\x00z"\xa0\xd7\x88\xd0#?5\x01\xbd\x16\xa0?\x02\xe8\xb5\x01\xfdQ@\xaf\x03\xe8u\x01\xbd\x1e\xa0\xd7\x8f\xd0\x85\x1fz\x9cZ^\xbfB\xca\x8c\xd9M.n/\x93F=\x06\xe8\r\x00\xbd!\xa07\x02\xf4\xc6\x80\xae\x03\xf4\xc7\x01\xfd\t >M\x00\xbd)\xa07\x03\xf4\xe6\x80\xde\x02\xd0[\x02\xfa\x93\x80\xde*B\x9f\xf0\xfa\xa7/\xf6\xcd\xca\xb3mjt\xb8h\xe5\x99\x94\x06O\x01zk@O\x02\xf46\x80\xfe4\xa0\x93\x80\xae\x07\xb6\x9f\x02t\x1a\xd0\x19@g\x01\x9d\x03t\x03\xa0\xf3\x80n\x04\xf4\xb6\x80\xde\x0e\xd0\xdb\x03\xfa3\x80\xde\x01\xd0\x9f\x05\xf4\x8e\x80n\x02t3\xa0[\x00\xdd\n\xe86@\xb7\x03z2\xa0w\x02\xf4\x14@\xef\x0c\xe8\xa9\x80\x9e\x06\xe8\xe9\x80\x9e\x01\xe8\x99\x80\x9e\x05\xe8]\x00=\x1b\xd0s\x00\xbd+\xa0w\x03\xf4\xee\x80\xde\x03\xd0{\x02z/@\xef\r\xe8\xcf\x01z\x1f@\x7f\x1e\xd0\xfb\x02z?@\xef\x0f\xe8\x02\xa0;\x00]\x04t\t\xd0e@w\x02z.\xa0\x0f\x00t\x17\xa0\x0f\x04\xf4<@\xcf\x07\xf4\x02@/\x04t7\xa0{\x00}\x10\xa0{\x01\xdd\x07\xe8~@\x1f\x0c\xe8E\x80>\x04\xd0\x87\x02\xfa0@\x1f\x0e\xe8/D\xe8~\xd9[\xe0\xf3\x16\xfd\xa3\xbf\x18Z\x06\xcf\x93R\n<\xf9:\xa7\xd7%\x17J\xf9\xc3t\x85B\x81|\xdf\xf9l\xdcK\xc0\xf7\x8d\x08-\x83\xc7\xa5\x19\xb2\x7f\x88\xdb\x9b\xa7\xb3\xb8\x0b\x0be\xd1\xefr\x17\xea\xcc^w\x9e\xec\xd5\xf9doQ`\x11\xf8"\x8f\xdbU\xe8\x7f\x00g\xa4F\x9c\x975\xe2\x8c\xd2\x88S\x1cZ\x06\xcf\x1bSMY)\xba\x9c\xc0*.Q\x8e\xb5\xceh\x8cuJ0\xd6\x19\x83\xb1\xce\xd8\xd0\xf2Y\x94\xb8\xe8\x9cn\xaf.\xc3b\xd6e\xcb>\xd9\xaf+pK\x83\xf3\xe5\xfb\xd9\xafh\xc8~o\xde\xc7\xb7*|].e\xf4\xd6\xd5\xc3[Lm\xef\x19\x17bG\x1b\xc7\xe3\x01}B\x84~\xe5\xe4\x87\xdbxs\x15\xeb\xae\xa5\xf3\x96\x14Oi\xf0\xd7D`\xfdI\xa1ep\xfe!#;\x0b\x1c9\x93\xef\xf5\xe7\xa4\x80\xfeWC\xcb\xe0\xfc\x8a\xadH\x0e\xc4%\xdf\x9d\xab\xebj\xc9J\xc9\xba\xcf;\x05\xc1;\x15\xc1;-\xb4\x0c\xce1X;Y\xb2\x8a8\x9d%\xdf\x15\\\'-;\xcb\xa2\xb3Ek\xfbt\xcc\xf5f\x84\x96\xf5C\xebE[\xeb\xfd?\x8e7\xddP\xd42s\xe1\xd1\x91\xf9o\xd9\xc7\xc6\xcd\x0c\xad\x17\x9c+I\xf6\xba\x07{tY\xee|\x978L\x17\\/\xa50\x902\x9d\x82\x18\x18C\xb9\x1e\xb1\x14x\xe7\xfbf\x85\x96\xd1r\xa7\xc7\xebv\xba\xf2\xe5;%\x88\x98\r\xf8}r\xe1?\xde\xe0g\x0e\xa2\x7f.\xe0\x8f\xfc\xcc\x0b-\xa3\x8d\xd1\xf9\x80\xbe\x00\xd0\x17\x02\xfa"@_\x0c\xe8K\x00})\xa0/\x03\xf4\xe5\x80\xbe\x02\xd0_\x03\xf4\x95\x80\xfe:\xa0\xaf\x02\xf4\xd5\x80\xbe\x06\xd0\xd7\x02\xfa:@\x7f#\xb4\x0c\xeeW=\\\x85\x9d\xfc~\x8f\xce4\xd8\xefN\xca\xf2\xba\x87\x0e\xbbS]\xee_\xefM\xcc\xf5\xd6\x87\x96\xc19\xefn\x81\x04\x99\x9e\xeb\rd\x80\xfb}\x1b\x14\xfa6*\xf4mR\xe8\xdb\x1cZ\x06\xe7\xfc\xedCt\x81\n\xeb\xbb\xcf\xb3E\x81g\xab\x02\xcf6\x05\x9e\xb7\x14x\xdeV\xe0\xd9\xae\xc0\xb3C\x81g\xa7\x02\xcf\xae\xd028/n\x16|\xb2\xce\xee\xf2\xcaC\x84\xfc\xfc@\x82\xcfu\x15\xca\xc1\xb5\xee[)\xf0\xd9\x1dZ\x06\xafWX\x9d>\xab\xaf\xb4\x82>p\x18\x11{\x10\xbc{C\xcb\xe0\x0e-\x83\xd7/n\xa7\xcdt\xa1P\xc8\r\x1c2\xde>\x86Qp\xaaB\x1c\xd7\x80\xf1\x89\x06\x8c\x13\xa1e\x8b\xfb\x18\x81#\xac"\x97\xa4\xe4\xc4\xebS\r\x18\'5`|vO\r\xd0\xa7\x87\x84\xd2\xfb\xc2\xd2\xfb\xf5p\x15J\xee!\xa1K\xf0\xd9Y\x96>\xd1\xee\x96\x99\x01pg\x02\xfa\xac\x90\x10\xbc\xe7\xa2\xbb\xa70\xea=\x17\xb3\x15\xfa\xe6(\xf4\xcd\x8d\xe2s\xaei\x917zz\xcf\xb4U\xe3\xc7\xfe\xfdJ\xe2\xce\xc5\xf3"\xda\x1f\xa9\xcf\x8f\xd0OU\x95\xcc\xcf\xb3\xc6\xcc\x05sV\xd1S+V\x18\xbf B\xb7\x97;;|\x89\xef-[\xf1\x91W\xe6\x0cJ\x1c;la\x84\xce2\xba\xdcn\xee\xda\xb6\x89SV1\x1b\x9b?\xfa\xc8" ~\x8b\x01}\t\xa0/\x05\xf4e\x80\xbe\x1c\xd0WD\xe8K{\x9dj\xc7}nM\x9f\xea\x7f\'xi4\xee5`\xfd\x95\x80\xfe:\xa0\xaf\x02\xf4\xd5\x80\xbe&B\xaf\xf8\xd3;\xcf\xfd\xf6[\xbfN\x8bn\xb9?~\xe3\xfa\xf2\xf6kCB\xf0Y~\xbb\xd7G\xdd{\xe1,\xd2\xbb.\x867\xf2\xf3\x06\xd0\xae7\x01}}H\x08^\x1f\xc9\x12\x85\x9c"\xf1\xf6\xf3\x8a\xc1\xbf\xf2\x81\x8c]\xcd\xf3W\xef/O\xbe\xde\xcb\\\xdc\xa6u\xc9W\rOU\x8b#\x82\xaf{\x08\x08\xe5\x9e\x1f7\xe1\x87\x06\x87j,\xb9X\x89x*\xb1S\xff2\xa5B\x1cQ\xe5\xf6\xa2b\x19\xe2@`\x11_\x8e\xd8\xbf\x9a\x90J\x9f1\x0c\xfe\x95\xc3bV\xbd\xbdHL$n\'\x1c_\xce\x80\xc1\xfe\xc0\xb2\xb0\xf4\xd9\xb7\xbej\xdb\x9aP\x8b\xe8\x93\x95\x92e\xeb\x93R\xe8\xf2\xff\x83\x8e#jT \xfa\xf4\xb1Z\x82\xcf?g\x86\xfe\xd4\xb5?!\x903}\xb9\xa9\xd9\x1e\x914P\x06\x03I\x84\xc7\xe5\xdd\xe6?\x17=\xf2c3\xcb\x84y\x03\xedLI\xdd\rZ\xc5\x05\x99\x8b\x19\x17\xec\xf6\xdf\x1f\x97\xe03\xb9\x9d\xeep\xb7\xfd{\xc1\xc7\x83\x04\xa6\xf3\xbes\x15n\x14\xcb\xcd\x06(\xe6V"D\x9f\xec\xf0\x0cv\x94>\xe3\xd9S5\xaf.\x11|\x8e!\xc9\xc1\xc92/K\x94\x91eX\x916:EN[\xbe\xec4R\x06=\xe948E\x07\xc5\x18)J\xd0\x96op2<\'\x91F\xbdL3\x06R\xefp\x8a\xda\xf2Y\x92\xe2\x1c\x92\xc0\x93\x1c-R$GQFm\xf9zFt\xb2\x06\xd6\xe1`$Jp\xf2\xa2\x91)}\x86\xb5\xbfj\xbe\x8e\xc8L\xb3\xd9(\x1bI[\xf4\x9c\x89\xb2\x1b\xf5\xa4\xc1\xce\x1b,,e\xd2\xdb\xedv\x9e\xd7v;H#gt\xb2N\'\xa9\xa7\x05#\xc7\x8b4\xd6\xd5\xf2\xa1\xb1\xae\x96\x0f\x8dul>\xe2XW\xbb\x1d\xd0X\xc7\xe6\xc7\x18\xeb\xd8\xcc\xffQ^\x7f\x98w\x1f\xe6]\x15y7\x8c\xf7X\xc5^gjS\xbd\xd2JF\xcdi\xf3\xd5\xe9\x84\x81jy/\xc4%Wk\xb7\xe5M\xfb\xe2\xba\x7f\xcd\xb6\xe5g\rU\xcb\xab\xe2\xee8\xcb\xb8\xb9\xaee\xda\xae\x8fV\xbc\xe6\xbezZ-\xef\xc3u_d\x9c\xbb6\xd12\xb7\xf1\xae\xea\x13v\xc7UU\xcb\xb3\xff~-aW\xad\xca\x1d\xd7e\xcd\x1f\xfa\xf3\xdb\xab{\xab\xe5\x15.-v\x9d\xeb\xb7\xc9\xbee\xe1\xf2\x83\xb6}K]jy\x83\x98V\x95\xde\xd2\xb72\xef\xd0\xaf\xb4\xc6\xcd\xae\xeaP\xcb\xdb\xc8\x9d\xbf!m\x7f\xa5\xf3\xe2\xaf\x9e\x1c\x93|\xb4\xf8g\xb5\xbc\xc33\x9f\xf9V\xf2\x1b;m\xb4\xc6/\xbf9\xb1\xed\xb7jy{K&\xd7\x996}fJ\xf1{\xcfN\x9d\x7f\xb8wY\xb5<\xe7\xa57\xd2-\xcb\xb3\xac\xeb\x9f]\xf0\xc2\x91\x9c\xda\xdb\xcb\xa8\xe4\xfd\xbe\x9b\xaa\xbb\xfd\xf0\xa7\xa61\x9f\x1c2U\x94\xfeP=^\x8e\xddx\xfc\xe0\x9a\xf8\x83\xe6M\xdd\xbc\xfc\x98YG\xae\x12*y\x1f}9\xb2u\x85\x0b\x1dS\xb7\xd6\x9c\xa5\xafxe\xc1F\x0c^X\x8e}\xf7\xc8\xbaq\xdf3\xeb\x8b\x8ak\x17\r\xd6\xc9\xbd\x9b(\xe6)\xacEj\xf9P-\xc2\xe6#\xd6"\xb5\xdb\x01\xd5"l~\x8cZ\x84\xcd\x8c2Vv\xd79P\xb4\xf5\x838\xf3*\x9b\xa1\xed\x97e\x9f\xaa\xaf\xf5q\x8bZ>4V\xb0\xf9\x88cE\xedv@c\x05\x9b\x1fc\xac`3\xa3\x8c\x15j\xc7w\xdd\x86\xd5\xfb\xcc6\xf5\xe0H\x87\xc7k\xaa\xa9\xf5XQ\xcb\x87\xc6\n6\x1fq\xac\xa8\xdd\x0eh\xac`\xf3c\x8c\x15lf\x94\xb1\xb2w\xd3\xc81\x99\x95\xb6en[\x98\xda:\xf5\xdb\xab\xc7\xb4\xeeKl>b_\xaa\xdd\x0e\xa8/\xb1\xf91\xfa\x12\x9b\x19\xa5/7\x9c:\xbc=\xf5\xe2\x0e{\xc9\xcf\xeeA\xfb\x9f]\xdcM\xeb\xbe\xc4\xe6#\xf6\xa5\xda\xed\x80\xfa\x12\x9b\x1f\xa3/\xb1\x99Q\xfarV\x99\x7f\xe9vN\xfe\xae\xd3\xfa}\x1d\x86^i\xb7\xc9\xaau_b\xf3\x11\xfbR\xedv@}\x89\xcd\x8f\xd1\x97\xd8\xcch9vw\xadf\x9f\xe6w\xb2\x8c\xe9;\xe2f{\xba\xe0\x84\xe69\x16\x97\x8f\x9acUn\x07\x98cq\xf9\xb1r,.3J_\xb2-:T[\xfa\xd1\x87\x19k\xce\x99\x96\x7fQ\xff\xa7\x0c\xadc\x80\xcd\x8f\x11\x03lf\x94\x18\xdc\xbc^\xfe\xbd\x15W[e,\xf5\xf9\xba\'L^\xdcA\xeb\x18`\xf3c\xc4\x00\x9b\x19%\x06\xd3\x0e\x1b\x1e\xdbT\xd47s\xc5\xfb\xad\xf6\xbd\xd7\xf8\xa9\x8dZ\xc7\x00\x9b\x1f#\x06\xd8\xcc(1\xf8\xa6\xdd\x7f\'^)H\xca\xd8\xe0\x7f1\xf9h\xe5\x7fw\xd6:\x06\xd8\xfc\x181\xc0fF\x89A\xc7Us\x93\x1a\xf6\xdcaZ[m\xf5\xc4E}.\xf1Z\xc7\x00\x9b\x1f#\x06\xd8\xcc(\xf3b3\xd7\x0be\x1f-\x9b\x9f6\xe1\x8b\x833\r=\x89sjy\xedF7\xedY\xbf\xfd\'\x9d\xf7\xdcz\xb7a\xbds\x1dp\xee\x15\x08\xe3y\xe2\xd7\xeeig\xea\xd0y\xf1\xf3\xbf\x9f^4\xa2b\x7f\xb5\xbc\xf1\x93\xe9v7\xf2\xb7[KF\xe4\xd4\xd6\xef\x9e7]-o\xc7\x89\xe2W\xf6\xfc`\xb5\xaf\xf8\xe2\xa27g\xd4\xb4\x05jyL\xd9\xe7*u=?-}\xc1$\x13s\xfd\xc2\x8c_\xd4\xf2\xb6\xe7\xfd\x98z`\x9b;u\xf3;\xcf\xaf<6\xfd\xe3/U_\xe3+x\xe3\xd0\xdb\xc5\xdd2\xd6O8/\xfd\xf4\x82y\x08\xf2\xf5l\x03O\xd12)\x89N\x92b$F\x14\xc4\xff\xc1\xbd\x95\xacE\xaf\x8fhw\x8f\x17\x12\xce7\x1b~"m\x19;\xf2\xa5g\xae\x1f\xcfCm7\xcf3NQ\x12DR&I\x9a\xd4\x0b\x066\xfc8\x0e\x9b_z\x1c\xc7\x1a\r\xbc\x9e4Q6\x83\x9d\xb2\x9bi\x13G\xf2&\x83\x81d9\x96\xa4\xb4\xdd\x0e\x87\x18\x8c?\xcd\xf1\x0e\x86%e\x9a\xa7Hm\xf9\xfa@\xb7\x8a,K\xb1\x94\x9118\x1c\x923\xe2\x9e\xd4\xbe\'\x13\xc6\xed\xb0\x9c\xad\xb1i\xf5\xb1\x8c\xaf?\xd7\x8d\xd1\xba\x1f\xb0\xf9\x88\xfd\xa0v;\xa0~P\xcb\x87\xfa\xa1q\xf1\x13\xd9\xe7N\x8e\xe9Tr\xad\x9f0f\xe9\x8f&\xad\xdb\xaf\x96\x0f\xb5_\xf5\xb5z\x80\xff\xde\xa7\xc4\xca\xe3\xd5,\xb6\xddUO^g\xc6~\xff\'\xfa\xb1A\xe0\xc4\xcf)\xf2\x92\x93"I\xc9\xc8\x90Fm\xf92\'3\x12-\x08\xa4\xd1ht\xca\x02\xad\xd7\xf8\x1e8\x88_\xf5\xe0\x9b\xe4\xee>\x1f\xd9\xe7W\x9f6-y\xfc\xb9\xf6\xa8|\'\xc3\x1aE\x07\xcd\xf3N\x83\x81rH")\x85\xf3\x8fUY5bC\xa7\x81\x99\xe3\x07\x0c\xb4V\xecz\xf2\x06r\xffR\x82(r\xa4^b\x04\')\xcb\x81\x7f\x87?\xef\xf1\xc7\x86\xbd\xbf\xd7k\xb4+s\xc9\x94\xed\x8buu\xe9\x17\x95>+\x10|\xde#\xee\xee\xf3\x1e\xc1\xdfD\xea\xad\x96\x99\x90@\xf4\xf1\xb8\xac\xfd\x1b\xb3_5\xac\xd9\x7f\xacFq\x12\xb1\x96\xe8r>A\xeb\xb1\x83\xccGl\x7fq\x85?\x1aw\xa9|\x91\xacX\xe0\xafr\xf1\xa3K\x13\xb5n?2\x1fq\xec7\xb8\xe2\xaf\xfc\x9a\xa9B\xc6\xe8\xf5\xa7\xfd7\x1b\x9c\xc9\xd1\x80\x1f|HZ\xbe\xc3O?\xe3c\xc6\xb5\xa9\xd0q\xe1\xce\'z\xc4\xc7\x9fI,\x1f\xc0\xc4)\xe17#H\xd9IR2\xcf\'\xc9\x0e\x9aNb\x18=\x97\xc4S\x14\x95\x148\xb3&\r$\xed\x08\xeck\xc2}\xc7L\xd8\xdfw\xe7\xdcHp\xf2\x1c-\x88\xce\xc0\x169dJd\xd8p\xbend\xfbj\x93\x1bt\xb5/\xdb\xd2\xa5{\xbb\'\'~\x85|\xcc\xc4p\x06\x03\xc712\xeb`\x03Gd\x8c\xde\x18>G\x80\xcd\xbf}\xcd\xcd\xc22\x16\xcefb\xad\xac\xd1`dm\xa4EO\x1b\xf5\xb4IO[#\x9e\xe9{k\xe7\x94M\x7fv/\x93\\r\xd6T\x9e\x9a\xb1Z\xf9=\xaf\n\xb7\x03\x9b\x8f\xb8\x1d\xc5\xcdw<=O>m^_kO\xeb\xd5\x0b\x8e7@\xae\x9bFY\x12D\x81!y\xde@\xd3\xbc\xc09\xb5\xe5;\x04\x87@\n\xb23p$\xaeg\xf5\xa4\xc0;\xb4\xe5\x8b\x0c/\xd1\x0c\xcbI\xa4\x9ebX)\xb01\xda\xf2\xe9 \x94"%\xde\x118\xc6\xd7\x1beg\xe4\xbd\xa0\xb4\xff\xd7E\x1dz\x98\xd6~:zm\xe3>IiZ\xc7G-\x1f\x8a\x8fZ>\x14\x9f}5\x16\x8d\xdc12\xcf\xb2\xba\xe6\xe6\x93U\x9fOS~o\xb4\xc2\xf6\xab\xe5C\xedoV.\xe5L\xb3\x1c\xd9\xf6\xb6\x7fD\x8b\xcf\xf6/l\xaa!\xbf\xb4~\xfee\xa95rMV\x8b\x94UC\xab\xf6\xec\xd5\xe5\x857\x14\xd7\xcfj\xa1\xfa9$\xcf\x17Q@\xc3\xda\x8f\xcc\x8f\x8c\xbf^\x96\x1c<\x1f8\xbbu\n\xc1\x19\'*"\xffT\xab_\xe9\x87\xcb\x89\x13\x92\x97\x9d\xc9\x9a\x97=\xfa\xc2\xea\xb2\x1a\xf3\xe3\xb7\x1e\xfa\xf9\xd0;\x84i\xac\xf1\x8bu\x8f\x8cu!_\x0b\x85\xf8g\xb6\x1d?0\xeaF\x7f\xeb\xa2\xcf.\xc8\x13\xc7\xd8R\x91\xf9\xb2\xc8K\x0e\x87S\xa6(\x89\x97E\x83\x1cQ\x07\xb0\xf9\xa5u\xc0L\x19\x18\xbd\x9e\xe2\xf4V\x0be5\x18H\xd2\xa6\xe7X\xab\x857\xd9-\x11\xe3t\x83#\xe3\xd7M_\x1d\xb0\xbe\\\xab\xdc\xfcE\xe7\xeb(\x9f\xd3Q\xb8\x1d\xd8|\xc4\xed(\x9f\xfb\xc3\xf0\xef\xda\xd6O\x1e\x97\xf4\xcb\xc2\xc4\xc5\xa7\x91\xef\x1d\x85\xb6\x03\x9b\x8f\xb8\x1d\x8c\xf0\xc7\x85\xdf\x16\xa7\xa4,\xff\x8c\x1e\xf0a\xfc8\xe5\xd7\n\x15n\x076\x1fq;*O\x1d\xb9\xe5\xfc\xf7\xe5,\xcb\xd7\xd5\xdc\xe5\x9c\x147N\xeb\xed\xc0\xe6#n\xc7\x8a\xfc\x9b\xeb\xfaVe\x92\xa7\x8f\xa9|\xd9\xfb\xd7d\xf4\xe3\x0c`;\xb0\xf9\xf0v\x84}\x8f\xa7\xf7\xfc\x7f\x8d\x9c\xe0\xc9\xd8\xf9h\xa3)\x8d\xebu\xb8\x86\xf6=V+\xc3\xf1\xa4\xd9\xcc[\xadz\xce@2F\x93\xc1\x1c8\xb0\xd4\x9b)J\x1f>w\x89\xfd=\xd5\x89\xae\xb2W\x96\xdc\x16w\xa1\xdf\xeb\x0e\xde#\x91\x1d\xfaS\xc7\xad\x19\xe2Z]Bn\xa1\xdb\xe7w\x89>"\xe2\xdc\x07\x9b}\xe7\xba\x87\xcc\xd0\x06\x9ee\x9d\x92\xa4g\x04V\x16\xc5\xf0\x98\x0c9\x9b\xf7n\xea7\x972\'\xec\xbe\xd8\xfdJf9\xe5\xe7\xa2@L\xb0\xb9\nb\x82\xcdV\x18\x93\xcdl\xff6\xd7.\xae2-\xb5\xcay\x96\x85i\x8cV1\xc1\xe6*\x88\t6;zL\xc2\xf8\x03+&u{\xf2\x885c\xe2\xc0\xe3}{\x9d\xdb\xaf\xfc>\x89\xe8\xfc\xb0\xf9\xd7\xf5-\x96wy\xae\xf8g\xd3\xd65\xf5\x0fu\x1b\x9eX\x17e\xfe\xb5\xe6\xdd\xf9\xd7\xb0~Df\xde\xed\xc7\x0c[\xd7\xb4\xcc\xe4\xcc\x8c~\xc1\xc6\x13\x84vm\xad|\xb7\xad\x8e\xc0\xfft\xa8e&\xd4\t]\xcb\xd3;8Q\x96\xf4F#+\xc8\x82\x931\x84M?i\xd3\xfe\xc6\x81\xf6\xc7\xdfm\x7f\xd8\xb52\xecX\xc7\x13\x19]\xad9\xfd\xd2J\xc3\x1c^\x17\xb0\x99\xa5u\xc1nd\xecf\xc6h\xb0\x99\x98@\x0cL\xb4\xd1\xce\xf06\xcaH2v\xce\xa6]\x7f\x96\xbd\x1b\x8f\xb0k\x9c\xd8m\xafL\xf8\x84\x02\x9fO\x97\xef\t^<\xbc3/\xa9\x8eY\x87\xc8qI\xc1\x9f\x15Ks\x8bB\xf0\xe7\x84%]\xd6\xed_\xe0\x0c\xcbO\xd8\xfc\x9a\xc1\xdf\x83\xf7\xcb\xa2_\x96\xfa\xf9\xfcn\xaf\x90+\xdf3\xe6\xd4\xb1\xab\x10\xf9>\xc1\xe7\xf3\xb8\xbc\xc1\x80\x84\xbd\xd7\x0f\x9b\x99\x18dz\xdc\xf9.qX\xbe\xdb\x9d7\xd8Ct\t\x08]Tsk\x10i9\xa6~6SN?[\x865+3%#x\xb9\x80H\x0e\xfd\xa9cW\x0c\xb6Y\x14<\xda\xc5 \xd0\xd6@r\xbb\xdbT\xadr\xa7O\x16\x07{]\xfea\xa5\xd7\xff\x03\xff\xd3\x1e\xfaS\xc7\xad@\x08\x83%\x97\xff\x9e}B]\xde\xac\x1a\xca\x9b\xa5\xa3+,Y\x86\xe5\x06k\xf2\xdbS\xfe\xdeL\xa4\xed\xad\xb6\xf8\xfa\xb2i\x7f=\xa6E]Bf*\xacKj\xda\x1a\xad.\xe12q\xea\x12\xeew\xc5\xaaK\xd8\xb1\x8eQ\x97\xb0\x99\x88uIM\x7fF\xabK\xd8m\x8fQ\x97\xb0\x99\n\xeb\x126_A]\xc2f\xc7\xa8K\xd8L\xa0.as\x15\xd4%lv\x94\xba\xa4"\x061\xeb\x92\x8a\xdc\x19\xb3.as\xa3\xd4%\xec\xbc\xa9\xb0.\xed\xcb\xee\xbd\xf9\xcc\xb7\x9d3\xd7\xbf>iU\x8bV\xd5\xc7*\xbdG V]Bf*\xacKj\xda\x1a\xad.\xe12q\xea\x12\xeew\xc5\xaaK\xd8\xb1\x8eQ\x97\xb0\x99\x88uIM\x7fF\xabK\xd8m\x8fQ\x97\xb0\x99\n\xeb\x126_A]\xc2f\xc7\xa8K\xd8L\xa0.as\x15\xd4%lv\x94\xba\xa4"\x061\xeb\x92\x8a\xdc\x19\xb3.as\xa3\xd4%\xec\xbc\xa9\xb0.\r\xed\xce\xd4dj\x1e]w\x85\x88kb\xcb=vS\x8b\xf3%d\xa6\xc2\xba\xa4\xa6\xad\xd1\xea\x12.\x13\xa7.\xe1~W\xac\xba\x84\x1d\xeb\x18u\t\x9b\x89X\x97\xd4\xf4g\xb4\xba\x84\xdd\xf6\x18u\t\x9b\xa9\xb0.a\xf3\x15\xd4%lv\x8c\xba\x84\xcd\x04\xea\x126WA]\xc2fG\xa9K*b\x10\xb3.\xa9\xc8\x9d1\xeb\x1267J]\xc2\xce\x9b\xd1\xebR\xf8\xb5\xdf\xc7\x1bT\xa1zT\xcf\x98t\xe2\xcbM\xeev%\x9f+~\xc7\x1ePC\x90\xb9\nj\x08.\x13\xa7\x86\xe0~W\xac\x1a\x82\x1d\xeb\x185\x04\x9b\x89XC\xd4\xf4g\xb4\x1a\x82\xdd\xf6\x185\x04\x9b\xa9\xb0\x86`\xf3\x15\xd4\x10lv\x8c\x1a\x82\xcd\x04j\x086WA\r\xc1fG\xa9!*b\x10\xb3\x86\xa8\xc8\x9d1k\x0867J\r\xc1\xce\x9b\x0fk\xc8\xc3\x1a\xf2\xb0\x86<\xac!\x0fk\xc8\xc3\x1a\xa2}\r\t\x9f\xa3\xe8q\xefy\xce\xf8uZ\xe4z\\&\xd6\x9c\x13\xe6w\xc5\x9csBe*\x99s\xc2e\xa2\xce9\xa9\xe8\xcf\xa8sN\xb8m\x8f5\xe7\x84\xcbT:\xe7\x84\xcbW2\xe7\x84\xcb\x8e5\xe7\x84\xcb\x84\xe6\x9cp\xb9J\xe6\x9cp\xd9\xd1\xe6\x9c\xf0c\x10{\xce\t\x97\x0b\xcd9\xe1r\xa3\xcd9\xe1\xe6M\xbc\\\xaf\xf898\x84\\\xaf\xfc\xd9:\xf5\xb9^\xf1w!\xe4z\xe5\xcf\xee)\xcf\xf5\xca\x99\xear=R\x7f*\xcc\xf5\xca\xdb\xae<\xd7+g\xe2\xe5z\xe5|\xf4\\\xaf\x9c\xad<\xd7+g\xa2\xe5z\xe5\\\xf4\\\xaf\x9c\xad,\xd7\xa3\xc4\x00%\xd7+\xe7\xa2\xe5z\xe5\\e\xb9^y\xde\x8c\x9e\xeb\xc3\xf22k\xee\xf2\x9f\n\xa9\xc7k\xac\x0b\xec\xe4\x99\x07N\x7fR\xee\x7f\x98\x97q\xbf+V^Ff*\xc8\xcb\xd8L\xc4\xbc\x8c\x1b\x8fXy\x19\xbb\xed1\xf226Sa^\xc6\xe6+\xc8\xcb\xd8\xec\x18y\x19\x9b\t\xe4el\xae\x82\xbc\x8c\xcd\x8e\x92\x97U\xc4 f^\xc6\xe6\x02y\x19\x9b\x1b%/c\xe7\xcd\xe8y9,\x07a\xbf\xf7\x0f1\x07!\x7f\x8f\x82\x1c\x84\xdd\xf6\x189\x08\x9b\xa90\x07a\xf3\x15\xe4 lv\x8c\x1c\x84\xcd\x04r\x106WA\x0e\xc2fG\xc9A*b\x103\x07\xa9xWh\xcc\x1c\x84\xcd\x8d\x92\x83p\xf7\xdd\x189(\xec\xd9\xe9\x84\xb8j\xf5\xe9\x06\xad-\x1b\x7f\x9b\xf4\xeb\xc5\xf2\xe4<\xd4g\xa7%I\xe4%\xd6!\x1b\x05J`E#GG\xbc\x07\xa2l\xc1\xa0\xb9o\x8c\x7f\xdf\xbakR\xcb\x9eM\xbfm\x80\xfc>\x99\x07\xf0\xc3r\xdb\xecg\xaf\x16\xdd:\xfa_\xeb\xf2\xd6\xd2\xac\xcaD\xcdx\x94\xdcV5\xca\xbb1\x91\x99\x11m\x96%#/Q\xa2$8I\x96\xd1\xb32\x15\xf1<\xb9\xd0\xdc\xd6yj1\x97:e\xef\xc8\x8e3\xd6\xa4\xc0\xbf\x9f\xac\xa0\xcd\xc8L\xc46/\xa8@\xaf_\xbd\xfc\xb2}9{\xe6\x16\xbd\xe0\xdc(-\xda\x8c\xccDl\xf3\x8dS\x85U\x9av~/sB\x93\x99O5:,\x1f\xd7\xa2\xcd\xc8L\xc46\x87\xdfk6~\x9d\x16mFf\xc2m\x0e\xe3\xb7\xfd#\xfd\xeb\xa4\x83m3^.[b\xb8\xf5\'\xd9\n\xf5wm)\xd9)\xc9\xb2\x835\xb0N\xbdS\x08\xec\xee\x11\xef\x8cl\xb3t\xc2\xb6\x92\x96s2F\x15e\x9c-\x18f*An?\xcd\xf1\xa4\xdeH\x8a\x82$Q\x1c)\x91dx\xccG\xed\x1a\xceY\xe7^c\xdf\xa4\xe6^\xe5\x89\xc4\xb2e\x10b\x9e\x10%\xe6\xcdGT\x96\xb7\x7fOw\xfb\xc8\xb2\xb0\xe1\x7f\xbf\xa9]\x03\xb9\xcdFV\xe4\r$ep\x90<\xcb;E\'\x1f~\xfc\x85\xcd\xafLt\xf7\x14\xba\xf2\xe4\xec\xd2\xe3\xaf\xb0c\x18lf\x15"[\xf0\x15\x08\x85i\xa5\xc70a\xef[CfF\xbeo-;\xb3[W[6\x11\xed}kg\xab\xe6M\xad\xf2~n\xe2\x96:\xc4\x99\xb8\n\x0e\xe4\xf7c\n\x06\x07\xc7J\x8c\xe0\xe0i\x89b\xf4F\x86\x0f?F\xc7\xe6\x97\x1e\xa3\xf3\x8c\x85\xb4r6\xd2`\xb0\x18\x18\xbb\xc5j\xb5\xd8lV=\xa3\xa7h\xde\xac\xedv\xe8\x8d\x9cAtr\x0e\xdeh\xe0i\x83\xc3@S\xffo\xf1\x1b\x15\x1d\x9d\xfb\xd2\x85\x0fL+\xf6\'\xbc\xdd\xfd\xcb\xea\x07P\xf9\x9c(\xb1\xa4\xa4\xd7;\x04Jd\r\x82DF\xbc\x97\xee\xca\xac\x96\x9f\xfc\xba\xe1\xa0y\xe5\xe6MV\xe7\x89\xce[\xb5\xe6\xf7\xcf\x99\xf2\x1f\xef\x98\x0e\xc9o\xcd~\xcc\xfeJ\xbb\xcb\xc89\x18\xe2\x7f\xd07~\xd6\xf7\xd4\xc6\x8c\x15\xe7\xca5\x9e\xb3E@\xfe\x8d\x0b\x88\xff\xd4\x85\x89\xb7\x8e\xaei\x9d\xba\xe6p\xfaOM\x9f\xde\x9b\xae5\xbf\xde\xbeEi\x8bO\xecH\x9dzy\xca\xa0a\x9b\xcb\x8c\xd6\x9a\x7fk\xc9\xd5_\x8e\xdd\x9cf\x9e\x99\xeey\xb3\xb0\xbao\x172_\xef\xe0\r2\'\tFI\xa6h\x92\x95\x9c\xe15DW\xa7\xf8\xb9v\xbf\x7f\\c\xed\'\xc4\xfe\xccw\x9b\x9eB\xa9\xdb\xb5\xee\xd6\x90\xb0\xdc\x83\xcc\x0c\xcb=\x94\x99\xa5H\x93\x853\x99\x8d&=i\xe3\xcc\xb4\xcdn\xd4\xf3\x16\x8e7\xe9\xc3\xdb>\xf7\xe3\xbf\xf3j.K\xe4\xe6\xd2\xf6w\xff\xf4\xe7$i\xd1vd&f\xdb_;\xb0.+\xbd\xc7\x96\x8e\x8b\x9c\x973O\xde\\ZT\x1e\xa1\xedu\xef\xb6=\x8c\xd9\xaf\xcd\x88\xf1\xbf\xcc(\xb2,\\x\xeb\x89!)}Z\xa0\xc4\xa3e\x94x 3\xc3\xe2\xc1\xf2f+e\xb5\x18\xacV\xdef\xe3Y;\xcf\xb0\xb4\x91\xe5X\xbdIO\x87\x8f\xf3\x9a\x87\xd2\xf7=\xfdm\xb9\x94W\xf7\xce\xa8\xf3\xce\x9eO\xe7\xa0\x8es\xd6H2\x12)\x894\xcd\x18YI\x10\xe4\x88\xdf\x94\xc2\xe6\x97n\x87\xc1h0q\x94\x8d\xa5-\xa4\xcd`\xe7\xcd\x81\xe3I\xda\xac7\x98)\x13e\x0f\xdf\x0enin\xc5\'\xaa\xad\xed\xb4\xd3\xf5V\xbdN\t\xfc\x11\xe4wz\x91T`\x1f%9\xbd s\xb2\x18\xf8W\xe9\xab\xbb\x89\xff\x03\x056\xf1\x00') + +conf.max_list_count = 500 +pkt = ept_lookup_Response(data, ndr64=False) +towers = [protocol_tower_t(x.valueof("tower").tower_octet_string) for x in pkt.valueof("entries")] + +assert len(towers) == 430 +assert [x.floors[3].rhs.decode().rstrip("\x00") for x in towers if x.floors[3].protocol_identifier == 15] == [ + '\\PIPE\\InitShutdown', + '\\PIPE\\InitShutdown', + '\\pipe\\eventlog', + '\\PIPE\\atsvc', + '\\PIPE\\atsvc', + '\\PIPE\\atsvc', + '\\PIPE\\atsvc', + '\\PIPE\\atsvc', + '\\PIPE\\wkssvc', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\1b6ced1995aeaf47', + '\\pipe\\lsass', + '\\pipe\\lsass', + '\\PIPE\\ROUTER', +] + +tower = next(x for x in towers if x.floors[3].protocol_identifier == 15 and x.floors[3].rhs == b"\\PIPE\\ROUTER\x00") +assert tower.floors[0].uuid + += DCE/RPC 5 NDR: Test length_is with size_is with after-the-fact size + +# From [MS-RRP] + +class BaseRegQueryValue_Response(NDRPacket): + fields_desc = [ + NDRFullPointerField(NDRIntField("lpType", 0)), + NDRFullPointerField( + NDRConfVarStrLenField( + "lpData", + "", + size_is=lambda pkt: (pkt.lpcbData if pkt.lpcbData else 0), + length_is=lambda pkt: (pkt.lpcbLen if pkt.lpcbLen else 0), + ) + ), + NDRFullPointerField(NDRIntField("lpcbData", 0)), + NDRFullPointerField(NDRIntField("lpcbLen", 0)), + NDRIntField("status", 0), + ] + + +pkt = BaseRegQueryValue_Response(b'\x00\x00\x02\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x00U\x00s\x00e\x00r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00', ndr64=True) + +assert pkt.valueof("lpType") == 1 +assert pkt.valueof("lpData").decode("utf-16le") == 'Windows User\x00' +assert pkt.valueof("lpcbData") == 26 +assert pkt.valueof("lpcbLen") == 26 +assert pkt.status == 0 + += DCE/RPC 5 NDR: Test DEPORTED_CONFORMANTS with offsetted padding + +from scapy.layers.msrpce.mseerr import * + +pkt = DceRpc5ExtendedErrorInfo(b'\x01\x10\x08\x00\xcc\xcc\xcc\xcc\x98\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x04\x00\x02\x00\x01\x00\x01\x00\x04\x00\x00\x00\x08\x00\x02\x00\xc0\x03\x00\x00\x00\x00\x00\x00\xa5\xcfq`,\xea\xd9\x01\x02\x00\x00\x00!\x07\x00\x00L\x06\x00\x00\x01\x00\x00\x00\x03\x00\x03\x00\xc4\xfe\xfc\x99\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x00\xc0\x03\x00\x00\x00\x00\x00\x00)fo`,\xea\xd9\x01\x03\x00\x00\x00\x00\x00\x00\x00G\x00\x00\x00\x03\x00\x00\x00\x03\x00\x03\x00\n\x00\x00\x00\x03\x00\x03\x00\x06\x00\x00\x00\x03\x00\x03\x00!\x07\x00\x00\x04\x00\x00\x00D\x00C\x001\x00\x00\x00\x00\x00\x00\x00', ExtendedErrorInfo) + +assert isinstance(pkt.extended_error.value, ExtendedErrorInfo) +assert pkt.extended_error.value.max_count == 1 +assert pkt.extended_error.value.Next.value.ProcessID == 960 +assert pkt.extended_error.value.Next.value.TimeStamp == 133395140301514281 +assert [x.Type for x in pkt.extended_error.value.Next.value.Params] == [3, 3, 3] + +assert pkt.extended_error.value.ComputerName.value.value.valueof("pString") == b'D\x00C\x001\x00\x00\x00' +assert pkt.extended_error.value.ProcessID == 960 +assert pkt.extended_error.value.TimeStamp == 133395140301672357 +assert pkt.extended_error.value.Status == 1825 +assert pkt.extended_error.value.DetectionLocation == 1612 +assert pkt.extended_error.value.Params[0].Type == 3 + += [MS-EERR] test show() + +with ContextManagerCaptureOutput() as cmco: + pkt.show() + result = cmco.get_output() + +EXPECTED = """# Extended Error Information +PID: 960 - 18/09/2023 12:33:50.167234 (1695040430) + | ComputerName: DC1\x00 + | Generating Component: Runtime + | Status: 1825 + | DetectionLocation: OSF_SCALL__DoSecurityCallbackAndAccessCheck + | Flags 0 + | Params: [('eeptiLongVal', -1711472956)] +PID: 960 - 18/09/2023 12:33:50.151428 (1695040430) + | Generating Component: Security Provider + | Status: STATUS_SUCCESS + | DetectionLocation: AcceptThirdLeg10 + | Flags 0 + | Params: [('eeptiLongVal', 10), ('eeptiLongVal', 6), ('eeptiLongVal', 1825)] +""" + +result +assert result.strip() == EXPECTED.strip() + ++ [PASSIVE] Passive sniffing +~ passive + += [PASSIVE] Passive sniffing of DCE/RPC packets encrypted with SPNEGOSSP[NTLMSSP] + +from scapy.libs.rfc3961 import * +import uuid + +bind_bottom_up(TCP, DceRpc5, dport=49679) +bind_bottom_up(TCP, DceRpc5, sport=49679) + +conf.dcerpc_session_enable = True +conf.winssps_passive = [ + SPNEGOSSP( + [ + NTLMSSP( + IDENTITIES={ + "Administrator": MD4le("Password123!"), + }, + ) + ] + ) +] +pkts = sniff(offline=scapy_path('test/pcaps/dcerpc_privacy_ntlm.pcapng.gz'), session=TCPSession) +pkts.show() + +conf.dcerpc_session_enable = False + +# Packet 16 has an encrypted vt_trailer +assert pkts[16].vt_trailer.commands[0].Command == 2 +assert pkts[16].vt_trailer.commands[0].TransferSyntax == uuid.UUID('8a885d04-1ceb-11c9-9fe8-08002b104860') +assert pkts[16].load == b'\x00\x00\x02\x00\x0e\x00\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x001\x009\x002\x00.\x001\x006\x008\x00.\x000\x00.\x001\x000\x000\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00' + +assert pkts[22].load == b'0\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\x00A\x00D\x00W\x00S\x00\x00\x00\xee`\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\xea\x00\x00\x00' +assert pkts[23].load == b'\x00\x00\x00\x00\xad\xb3\xf5\xd1\x8eJ\xdeG\xa9\xa5\x85\xccvb\x8b\x970\x00\x00\x00\x03\x00\x00\x00\x1d\x83\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00' + +# Packet 32 is defragmented and encrypted ! +assert pkts[32].auth_padding == b'\x00\x00\x00\x00\x00\x00\x00\x00' +assert len(pkts[32].load) == 33592 # reassembled +assert hashlib.sha256(pkts[32].load).digest() == b"\xc0\xb5\xde\x1c0\\\x02\x04\x1c\x7f\x05\xcc\xde\xd7\x01\xa5{\x917\xb4\xff\xc7\xa4\xd1\x89\xcd\x1cQ\xa1'3!" + += [PASSIVE] Passive sniffing of DCE/RPC packets encrypted with SPNEGOSSP[KerberosSSP] with AES + +from scapy.libs.rfc3961 import * + +bind_bottom_up(TCP, DceRpc5, dport=49701) +bind_bottom_up(TCP, DceRpc5, sport=49701) + +conf.dcerpc_session_enable = True +conf.winssps_passive = [ + SPNEGOSSP( + [ + KerberosSSP( + KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("85abb9b61dc2fa49d4cc04317bbd108f8f79df28239155ed7b144c5d2ebcf016")), + SPN="ldap/dc1.domain.local", + ) + ] + ) +] +pkts = sniff(offline=scapy_path('test/pcaps/dcerpc_privacy_krb.pcapng.gz'), session=TCPSession) +pkts.show() + +conf.dcerpc_session_enable = False + +# Packet 15 has an encrypted vt_trailer +assert pkts[15].vt_trailer.commands[0].Command == 2 +assert pkts[15].load == b'\x00\x00\x02\x00\x00\x00\x00\x00\x1a M\xe2\xd6O\xd1\x11\xa3\xda\x00\x00\xf8u\xae\r\x00\x00\x02\x00\x00\x00\x00\x004\x00\x00\x00\x00\x00\x00\x004\x00\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\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' + +assert pkts[21].obj.referent_id == 0x1 +assert pkts[21].map_tower.value.tower_octet_string == b'\x05\x00\x13\x00\r5BQ\xe3\x06K\xd1\x11\xab\x04\x00\xc0O\xc2\xdc\xd2\x04\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0b\x02\x00\x00\x00\x01\x00\x07\x02\x00\x00\x87\x01\x00\t\x04\x00\x00\x00\x00\x00' +assert pkts[21].max_towers == 4 + +assert pkts[22].num_towers == 1 +assert pkts[22].ITowers.max_count == 4 +assert pkts[22][ept_map_Response].valueof("ITowers")[0].max_count == 75 +assert pkts[22][ept_map_Response].valueof("ITowers")[0].tower_length == 75 +assert pkts[22][ept_map_Response].valueof("ITowers")[0].tower_octet_string == b'\x05\x00\x13\x00\r5BQ\xe3\x06K\xd1\x11\xab\x04\x00\xc0O\xc2\xdc\xd2\x04\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0b\x02\x00\x00\x00\x01\x00\x07\x02\x00\xc2\x03\x01\x00\t\x04\x00\xc0\xa8\x00d' + ++ MS-RPC client and server + +% The fact that all of this actually works is crazy to me. + += Functional: Define a MS-RPC server +% Same as in dcerpc.rst + +from scapy.layers.dcerpc import * +from scapy.layers.msrpce.all import * +from scapy.layers.msrpce.raw.ms_wkst 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." + """ + req = req[NetrWkstaGetInfo_Request] + req.show() + if req.Level != 0x00000064: + return None + return NetrWkstaGetInfo_Response( + WkstaInfo=NDRUnion( + tag=100, + value=LPWKSTA_INFO_100( + wki100_platform_id=500, # NT + wki100_ver_major=5, + wki100_computername=req.valueof("ServerName") + b"Server" + ), + ), + ndr64=self.ndr64, + ) + @DCERPC_Server.answer(NetrEnumerateComputerNames_Request) + def handle_NetrEnumerateComputerNames(self, req): + """ + NetrWkstaGetInfo [MS-SRVS] + "returns information about the configuration of a workstation." + """ + req = req[NetrEnumerateComputerNames_Request] + req.show() + return NetrEnumerateComputerNames_Response( + ComputerNames=PNET_COMPUTER_NAME_ARRAY( + ComputerNames=[PUNICODE_STRING(Buffer=x) for x in ["Scapy", "Foo", "Bar"]] + ), + ndr64=self.ndr64, + ) + += Functional: Define wrapper over samba's rpcclient +~ linux samba + +import subprocess + +# Create a temporary directory for config +TEMP_DIR = pathlib.Path(get_temp_dir()) +TEMP_DIR.chmod(0o0755) +print(TEMP_DIR) + +# required for smb.conf to work in standalone without root.. wtf +LOGS_DIR = TEMP_DIR / "logs" +LOCK_DIR = TEMP_DIR / "lock" +PRIVATE_DIR = TEMP_DIR / "private" +PID_DIR = TEMP_DIR / "pid" +CACHE_DIR = TEMP_DIR / "cache" +STATE_DIRECTORY = TEMP_DIR / "state" +NCALRPC_DIR = TEMP_DIR / "ncalrpc" + +for dir in [LOGS_DIR, LOCK_DIR, PRIVATE_DIR, PID_DIR, CACHE_DIR, STATE_DIRECTORY, NCALRPC_DIR]: + dir.mkdir() + +SMBD_LOG = LOGS_DIR / "log.smbd" +SMBD_LOG.touch() + +# smb.conf +CONF_FILE = get_temp_file(autoext=".conf") +CONF = """ +# Scapy unit tests rpcserver client + +[global] + lock directory = %s + private directory = %s + cache directory = %s + ncalrpc dir = %s + pid directory = %s + state directory = %s +""" % ( + LOCK_DIR, + PRIVATE_DIR, + CACHE_DIR, + NCALRPC_DIR, + PID_DIR, + STATE_DIRECTORY, +) + +print(CONF) + +with open(CONF_FILE, "w") as fd: + fd.write(CONF) + +def run_rpcclient(transport, command, debug=False): + args = [ + "rpcclient", + "-c", + command, + "%s:127.0.0.1[12345%s]" % ( + transport, + ",seal" + if transport == "ncacn_ip_tcp" + else "" + ), + "-p", "12345", + "-U", "User", "--password", "Password", + "--configfile", CONF_FILE, + ] + if debug: + args += ["-d 5"] + print(" ".join(args)) + proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + return proc.communicate(timeout=10)[0] + += Functional: Start the MS-RPC server over NCACN_IP_TCP with NTLMSSP + +ssp = NTLMSSP( + UPN="User", + HASHNT=MD4le("Password"), + IDENTITIES={ + "User": MD4le("Password"), + }, +) + +rpcserver = MyRPCServer.spawn( + DCERPC_Transport.NCACN_IP_TCP, + iface=conf.loopback_name, + ssp=ssp, + port=12345, + bg=True, +) + += Functional: Connect to it with DCERPC_Client over NCACN_IP_TCP with NTLMSSP + +client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=DCE_C_AUTHN_LEVEL.PKT_INTEGRITY, + ssp=ssp, + ndr64=False, +) +client.connect(get_if_addr(conf.loopback_name), port=12345) +client.bind(find_dcerpc_interface("wkssvc")) + +req = NetrWkstaGetInfo_Request( + ServerName="Nice", + Level=0x00000064, # WKSTA_INFO_100 + ndr64=False +) +resp = client.sr1_req(req) + +assert isinstance(resp.valueof("WkstaInfo"), LPWKSTA_INFO_100) +assert resp.valueof("WkstaInfo").valueof("wki100_computername") == b"NiceServer" + += Functional: Start an endpoint mapper for NCACN_IP_TCP +~ linux samba needs_root + +* rpcclient is dumb and doesn't understand 'ncacn_ip_tcp:127.0.0.1[12345]' means: don't try the endpoint mapper +* ==> we must spawn an endpoint mapper on port 135 +* ==> we must be root. + +portmapserver = DCERPC_Server.spawn( + DCERPC_Transport.NCACN_IP_TCP, + iface=conf.loopback_name, + port=135, + bg=True, + portmap={ + find_dcerpc_interface("wkssvc"): 12345, + }, +) + += Functional: Connect to the server with samba's rpcclient over NCACN_IP_TCP with NTMLSSP +~ linux samba needs_root + +# Note: this is broken in rpcclient < 4.16 .. D: +# https://github.com/samba-team/samba/commit/b5e56a30dfd33e89cfb602b1e7480e210434d600 + +# Note: if this eventually crashes, consider checking whether rpcclient is now greater than 4.16 in github actions (ubuntu-latest) +import re +rpcver = subprocess.Popen(["rpcclient", "-V"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True).communicate()[0] +rpcver = tuple(int(x) for x in re.search(r"[^\d]+(\d+\.\d+\.\d+).*", rpcver).group(1).split(".")) + +if rpcver <= (4, 16, 0): + print("Skipping ncacn_ip_tcp test (broken rpcclient)") +else: + result = run_rpcclient("ncacn_ip_tcp", "wkssvc_enumeratecomputernames") + print(result.decode()) + assert b"Scapy" in result + += Functional: Close the endpoint mapper +~ linux samba needs_root + +try: + portmapserver.shutdown(socket.SHUT_RDWR) +except OSError: + pass + +portmapserver.close() + += Functional: Close the server + +# Close everything now +client.close() +try: + rpcserver.shutdown(socket.SHUT_RDWR) +except OSError: + pass + +rpcserver.close() + += Functional: Re-Start the same MS-RPC server over NCACN_IP_TCP with KerberosSSP + +load_module("ticketer") +SRVKEY = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("85abb9b61dc2fa49d4cc04317bbd108f8f79df28239155ed7b144c5d2ebcf016")) + +* Server SSP +srvssp = KerberosSSP( + KEY=SRVKEY, + SPN="ldap/dc1.domain.local", +) + +* Client SSP +t = Ticketer() +t.import_krb( + KRB_Ticket(bytes.fromhex("618204ae308204aaa003020105a10e1b0c444f4d41494e2e4c4f43414ca2233021a003020103a11a30181b046c6461701b106463312e646f6d61696e2e6c6f63616ca382046c30820468a003020112a10302010ea282045a04820456280c76dee773a1c5e5bd966094201dc028c76f36bbcb9b04c6bb15e02893834f92c694b26bd627fb3f17c2b7eb3ccc57f926e28a9b578b75d1a179c2ce5cba08c67d6b8529f4988490a86a25ec181615e29a344df498ee5ab11a76ff34d862a09b457f6ed528aeb3ad7e7f075f5a02513830554d17edd00554c8f80bab69b80dec86a55111e7ac476d5f099f2ae374378f814a7b85d60f3ce3cff003ff82dd81a7a91a38ff79e5f51e8576de6aba5c86cc7ae2baf13038a8b4b554ff07b9873f19a0c682e83a57811475688e93b2ff53d232a037a19aab83d741204f088fb711c883ce66f4f989752b2c8b18b5cc3fffecbfd9076c25ee39cb13856c09e2ff4958c26e5ecade8c47a2adfd5ceab9d458617b6d3998dd8ee99d0eb57765d0976031a5eb618b076b1e3f6565b4370f238e8829b13deccf5ec35279946816969d5e307e33820f98efb6f601f79c16344d891a415babc6d4d01f992d15ebbf12fb5948cdbef6ed1ba2e5303ca2b0afd0ef1e5231458571bb2e7f463ce539faef5706ac1f8fb34668b6dff101c2fdb4f231fa75c24bb5aff7ee4349ce1948c42fdb91863772bd6c0dac26f47fe6ab1e617cdc85d9e015898fb5d6a0d8a38423c2ef49ec42e200f983fa45526b8cd205db3015e9d37de9cdd5b5befe519f22b7e65780f251215f3ca618f136f73200dd719c23dd3d4072b185e58628b2408377d688ab4540d1395af818a609d3f4df611483a77cd13511978eacf7acc91dd9740d97a9cbbb1299898219650d5ae0d3c0d0521e32132c889a65819ead424ec4f2be1d930f022f27b88078d301a1ce73070062ddf2259b839211e9f83d4585242328e310656f188f3f4cec5d5a61f08f9f0c2a15992a5aa65c4da838a5fd8df426fc4c7679d6af4a261d943a2501ba7221a0af1bc2db19bdfda44064efd94db45231b89035db904b3361afb0c0da0ab4c17857e86a820027f274e01a60388931520db0d667b5453e985152ebd382872122415ec13a88eaaf8522e18b54f580365742ce5884c5fe1d719b752788ff283725c446739686c9f76c850800016287f7cb85390c045fd250104d44f641d62ce1c7882bad72b574e10e1521d843938f30ab7064b007479f2bdc5e8d0aaf26b89993bf2c7c413aec8b8cad4c8d4714904125b868a807329d54674eff909a690bfd735d2c7134c9e819e48a66385a4d48d13ea710f45df9605d727a3d28e5bd09f7385bcab92bc1903ce888571309ffaf370024c5cc527730d256b20ba19511df8f0aa970b638a4393a45db03969b7415270887ef7ec94abbda98632a8d14b0d73f855e416e6d167269d04ec2489c843f11db04074c60c7ea9a13d2d1aca94379e84529bbd96a73f0cd6d8d9d85b5e06272e8739d0d2607d0b57b6e763118996aa8bf903bbaf4ce2ebc20b071e1dbbd48102634823059d4a37d73c054d0e066a09b6c53fe7319a7fcde0f4624461c8b584743d40dc334b34230d56c338bab40426ce7ade90f05a01cb0c0b8963860e4156831e8aecfb8721bf437ab71af74c426acfe7f9134163364a7ee2e")), + key=SRVKEY, +) +clissp = t.ssp(0) + +rpcserver = MyRPCServer.spawn( + DCERPC_Transport.NCACN_IP_TCP, + iface=conf.loopback_name, + ssp=srvssp, + port=12345, + bg=True, +) + += Functional: Connect to it with DCERPC_Client over NCACN_IP_TCP with KerberosSSP + +client = DCERPC_Client( + DCERPC_Transport.NCACN_IP_TCP, + auth_level=DCE_C_AUTHN_LEVEL.PKT_INTEGRITY, + ssp=clissp, + ndr64=False, +) +client.connect(get_if_addr(conf.loopback_name), port=12345) +client.bind(find_dcerpc_interface("wkssvc")) + +req = NetrWkstaGetInfo_Request( + ServerName="Nice", + Level=0x00000064, # WKSTA_INFO_100 + ndr64=False +) +resp = client.sr1_req(req) + +assert isinstance(resp.valueof("WkstaInfo"), LPWKSTA_INFO_100) +assert resp.valueof("WkstaInfo").valueof("wki100_computername") == b"NiceServer" + += Functional: Close the server + +# Close everything now +client.close() +try: + rpcserver.shutdown(socket.SHUT_RDWR) +except OSError: + pass + +rpcserver.close() + += Functional: Re-Start the same MS-RPC server over NCACN_NP + +rpcserver = MyRPCServer.spawn( + DCERPC_Transport.NCACN_NP, + iface=conf.loopback_name, + port=12345, + bg=True, +) + += Functional: Connect to it with DCERPC_Client over NCACN_NP + +client = DCERPC_Client( + DCERPC_Transport.NCACN_NP, + ndr64=False, +) +client.connect(get_if_addr(conf.loopback_name), port=12345) +client.open_smbpipe("wkssvc") +client.bind(find_dcerpc_interface("wkssvc")) + +req = NetrWkstaGetInfo_Request( + ServerName="Nice", + Level=0x00000064, # WKSTA_INFO_100 + ndr64=False +) +resp = client.sr1_req(req) + +# Close everything now +client.close() +try: + rpcserver.shutdown(socket.SHUT_RDWR) +except OSError: + pass + +rpcserver.close() + +assert isinstance(resp.valueof("WkstaInfo"), LPWKSTA_INFO_100) +assert resp.valueof("WkstaInfo").valueof("wki100_computername") == b"NiceServer" + += Functional: Re-Start the same MS-RPC server over NCACN_NP with SPNEGOSSP+NTLMSSP + +from scapy.layers.spnego import SPNEGOSSP + +ssp = SPNEGOSSP( + [ + NTLMSSP( + UPN="User", + HASHNT=MD4le("Password"), + IDENTITIES={ + "User": MD4le("Password"), + } + ) + ] +) + +rpcserver = MyRPCServer.spawn( + DCERPC_Transport.NCACN_NP, + iface=conf.loopback_name, + ssp=ssp, + port=12345, + bg=True, +) + += Functional: Connect to it with DCERPC_Client over NCACN_NP with NTLMSSP + +client = DCERPC_Client( + DCERPC_Transport.NCACN_NP, + ssp=ssp, + ndr64=False, +) +client.connect(get_if_addr(conf.loopback_name), port=12345, smb_kwargs={"debug": 5}) +client.open_smbpipe("wkssvc") +client.bind(find_dcerpc_interface("wkssvc")) + +req = NetrWkstaGetInfo_Request( + ServerName="Nice", + Level=0x00000064, # WKSTA_INFO_100 + ndr64=False +) +resp = client.sr1_req(req) + +assert isinstance(resp.valueof("WkstaInfo"), LPWKSTA_INFO_100) +assert resp.valueof("WkstaInfo").valueof("wki100_computername") == b"NiceServer" + += Functional: Connect to the server with samba's rpcclient over NCACN_NP with NTLMSSP +~ linux samba + +result = run_rpcclient("ncacn_np", "wkssvc_enumeratecomputernames") +print(result.decode()) +assert b"Scapy" in result + += Functional: Close the server + +# Close everything now +client.close() +try: + rpcserver.shutdown(socket.SHUT_RDWR) +except OSError: + pass + +rpcserver.close() + ++ Cleanup + += Restore conf.debug_dissector + +conf.debug_dissector = old_debug_dissector diff --git a/test/scapy/layers/dhcp.uts b/test/scapy/layers/dhcp.uts index dd2346dd58d..8b226b267af 100644 --- a/test/scapy/layers/dhcp.uts +++ b/test/scapy/layers/dhcp.uts @@ -35,25 +35,26 @@ assert udof.m2i("", unknown_value_pad) == [(254, b'\xff'*255), 'pad'] = DHCP - build s = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("message-type","discover"),"end"])) -assert s == b'E\x00\x01\x10\x00\x01\x00\x00@\x11{\xda\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x00\xfcf\xea\x01\x01\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\x0000:01:02:03:04:0\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\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\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\x00\x00\x00c\x82Sc5\x01\x01\xff' +assert s == b'E\x00\x01\x10\x00\x01\x00\x00@\x11{\xda\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x00\xfc\x04}\x01\x01\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\x02\x03\x04\x05\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x01\xff' s2 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="05:04:03:02:01:00")/DHCP(options=[("param_req_list",[12,57,45,254]),("requested_addr", "192.168.0.1"),"end"])) -assert s2 == b'E\x00\x01\x19\x00\x01\x00\x00@\x11{\xd1\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x058\xeb\x01\x01\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\x0005:04:03:02:01:0\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\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\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\x00\x00\x00c\x82Sc7\x04\x0c9-\xfe2\x04\xc0\xa8\x00\x01\xff' +assert s2 == b'E\x00\x01\x19\x00\x01\x00\x00@\x11{\xd1\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x05\xd5\x83\x01\x01\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\x05\x04\x03\x02\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\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\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\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\x00c\x82Sc7\x04\x0c9-\xfe2\x04\xc0\xa8\x00\x01\xff' s3 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="05:04:03:02:01:00")/DHCP(options=[("time_zone",123),("uap-servers","www.example.com"),("netinfo-server-address","10.0.0.1"), ("ieee802-3-encapsulation", 2),("max_dgram_reass_size", 120), ("pxelinux_path_prefix","/some/path"), "end"])) -assert s3 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)\x04i\x01\x01\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\x0005:04:03:02:01:0\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\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\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\x00\x00\x00c\x82Sc\x02\x04\x00\x00\x00{b\x0fwww.example.comp\x04\n\x00\x00\x01$\x01\x02\x16\x02\x00x\xd2\n/some/path\xff' +assert s3 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)\xa1\x01\x01\x01\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\x05\x04\x03\x02\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\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\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\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\x00c\x82Sc\x02\x04\x00\x00\x00{b\x0fwww.example.comp\x04\n\x00\x00\x01$\x01\x02\x16\x02\x00x\xd2\n/some/path\xff' -s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), "end"])) -assert s4 == b'E\x00\x01"\x00\x01\x00\x00@\x11{\xc8\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0e\tr\x01\x01\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\x0000:01:02:03:04:0\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\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\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\x00\x00\x00c\x82Sc\xa1\x13https://example.org\xff' +s4 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("mud-url", "https://example.org"), ("captive-portal", "https://example.com"), ("ipv6-only-preferred", 0xffffffff), "end"])) +assert s4 == b'E\x00\x01=\x00\x01\x00\x00@\x11{\xad\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01)\xeai\x01\x01\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\x02\x03\x04\x05\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Sc\xa1\x13https://example.orgr\x13https://example.coml\x04\xff\xff\xff\xff\xff' -s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), "end"])) -assert s5 == b'E\x00\x01 \x00\x01\x00\x00@\x11{\xca\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x0c\xabQ\x01\x01\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\x0000:01:02:03:04:0\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\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\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\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02\xff' +s5 = raw(IP(src="127.0.0.1")/UDP()/BOOTP(chaddr="00:01:02:03:04:05")/DHCP(options=[("classless_static_routes", "192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"), ("rapid_commit", b""), ("forcerenew_nonce_capable", [1, "HMAC-MD5"]), "end"])) +assert s5 == b'E\x00\x01&\x00\x01\x00\x00@\x11{\xc4\x7f\x00\x00\x01\x7f\x00\x00\x01\x00C\x00D\x01\x12D\xf6\x01\x01\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\x02\x03\x04\x05\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\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\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\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00c\x82Scy\x11 \xc0\xa8{\x04\n\x00\x00\x01\x18\xa9\xfe\xfe\n\x00\x01\x02P\x00\x91\x02\x01\x01\xff' = DHCP - fuzz pkt = fuzz(DHCP()) assert isinstance(pkt.options, RandDHCPOptions) +pkt.show() pkt = DHCP(bytes(pkt)) pkt.show() @@ -80,10 +81,39 @@ assert p3[DHCP].options[6] == "end" p4 = IP(s4) assert DHCP in p4 assert p4[DHCP].options[0] == ("mud-url", b"https://example.org") +assert p4[DHCP].options[1] == ("captive-portal", b"https://example.com") +assert p4[DHCP].options[2] == ("ipv6-only-preferred", 0xffffffff) p5 = IP(s5) assert DHCP in p5 assert p5[DHCP].options[0] == ("classless_static_routes", ["192.168.123.4/32:10.0.0.1", "169.254.254.0/24:10.0.1.2"]) +assert p5[DHCP].options[1] == ("rapid_commit", b"") +assert p5[DHCP].options[2] == ("forcerenew_nonce_capable", [1, 1]) + +repr(DHCP(b"\x01\x00")) +assert DHCP(b"\x01\x00").options == [b"\x01\x00"] +assert DHCP(b"\x28\x00").options == [("NIS_domain", b"")] +assert DHCP(b"\x37\x00").options == [("param_req_list", [])] +assert DHCP(b"\x50\x00").options == [("rapid_commit", b"")] +assert DHCP(b"\x79\x00").options == [("classless_static_routes", [])] +assert DHCP(b"\x91\x00").options == [("forcerenew_nonce_capable", [])] +assert DHCP(b"\x01\x0C\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b").options == [("subnet_mask", "0.1.2.3", "4.5.6.7", "8.9.10.11")] + +b = b"\x79\x01\xff" +p = DHCP(b) +assert p.options == [b] +p.clear_cache() +assert raw(p) == b + +b = b"\x79\x0a\x21\x01\x02\x03\x04\x05\x06\x07\x08\x09" +p = DHCP(b) +assert p.options == [b] +p.clear_cache() +assert raw(p) == b + +b = b"\x79\x09\x20\x01\x02\x03\x04\x05\x06\x07\x08\xff" +assert DHCP(b).options == [("classless_static_routes", ["1.2.3.4/32:5.6.7.8"]), "end"] + = DHCPOptions # Issue #2786 @@ -93,8 +123,17 @@ assert DHCPOptions[46].name == "NetBIOS_node_type" assert DHCPRevOptions['static-routes'][0] == 33 = Check that the dhcpd alias is properly defined and documented -~ python3_only assert dhcpd import IPython -assert IPython.lib.pretty.pretty(dhcpd) == '' + +result = IPython.lib.pretty.pretty(dhcpd) +result + +# 3 results depending on the Python version +assert result in [ + '', + '', + '', + '', +] diff --git a/test/scapy/layers/dhcp6.uts b/test/scapy/layers/dhcp6.uts index 4ae3549891b..cb87561f730 100644 --- a/test/scapy/layers/dhcp6.uts +++ b/test/scapy/layers/dhcp6.uts @@ -298,7 +298,7 @@ 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] +a.optcode == 6 and a.optlen == 0 and a.reqopts == [] = DHCP6OptOptReq - Dissection with specific value a=DHCP6OptOptReq(b'\x00\x06\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') @@ -1022,7 +1022,7 @@ 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] +a.optcode == 43 and a.optlen == 0 and a.reqopts == [] = DHCP6OptRelayAgentERO - Dissection with specific value a=DHCP6OptRelayAgentERO(b'\x00+\x00\x08\x00\x01\x00\x02\x00\x03\x00\x04') @@ -1054,6 +1054,81 @@ raw(DHCP6OptLQClientLink(linkaddress=["2001:db8::1", "2001:db8::2"])) == b'\x000 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 - NTP Server + += DHCP6NTPSubOptSrvAddr - Basic dissection/instantiation +b = b'\x00\x01' + b'\x00\x10' + b'\x00' * 16 +assert raw(DHCP6NTPSubOptSrvAddr()) == b + +p = DHCP6NTPSubOptSrvAddr(b) +assert p.optcode == 1 and p.optlen == 16 and p.addr == '::' + += DHCP6NTPSubOptSrvAddr - Dissection/instantiation with specific values +b = b'\x00\x01' + b'\x00\x10' + b'\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +assert raw(DHCP6NTPSubOptSrvAddr(addr='2001:db8::1')) == b + +p = DHCP6NTPSubOptSrvAddr(b) +assert p.optcode == 1 and p.optlen == 16 and p.addr == '2001:db8::1' + += DHCP6NTPSubOptMCAddr - Basic dissection/instantiation +b = b'\x00\x02' + b'\x00\x10' + b'\x00' * 16 +assert raw(DHCP6NTPSubOptMCAddr()) == b + +p = DHCP6NTPSubOptMCAddr(b) +assert p.optcode == 2 and p.optlen == 16 and p.addr == '::' + += DHCP6NTPSubOptMCAddr - Dissection/instantiation with specific values +b = b'\x00\x02' + b'\x00\x10' + b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01' +assert raw(DHCP6NTPSubOptMCAddr(addr='ff02::101')) == b + +p = DHCP6NTPSubOptMCAddr(b) +assert p.optcode == 2 and p.optlen == 16 and p.addr == 'ff02::101' + += DHCP6NTPSubOptSrvFQDN - Basic dissection/instantiation +b = b'\x00\x03' + b'\x00\x01' + b'\x00' +assert raw(DHCP6NTPSubOptSrvFQDN()) == b + +p = DHCP6NTPSubOptSrvFQDN(b) +assert p.optcode == 3 and p.optlen == 1 and p.fqdn == b'.' + += DHCP6NTPSubOptSrvFQDN - Dissection/instantiation with specific values +b = b'\x00\x03' + b'\x00\x0d' + b'\x07example\x03com\x00' +assert raw(DHCP6NTPSubOptSrvFQDN(fqdn='example.com')) == b + +p = DHCP6NTPSubOptSrvFQDN(b) +assert p.optcode == 3 and p.optlen == 13 and p.fqdn == b'example.com.' + += DHCP6OptNTPServer - Basic dissection/instantiation +b = b'\x00\x38' + b'\x00\x00' +assert raw(DHCP6OptNTPServer()) == b + +p = DHCP6OptNTPServer(b) +assert p.optcode == 56 and p.optlen == 0 and p.ntpserver == [] + += DHCP6OptNTPServer - Dissection/instantiation with specific values +srv_addr = b'\x00\x01' + b'\x00\x10' + b'\x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +mc_addr = b'\x00\x02' + b'\x00\x10' + b'\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01' +srv_fqdn = b'\x00\x03' + b'\x00\x0d' + b'\x07example\x03com\x00' +b = b'\x00\x38' + b'\x00\x39' + srv_addr + mc_addr + srv_fqdn + +p = DHCP6OptNTPServer( + ntpserver=[DHCP6NTPSubOptSrvAddr(addr='2001:db8::1'), + DHCP6NTPSubOptMCAddr(addr='ff02::101'), + DHCP6NTPSubOptSrvFQDN(fqdn='example.com'), + ] +) +assert raw(p) == b + +p = DHCP6OptNTPServer(b) +assert p.optcode == 56 and p.optlen == 57 and len(p.ntpserver) == 3 +assert p.ntpserver[0] == DHCP6NTPSubOptSrvAddr(srv_addr) +assert p.ntpserver[1] == DHCP6NTPSubOptMCAddr(mc_addr) +assert p.ntpserver[2] == DHCP6NTPSubOptSrvFQDN(srv_fqdn) + + ############ ############ + Test DHCP6 Option - Boot File URL @@ -1167,6 +1242,11 @@ raw(DHCP6OptRelaySuppliedOpt(relaysupplied=DHCP6OptERPDomain(erpdomain=["toto.ex 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." += DHCP6OptRelaySuppliedOpt - deeply nested DHCP6OptRelaySuppliedOpt +# https://github.com/secdev/scapy/issues/3894 + +p = DHCP6(b'\x01\x00\x00\x00' + b'\x00B\x0f\x0f' * 100) +assert p.getlayer(DHCP6OptRelaySuppliedOpt, 100) ############ ############ @@ -1183,6 +1263,23 @@ 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 Captive-Portal + += Basic build & dissect +s = raw(DHCP6OptCaptivePortal()) +assert s == b"\x00\x67\x00\x00" + +p = DHCP6OptCaptivePortal(s) +assert p.optcode == 103 +assert p.optlen == 0 +assert p.URI == b"" + +p = DHCP6OptCaptivePortal(b"\x00\x67\x00\x13https://example.org") +assert p.optcode == 103 +assert p.optlen == 19 +assert p.URI == b"https://example.org" ############ ############ @@ -1213,6 +1310,19 @@ p = DHCP6OptVSS(s) assert p.type == 255 +############ +############ ++ Test DHCP6 Option - Address Registration Enabled + += DHCP6OptAddrRegEnable - Basic Instantiation +raw(DHCP6OptAddrRegEnable()) == b'\x00\x94\x00\x00' + += DHCP6OptAddrRegEnable - Basic Dissection +a=DHCP6OptAddrRegEnable(b'\x00\x94\x00\x00') +a.optcode == 148 and a.optlen == 0 + + + ############ ############ + Test DHCP6 Messages - DHCP6_Solicit @@ -1446,7 +1556,7 @@ 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) +a.optcode == 13 and a.optlen == 0 and a.message is None = DHCP6OptRelayMsg - Embedded DHCP6 packet Instantiation raw(DHCP6OptRelayMsg(message=DHCP6_Solicit())) == b'\x00\t\x00\x04\x01\x00\x00\x00' @@ -1467,4 +1577,52 @@ raw(DHCP6_RelayReply()) == b'\r\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ 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 == "::" +############ +############ ++ Test DHCP6 Messages - DHCP6_AddrRegInform + += DHCP6_AddrRegInform - Basic Instantiation +raw(DHCP6_AddrRegInform()) == b'\x24\x00\x00\x00' + += DHCP6_AddrRegInform - Basic Dissection +a = DHCP6_AddrRegInform(b'\x24\x00\x00\x00') +a.msgtype == 36 and a.trid == 0 + += DHCP6_AddrRegInform - Basic test of DHCP6_addrreginform.hashret() +DHCP6_AddrRegInform().hashret() == b'\x00\x00\x00' + += DHCP6_AddrRegInform - Test of DHCP6_addrreginform.hashret() with specific values +DHCP6_AddrRegInform(trid=0xbbccdd).hashret() == b'\xbb\xcc\xdd' + += DHCP6_AddrRegInform - UDP ports overload +a=UDP()/DHCP6_AddrRegInform() +a.sport == 546 and a.dport == 547 + += DHCP6_AddrRegInform - Dispatch based on UDP port +a=UDP(raw(UDP()/DHCP6_AddrRegInform())) +isinstance(a.payload, DHCP6_AddrRegInform) + +############ +############ ++ Test DHCP6 Messages - DHCP6_AddrRegReply + += DHCP6_AddrRegReply - Basic Instantiation +raw(DHCP6_AddrRegReply()) == b'\x25\x00\x00\x00' + += DHCP6_AddrRegReply - Basic Dissection +a = DHCP6_AddrRegReply(b'\x25\x00\x00\x00') +a.msgtype == 37 and a.trid == 0 + += DHCP6_AddrRegReply - Basic test of DHCP6_addrregreply.hashret() +DHCP6_AddrRegReply().hashret() == b'\x00\x00\x00' + += DHCP6_AddrRegReply - Test of DHCP6_addrregreply.hashret() with specific values +DHCP6_AddrRegReply(trid=0xbbccdd).hashret() == b'\xbb\xcc\xdd' + += DHCP6_AddrRegReply - UDP ports overload +a=UDP()/DHCP6_AddrRegReply() +a.sport == 546 and a.dport == 547 += DHCP6_AddrRegReply - Dispatch based on UDP port +a=UDP(raw(UDP()/DHCP6_AddrRegReply())) +isinstance(a.payload, DHCP6_AddrRegReply) diff --git a/test/scapy/layers/dns.uts b/test/scapy/layers/dns.uts index 77e600446dc..6496d6e4a82 100644 --- a/test/scapy/layers/dns.uts +++ b/test/scapy/layers/dns.uts @@ -17,6 +17,20 @@ def _test(): dns_ans = retry_test(_test) += DNS request using dns_resolve +~ netaccess DNS +* this is not using a raw socket so should also work without root + +val = dns_resolve(qname="google.com", qtype="A") +assert val +assert inet_pton(socket.AF_INET, val[0].rdata) +assert val == conf.netcache.dns_cache[b'google.com.;\x01'] + +val = dns_resolve(qname="google.com", qtype="AAAA") +assert val +assert inet_pton(socket.AF_INET6, val[0].rdata) +assert val == conf.netcache.dns_cache[b'google.com.;\x1c'] + = DNS labels ~ DNS query = DNSQR(qname=b"www.secdev.org") @@ -26,11 +40,11 @@ assert query.qname == query.__class__(raw(query)).qname ~ netaccess needs_root IP UDP DNS dns_ans.show() dns_ans.show2() -dns_ans[DNS].an.show() +dns_ans[DNS].an[0].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." +dns_ans2.qd[0].qname = "www.secdev.org." * We need to recalculate these values del dns_ans2[IP].len del dns_ans2[IP].chksum @@ -44,13 +58,13 @@ assert raw(DNSRR(type='A', rdata='1.2.3.4')) == b'\x00\x00\x01\x00\x01\x00\x00\x pkt = IP(raw(IP(src="10.0.0.1", dst="8.8.8.8")/UDP(sport=RandShort(), dport=53)/DNS(qd=DNSQR(qname="secdev.org.")))) assert UDP in pkt and isinstance(pkt[UDP].payload, DNS) assert pkt[UDP].dport == 53 and pkt[UDP].length is None -assert pkt[DNS].qdcount == 1 and pkt[DNS].qd.qname == b"secdev.org." +assert pkt[DNS].qdcount == 1 and pkt[DNS].qd[0].qname == b"secdev.org." * DNS over TCP pkt = IP(raw(IP(src="10.0.0.1", dst="8.8.8.8")/TCP(sport=RandShort(), dport=53, flags="P")/DNS(qd=DNSQR(qname="secdev.org.")))) assert TCP in pkt and isinstance(pkt[TCP].payload, DNS) assert pkt[TCP].dport == 53 and pkt[DNS].length is not None -assert pkt[DNS].qdcount == 1 and pkt[DNS].qd.qname == b"secdev.org." +assert pkt[DNS].qdcount == 1 and pkt[DNS].qd[0].qname == b"secdev.org." = DNS frame with advanced decompression ~ dns @@ -60,7 +74,9 @@ pkt = Ether(a) assert pkt.ancount == 3 assert pkt.arcount == 4 assert pkt.an[1].rdata == b'Zalmoid.local.' +assert pkt.an[1].rdlen is None assert pkt.an[2].rdata == b'Zalmoid.local.' +assert pkt.an[2].rdlen is None assert pkt.ar[1].nextname == b'1.A.9.4.7.E.A.4.B.A.F.B.2.1.4.0.0.6.E.F.7.1.F.2.5.3.E.0.1.0.A.2.ip6.arpa.' assert pkt.ar[2].nextname == b'136.0.168.192.in-addr.arpa.' pkt.show() @@ -80,25 +96,46 @@ assert b.an[6].rdata == b'24:e3:14:4d:84:c0@fe80::26e3:14ff:fe4d:84c0._apple-mob c = b'\x01\x00^\x00\x00\xfb\x14\x0cv\x8f\xfe(\x08\x00E\x00\x01C\xe3\x91@\x00\xff\x11\xf4u\xc0\xa8\x00\xfe\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01/L \x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x00\x05_raop\x04_tcp\x05local\x00\x00\x0c\x00\x01\x00\x00\x11\x94\x00\x1e\x1b140C768FFE28@Freebox Server\xc0\x0c\xc0(\x00\x10\x80\x01\x00\x00\x11\x94\x00\xa0\ttxtvers=1\x08vs=190.9\x04ch=2\x08sr=44100\x05ss=16\x08pw=false\x06et=0,1\x04ek=1\ntp=TCP,UDP\x13am=FreeboxServer1,2\ncn=0,1,2,3\x06md=0,2\x07sf=0x44\x0bft=0xBF0A00\x08sv=false\x07da=true\x08vn=65537\x04vv=2\xc0(\x00!\x80\x01\x00\x00\x00x\x00\x19\x00\x00\x00\x00\x13\x88\x10Freebox-Server-3\xc0\x17\xc1\x04\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x00\xfe' pkt = Ether(c) assert DNS in pkt -assert pkt.an.rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' -assert pkt.an.getlayer(DNSRR, type=1).rrname == b'Freebox-Server-3.local.' -assert pkt.an.getlayer(DNSRR, type=1).rdata == '192.168.0.254' -assert pkt.an.getlayer(DNSRR, type=16).rdata == [b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'] +assert pkt.an[0].rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' +assert pkt.an[1].rdata == [b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'] +assert pkt.an[2].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' +assert pkt.an[2].port == 5000 +assert pkt.an[2].target == b'Freebox-Server-3.local.' +assert pkt.an[3].rrname == b'Freebox-Server-3.local.' +assert pkt.an[3].rdata == '192.168.0.254' + += Other compressed DNS +~ dns +s = b'\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x06\x0bGourmandise\x04_smb\x04_tcp\x05local\x00\x00!\x80\x01\x00\x00\x00x\x00\x14\x00\x00\x00\x00\x01\xbd\x0bGourmandise\xc0"\x0bGourmandise\x0b_afpovertcp\xc0\x1d\x00!\x80\x01\x00\x00\x00x\x00\x08\x00\x00\x00\x00\x02$\xc09\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00s#\x99\xca\xf7\xea\xdc\xc09\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x01x\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10*\x01\xcb\x00\x0bD\x1f\x00\x18k\xb1\x99\x90\xdf\x84.\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\t\xc0\x0c\x00\x05\x00\x00\x80\x00@\xc0G\x00/\x80\x01\x00\x00\x00x\x00\t\xc0G\x00\x05\x00\x00\x80\x00@\xc09\x00/\x80\x01\x00\x00\x00x\x00\x08\xc09\x00\x04@\x00\x00\x08' +pkt = DNS(s) +assert [x.rrname for x in pkt.ar] == [ + b'Gourmandise.local.', + b'Gourmandise.local.', + b'Gourmandise.local.', + b'Gourmandise._smb._tcp.local.', + b'Gourmandise._afpovertcp._tcp.local.', + b'Gourmandise.local.' +] = DNS advanced building ~ dns -pkt = DNS(qr=1, qd=None, aa=1, rd=1) -pkt.an = DNSRR(type=12, rrname='_raop._tcp.local.', rdata='140C768FFE28@Freebox Server._raop._tcp.local.')/DNSRR(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', type=16, rdata=[b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2'])/DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, rclass=32769)/DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', rclass=32769, type=1, ttl=120) +pkt = DNS(qr=1, qd=[], aa=1, rd=1) +pkt.an = [ + DNSRR(type=12, rrname='_raop._tcp.local.', rdata='140C768FFE28@Freebox Server._raop._tcp.local.'), + DNSRR(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', type=16, rdata=[b'txtvers=1', b'vs=190.9', b'ch=2', b'sr=44100', b'ss=16', b'pw=false', b'et=0,1', b'ek=1', b'tp=TCP,UDP', b'am=FreeboxServer1,2', b'cn=0,1,2,3', b'md=0,2', b'sf=0x44', b'ft=0xBF0A00', b'sv=false', b'da=true', b'vn=65537', b'vv=2']), + DNSRRSRV(rrname='140C768FFE28@Freebox Server._raop._tcp.local.', target='Freebox-Server-3.local.', port=5000, type=33, cacheflush=1, rclass=1), + DNSRR(rrname='Freebox-Server-3.local.', rdata='192.168.0.254', cacheflush=1, rclass=1, type=1, ttl=120), +] pkt = DNS(raw(pkt)) -assert DNSRRSRV in pkt.an -assert pkt[DNSRRSRV].target == b'Freebox-Server-3.local.' -assert pkt[DNSRRSRV].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' -assert isinstance(pkt[DNSRRSRV].payload, DNSRR) -assert pkt[DNSRRSRV].payload.rrname == b'Freebox-Server-3.local.' -assert pkt[DNSRRSRV].payload.rdata == '192.168.0.254' +assert DNSRRSRV in pkt.an[2] +assert pkt.an[2][DNSRRSRV].target == b'Freebox-Server-3.local.' +assert pkt.an[2][DNSRRSRV].rrname == b'140C768FFE28@Freebox Server._raop._tcp.local.' + +assert pkt.an[3].rrname == b'Freebox-Server-3.local.' +assert pkt.an[3].rdata == '192.168.0.254' = Basic DNS Compression ~ dns @@ -127,64 +164,243 @@ recompressed.an[3].rdlen = None assert raw(recompressed) == raw(pkt) += DNS cache clearance on sub change +~ dns + +# GH4216 +p = DNS(b'\x00\x00\x01\x00\x00\x00\x00\x02\x00\x00\x00\x00\x03H-1\x05local\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x06\x03H-2\xc0\x10\xc0!\x00\x05\x00\x01\x00\x00\x00\x00\x00\x02\xc0\x0c') +p[DNS].an[0].rrname = 'H' +assert p.raw_packet_cache is None +assert bytes(p) == b'\x00\x00\x01\x00\x00\x00\x00\x02\x00\x00\x00\x00\x01H\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x0b\x03H-2\x05local\x00\x03H-2\x05local\x00\x00\x05\x00\x01\x00\x00\x00\x00\x00\x0b\x03H-1\x05local\x00' + = DNS frames with MX records ~ dns frame = b'E\x00\x00\xa4\x93\x1d\x00\x00y\x11\xdc\xfc\x08\x08\x08\x08\xc0\xa8\x00w\x005\xb4\x9b\x00\x90k\x80\x00\x00\x81\x80\x00\x01\x00\x05\x00\x00\x00\x00\x06google\x03com\x00\x00\x0f\x00\x01\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\x11\x00\x1e\x04alt2\x05aspmx\x01l\xc0\x0c\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x00\x14\x04alt1\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x002\x04alt4\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\t\x00(\x04alt3\xc0/\xc0\x0c\x00\x0f\x00\x01\x00\x00\x02B\x00\x04\x00\n\xc0/' pkt = IP(frame) -results = [x.exchange for x in pkt.an.iterpayloads()] +results = [x.exchange for x in pkt.an] assert results == [b'alt2.aspmx.l.google.com.', b'alt1.aspmx.l.google.com.', b'alt4.aspmx.l.google.com.', b'alt3.aspmx.l.google.com.', b'aspmx.l.google.com.'] pkt.clear_cache() assert raw(dns_compress(pkt)) == frame -= Advanced dns_get_str tests += DNS frame with typebitmaps ~ dns -assert dns_get_str(b"\x06cheese\x00blobofdata....\x06hamand\xc0\x0c", 22, _fullpacket=True)[0] == b'hamand.cheese.' - compressed_pkt = b'\x01\x00^\x00\x00\xfb\xa0\x10\x81\xd9\xd3y\x08\x00E\x00\x01\x14\\\n@\x00\xff\x116n\xc0\xa8F\xbc\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01\x00Ho\x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x03\x03188\x0270\x03168\x03192\x07in-addr\x04arpa\x00\x00\x0c\x80\x01\x00\x00\x00x\x00\x0f\x07Android\x05local\x00\x019\x017\x013\x01D\x019\x01D\x01E\x01F\x01F\x01F\x011\x018\x010\x011\x012\x01A\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x010\x018\x01E\x01F\x03ip6\xc0#\x00\x0c\x80\x01\x00\x00\x00x\x00\x02\xc03\xc03\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8F\xbc\xc03\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\xa2\x10\x81\xff\xfe\xd9\xd3y\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\x06\xc0\x0c\x00\x02\x00\x08\xc0B\x00/\x80\x01\x00\x00\x00x\x00\x06\xc0B\x00\x02\x00\x08\xc03\x00/\x80\x01\x00\x00\x00x\x00\x08\xc03\x00\x04@\x00\x00\x08' +pkt = Ether(compressed_pkt) +assert pkt.ar[2].nextname == b"Android.local." +assert pkt.ar[2].sprintf("%typebitmaps%") == "['A', 'AAAA']" -Ether(compressed_pkt) += Advanced dns_get_str tests +~ dns + +full = b"\x06cheese\x00blobofdata....\x06hamand\xc0\x00" +assert dns_get_str(full[22:], full=full)[0] == b'hamand.cheese.' = Decompression loop in dns_get_str ~ dns -assert dns_get_str(b"\x04data\xc0\x0c", 0, _fullpacket=True)[0] == b"data.data." +full = b"\x04data\xc0\x00" +assert dns_get_str(full, full=full)[0] == b"data.data." = Prematured end in dns_get_str ~ dns -assert dns_get_str(b"\x06da", 0, _fullpacket=True)[0] == b"da." -assert dns_get_str(b"\x04data\xc0\x01", 0, _fullpacket=True)[0] == b"data." +assert dns_get_str(b"\x06da", 0)[0] == b"da." -= Other decompression loop in dns_get_str -~ dns -s = b'\x00\x00\x84\x00\x00\x00\x00\x02\x00\x00\x00\x06\x0bGourmandise\x04_smb\x04_tcp\x05local\x00\x00!\x80\x01\x00\x00\x00x\x00\x14\x00\x00\x00\x00\x01\xbd\x0bGourmandise\xc0"\x0bGourmandise\x0b_afpovertcp\xc0\x1d\x00!\x80\x01\x00\x00\x00x\x00\x08\x00\x00\x00\x00\x02$\xc09\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00\x00s#\x99\xca\xf7\xea\xdc\xc09\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x01x\xc09\x00\x1c\x80\x01\x00\x00\x00x\x00\x10*\x01\xcb\x00\x0bD\x1f\x00\x18k\xb1\x99\x90\xdf\x84.\xc0\x0c\x00/\x80\x01\x00\x00\x00x\x00\t\xc0\x0c\x00\x05\x00\x00\x80\x00@\xc0G\x00/\x80\x01\x00\x00\x00x\x00\t\xc0G\x00\x05\x00\x00\x80\x00@\xc09\x00/\x80\x01\x00\x00\x00x\x00\x08\xc09\x00\x04@\x00\x00\x08' -DNS(s) +full = b"\x04data\xc0\x0f" +assert dns_get_str(full, full=full)[0] == b"data." + + += DNS record type 13 (HINFO) + +b = b'\x00\x00\r\x00\x01\x00\x00\x00\x00\x00\x02\x00\x00' + +p = DNSRRHINFO() +assert raw(p) == b + +p = DNSRRHINFO(b) +assert p.cpu == b'' and p.os == b'' + +b = b'\x00\x00\r\x00\x01\x00\x00\x00\x00\x00\r\x06X86_64\x05LINUX' + +p = DNSRRHINFO(cpu='X86_64', os='LINUX') +assert raw(p) == b + +p = DNSRRHINFO(b) +assert p.cpu == b'X86_64' and p.os == b'LINUX' + +d = DNS(raw(DNS(qd=[],an=[p]))) +assert raw(d.an[0]) == raw(p) + += DNS record type 15 (MX) + +p = DNS(raw(DNS(qd=[],an=DNSRRMX(exchange='example.com')))) +assert p.an[0].exchange == b'example.com.' = DNS record type 16 (TXT) -p = DNS(raw(DNS(id=1,ra=1,qd=None,an=DNSRR(rrname='scapy', type='TXT', rdata="niceday", ttl=1)))) -assert p[DNS].an.rdata == [b"niceday"] +p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='scapy', type='TXT', rdata="niceday", ttl=1)))) +assert p[DNS].an[0].rdata == [b"niceday"] -p = DNS(raw(DNS(id=1,ra=1,qd=None,an=DNSRR(rrname='secdev', type='TXT', rdata=["sweet", "celestia"], ttl=1)))) -assert p[DNS].an.rdata == [b"sweet", b"celestia"] +p = DNS(raw(DNS(id=1,ra=1,qd=[],an=DNSRR(rrname='secdev', type='TXT', rdata=["sweet", "celestia"], ttl=1)))) +assert p[DNS].an[0].rdata == [b"sweet", b"celestia"] assert raw(p) == b'\x00\x01\x01\x80\x00\x00\x00\x01\x00\x00\x00\x00\x06secdev\x00\x00\x10\x00\x01\x00\x00\x00\x01\x00\x0f\x05sweet\x08celestia' +# TXT RR with one empty string +b = b'\x05scapy\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x01\x00' +rr = DNSRR(b) +assert rr.rdata == [b""] +assert rr.rdlen == 1 +rr.clear_cache() +assert DNSRR(raw(rr)).rdata == [b""] + +rr = DNSRR(rrname='scapy', type='TXT', rdata=[""]) +assert raw(rr) == b + +rr = DNSRR(rrname='scapy', type='TXT') +assert raw(rr) == b + +# TXT RR with zero-length RDATA +b = b'\x05scapy\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00' +rr = DNSRR(b) +assert rr.rdata == [] +assert rr.rdlen == 0 +rr.clear_cache() +assert DNSRR(raw(rr)).rdata == [] + +rr = DNSRR(rrname='scapy', type='TXT', rdata=[]) +assert raw(rr) == b + += DNS record type 35 (NAPTR) + +b = b'\x00\x00#\x00\x01\x00\x00\x00\x00\x00+\x00\n\x00d\x01u\x07E2U+sip\x1b!^.*$!sip:info@example.com!\x00' + +p = DNSRRNAPTR(b) +assert p.order == 10 and p.preference == 100 and p.flags == b'u' and p.services == b'E2U+sip' +assert p.regexp == b'!^.*$!sip:info@example.com!' and p.replacement == b'.' + +p = DNSRRNAPTR(order=10, preference=100, flags="u", services="E2U+sip", regexp="!^.*$!sip:info@example.com!") +assert raw(p) == b + += DNS record type 39 (DNAME) + +b = b'\x05local\x00\x00\x27\x00\x01\x00\x00\x00\x00\x00\x07\x05local\x00' + +p = DNSRR(b) +assert p.rrname == b'local.' and p.type == 39 and p.rdata == b'local.' + +p = DNSRR(rrname=b'local', type='DNAME', rdata='local') +assert raw(p) == b + +# Even though according to https://datatracker.ietf.org/doc/html/rfc6672#section-2.5 +# The DNAME RDATA target name MUST NOT be sent out in compressed form +# dns_compress compresses it intentionally to make it easier to test +# DNS-related software that should be able to handle compressed and +# uncompressed DNAMEs anyway regardless of what the RFC says. + +# Make sure it isn't compressed by default +p = DNS(qd=[], an=[DNSRR(rrname='local', type='DNAME', rdata='local')]) +assert raw(p).endswith(b'\x07\x05local\x00') + +# Make sure it can parse uncompressed DNAMEs +rr = DNS(raw(p)).an[0] +assert rr.rrname == b'local.' and rr.type == 39 and rr.rdata == b'local.' + +# Make sure dns_compress compresses DNAME RDATA +cp = dns_compress(p) +assert raw(cp).endswith(b'\x02\xc0\x0c') + +# Make sure it can parse compressed DNAMEs +rr = DNS(raw(cp)).an[0] +assert rr.rrname == b'local.' and rr.type == 39 and rr.rdata == b'local.' + += DNS record type 64, 65 (SVCB, HTTPS) + +b = b'\x00\x00\x00\x04\x00\x01\x00\x06' +p = SvcParam(b) +assert p.key == 0 and p.value == [1, 6] +assert b == raw(SvcParam(key='mandatory', value=['alpn', 'ipv6hint'])) + +b = b'\x00\x01\x00\x06\x02h3\x02h2' +p = SvcParam(b) +assert p.key == 1 and p.value == [b'h3', b'h2'] +assert b == raw(SvcParam(key='alpn', value=['h3', 'h2'])) + +b = b'\x00\x02\x00\x00' +p = SvcParam(b) +assert p.key == 2 and p.value == [] +assert b == raw(SvcParam(key='no-default-alpn')) + +b = b'\x00\x03\x00\x02\x04\xd2' +p = SvcParam(b) +assert p.key == 3 and p.value == 1234 +assert b == raw(SvcParam(key='port', value=1234)) + +b = b'\x00\x04\x00\x08\xc0\xa8\x00\x01\xc0\xa8\x00\x02' +p = SvcParam(b) +assert p.key == 4 and p.value == ['192.168.0.1', '192.168.0.2'] +assert b == raw(SvcParam(key='ipv4hint', value=['192.168.0.1', '192.168.0.2'])) + +b = b'\x00\x06\x00\x10 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01' +p = SvcParam(b) +assert p.key == 6 and p.value == ['2001:db8::1'] +assert b == raw(SvcParam(key='ipv6hint', value=['2001:db8::1'])) + +b = b'\x00\x07\x00\x10/dns-query{?dns}' +p = SvcParam(b) +assert p.key == 7 and p.value == b'/dns-query{?dns}' +assert b == raw(SvcParam(key='dohpath', value=b'/dns-query{?dns}')) + +p = DNSRRSVCB() +assert p.rrname == b'.' and p.type == 64 and p.svc_priority == 0 and p.svc_params == [] + +p = DNSRRHTTPS() +assert p.rrname == b'.' and p.type == 65 and p.svc_priority == 0 and p.svc_params == [] + +# Real-world SVCB RR +b = b'\x04_dns\x03one\x03one\x03one\x03one\x00\x00@\x00\x01\x00\x00\x01,\x001\x00\x01\x03one\x03one\x03one\x03one\x00\x00\x01\x00\x06\x02h3\x02h2\x00\x07\x00\x10/dns-query{?dns}' +p = DNSRRSVCB(b) +assert p.type == 64 and p.ttl == 300 and p.svc_priority == 1 and p.target_name == b'one.one.one.one.' + +alpn = SvcParam(key='alpn', value=['h3', 'h2']) +dohpath = SvcParam(key='dohpath', value=b'/dns-query{?dns}') + +assert raw(p.svc_params[0]) == raw(alpn) +assert raw(p.svc_params[1]) == raw(dohpath) + +assert b == raw(DNSRRSVCB(rrname='_dns.one.one.one.one', ttl=300, svc_priority=1, target_name='one.one.one.one', svc_params=[alpn, dohpath])) + +# Real-world HTTPS RR +b = b'\ncloudflare\x03com\x00\x00A\x00\x01\x00\x00\x00>\x00=\x00\x01\x00\x00\x01\x00\x06\x02h3\x02h2\x00\x04\x00\x08h\x10\x84\xe5h\x10\x85\xe5\x00\x06\x00 &\x06G\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x10\x84\xe5&\x06G\x00\x00\x00\x00\x00\x00\x00\x00\x00h\x10\x85\xe5' + +p = DNSRRHTTPS(b) +assert p.type == 65 and p.ttl == 62 and p.svc_priority == 1 and p.target_name == b'.' + +alpn = SvcParam(key='alpn', value=['h3', 'h2']) +ipv4hint = SvcParam(key='ipv4hint', value=['104.16.132.229', '104.16.133.229']) +ipv6hint = SvcParam(key='ipv6hint', value=['2606:4700::6810:84e5', '2606:4700::6810:85e5']) + +assert raw(p.svc_params[0]) == raw(alpn) +assert raw(p.svc_params[1]) == raw(ipv4hint) +assert raw(p.svc_params[2]) == raw(ipv6hint) + +assert b == raw(DNSRRHTTPS(rrname='cloudflare.com', ttl=62, svc_priority=1, target_name='.', svc_params=[alpn, ipv4hint, ipv6hint])) + = DNS - Malformed DNS over TCP message _old_dbg = conf.debug_dissector conf.debug_dissector = True try: - p = IP(raw(IP()/TCP()/DNS(qd=None,length=28))[:-13]) + p = IP(raw(IP()/TCP()/DNS(qd=[],length=28))[:-13]) assert False except Scapy_Exception as e: assert str(e) == "Malformed DNS message: too small!" try: - p = IP(raw(IP()/TCP()/DNS(qd=None,length=28, qdcount=1))) + p = IP(raw(IP()/TCP()/DNS(qd=[],length=28, qdcount=1))) assert False except Scapy_Exception as e: assert str(e) == "Malformed DNS message: invalid length!" @@ -196,17 +412,17 @@ conf.debug_dissector = _old_dbg data = b'E\x00\x00n~\x82\x00\x00{\x11\xae\xeb\x08\x08\x08\x08\x01\x01\x01\x01\x005\x005\x00Z!\x17\x00\x00\x81\x80\x00\x01\x00\x00\x00\x01\x00\x00\x03www\x06google\x03com\x00\x00\x0f\x00\x01\xc0\x10\x00\x06\x00\x01\x00\x00\x002\x00&\x03ns1\xc0\x10\tdns-admin\xc0\x10\x14Po\x8f\x00\x00\x03\x84\x00\x00\x03\x84\x00\x00\x07\x08\x00\x00\x00<' p = IP(data) -assert p.ns.rrname == b"google.com." -assert p.ns.mname == b"ns1.google.com." -assert p.ns.rname == b"dns-admin.google.com." +assert p.ns[0].rrname == b"google.com." +assert p.ns[0].mname == b"ns1.google.com." +assert p.ns[0].rname == b"dns-admin.google.com." cp = dns_compress(p) -assert cp.ns.rrname == b'\xc0\x10' -assert cp.ns.mname == b'\x03ns1\xc0\x10' -assert cp.ns.rname == b'\tdns-admin\xc0\x10' +assert cp.ns[0].rrname == b'\xc0\x10' +assert cp.ns[0].mname == b'\x03ns1\xc0\x10' +assert cp.ns[0].rname == b'\tdns-admin\xc0\x10' p = IP(raw(cp)) -assert p.ns.rrname == b"google.com." -assert p.ns.mname == b"ns1.google.com." -assert p.ns.rname == b"dns-admin.google.com." +assert p.ns[0].rrname == b"google.com." +assert p.ns[0].mname == b"ns1.google.com." +assert p.ns[0].rname == b"dns-admin.google.com." = DNS - dns_compress on close indexes @@ -214,9 +430,25 @@ p = dns_compress(DNS(qd=DNSQR(qname=b'scapy.'), an=DNSRR(rrname=b'scapy.'), ar=D assert raw(p) == b'\x00\x00\x01\x00\x00\x01\x00\x01\x00\x00\x00\x01\x05scapy\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00)\x10\x00\x00\x00\x80\x00\x00\x00' p = DNS(raw(p)) -assert p.qd.qname == b'scapy.' -assert p.an.rrname == b'scapy.' -assert p.ar.rrname == b'.' +assert p.qd[0].qname == b'scapy.' +assert p.an[0].rrname == b'scapy.' +assert p.ar[0].rrname == b'.' + += DNS - dns_compress with 1-length strings + +data = b'\xac\x81\x81\x80\x00\x01\x00\x06\x00\r\x00\x00\x04mqtt\x0bweatherflow\x03com\x00\x00\x01\x00\x01\xc0\x0c\x00\x05\x00\x01\x00\x00\x00\xe4\x00K\xae\xf9\xacw\t\x83\xe8V\xaf\x0f\xa7m\xdaV\xf78z\xad\x1f\xbf\x95=5\xd9\x071\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\xc0\x0f\xe94\xdf\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x80\xff\x15\x85\xe18.\xcbrZ\xce\x1f5u\n\xd1') + +# measure the time it takes +old_max_list_count = conf.max_list_count +conf.max_list_count = 10 +import time +t = time.monotonic() + +with no_debug_dissector(): + try: + dns = Ether(data) + except MaximumItemsCount: + pass + +delta = time.monotonic() - t +assert delta < 10 + +conf.max_list_count = old_max_list_count + += DNS - Backward compatibility: keep deprecated behavior +~ dns + +# Get through a list (should be pkt.an[0].rdata) +c = b'\x01\x00^\x00\x00\xfb\x14\x0cv\x8f\xfe(\x08\x00E\x00\x01C\xe3\x91@\x00\xff\x11\xf4u\xc0\xa8\x00\xfe\xe0\x00\x00\xfb\x14\xe9\x14\xe9\x01/L \x00\x00\x84\x00\x00\x00\x00\x04\x00\x00\x00\x00\x05_raop\x04_tcp\x05local\x00\x00\x0c\x00\x01\x00\x00\x11\x94\x00\x1e\x1b140C768FFE28@Freebox Server\xc0\x0c\xc0(\x00\x10\x80\x01\x00\x00\x11\x94\x00\xa0\ttxtvers=1\x08vs=190.9\x04ch=2\x08sr=44100\x05ss=16\x08pw=false\x06et=0,1\x04ek=1\ntp=TCP,UDP\x13am=FreeboxServer1,2\ncn=0,1,2,3\x06md=0,2\x07sf=0x44\x0bft=0xBF0A00\x08sv=false\x07da=true\x08vn=65537\x04vv=2\xc0(\x00!\x80\x01\x00\x00\x00x\x00\x19\x00\x00\x00\x00\x13\x88\x10Freebox-Server-3\xc0\x17\xc1\x04\x00\x01\x80\x01\x00\x00\x00x\x00\x04\xc0\xa8\x00\xfe' +pkt = Ether(c) +with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert pkt.an.rdata == b'140C768FFE28@Freebox Server._raop._tcp.local.' + assert len(w) == 1 and issubclass(w[-1].category, DeprecationWarning) + +# Set qd to None (should be qd=[]) +with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + pkt = DNS(qr=1, qd=None, aa=1, rd=1) + assert len(w) == 1 and issubclass(w[-1].category, DeprecationWarning) + +pkt = DNS(bytes(pkt)) +assert pkt.qd == [] + += DNS - command + +p = DNS() +assert p == eval(p.command()) + +p = DNS(qd=[]) +assert p == eval(p.command()) -# RR type A -dnsrr1 = Raw(b'\x01a\x01b\x01c\x00\x00\x01\x10\x00\x00\x00\x00\x01\x00\x04\x01\x02\x03\x04') -# RR type NS & plain rdata -dnsrr2 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x06\x01x\x01y\x01z') -# RR type NS & compressed rdata -dnsrr3 = Raw(b'\x02ns\xc0\x0e\x00\x02\x10\x00\x00\x00\x00\x01\x00\x07\x04test\xc0\x0e') += DNS - iter through DNSStrFields -d = DNS(raw(DNS(ancount=3, an=dnsrr1/dnsrr2/dnsrr3))) -assert d.an[0].rdlen == 4 and d.an[1].rdlen == 6 and d.an[2].rdlen is None +pkt = DNSQR(qname=["domain1.com", "domain2.com"], qtype="A") +for i in pkt: + assert i.qname in [b"domain1.com.", b"domain2.com."] diff --git a/test/scapy/layers/dns_edns0.uts b/test/scapy/layers/dns_edns0.uts index 143957db38e..b8b1202f8bf 100644 --- a/test/scapy/layers/dns_edns0.uts +++ b/test/scapy/layers/dns_edns0.uts @@ -54,6 +54,13 @@ raw(tlv) == b'\x00\x05\x00\x04\x00\x11"3' #conf.debug_dissector = old_debug_dissector #len(r.ar) and r.ar.rdata[0].optcode == 4 # XXX: should be 5 ++ Test EDNS-COOKIE + += EDNS-COOKIE - basic instantiation +tlv = EDNS0TLV(optcode="COOKIE", optdata=b"\x01" * 8) +assert tlv.optcode == 10 +assert raw(tlv) == b"\x00\x0A\x00\x08\x01\x01\x01\x01\x01\x01\x01\x01" + + Test DNS Name Server Identifier (NSID) Option = NSID- basic instantiation @@ -71,6 +78,63 @@ def _test(): retry_test(_test) ++ EDNS0 - DAU + += Basic instantiation & dissection + +b = b'\x00\x05\x00\x00' + +p = EDNS0DAU() +assert raw(p) == b + +p = EDNS0DAU(b) +assert p.optcode == 5 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0DAU(alg_code=['RSA/SHA-256', 'RSA/SHA-512'])) + +p = EDNS0DAU(b) +repr(p) +assert p.optcode == 5 and p.optlen == 2 and p.alg_code == [8, 10] + + ++ EDNS0 - DHU + += Basic instantiation & dissection + +b = b'\x00\x06\x00\x00' + +p = EDNS0DHU() +assert raw(p) == b + +p = EDNS0DHU(b) +assert p.optcode == 6 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0DHU(alg_code=['SHA-1', 'SHA-256', 'SHA-384'])) + +p = EDNS0DHU(b) +repr(p) +assert p.optcode == 6 and p.optlen == 3 and p.alg_code == [1, 2, 4] + + ++ EDNS0 - N3U + += Basic instantiation & dissection + +b = b'\x00\x07\x00\x00' + +p = EDNS0N3U() +assert raw(p) == b + +p = EDNS0N3U(b) +assert p.optcode == 7 and p.optlen == 0 and p.alg_code == [] + +b = raw(EDNS0N3U(alg_code=['SHA-1'])) + +p = EDNS0N3U(b) +repr(p) +assert p.optcode == 7 and p.optlen == 1 and p.alg_code == [1] + + + EDNS0 - Client Subnet = Basic instantiation & dissection @@ -89,3 +153,62 @@ assert raw(d) == raw_d d = DNSRROPT(raw_d) assert EDNS0ClientSubnet in d.rdata[0] and d.rdata[0].family == 2 and d.rdata[0].address == "2001:db8::" + + ++ EDNS0 - Cookie + += Basic instantiation & dissection + +b = b'\x00\n\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00' + +p = EDNS0COOKIE() +assert raw(p) == b + +p = EDNS0COOKIE(b) +assert p.optcode == 10 +assert p.optlen == 8 +assert p.client_cookie == b'\x00' * 8 +assert p.server_cookie == b'' + +b = b'\x00\n\x00\x18\x01\x01\x01\x01\x01\x01\x01\x01\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02\x02' + +p = EDNS0COOKIE(client_cookie=b'\x01' * 8, server_cookie=b'\x02' * 16) +assert raw(p) == b + +p = EDNS0COOKIE(b) +assert p.optcode == 10 +assert p.optlen == 24 +assert p.client_cookie == b'\x01' * 8 +assert p.server_cookie == b'\x02' * 16 + + ++ EDNS0 - Extended DNS Error + += Basic instantiation & dissection + +b = b'\x00\x0f\x00\x02\x00\x00' + +p = EDNS0ExtendedDNSError() +assert raw(p) == b + +p = EDNS0ExtendedDNSError(b) +assert p.optcode == 15 and p.optlen == 2 and p.info_code == 0 and p.extra_text == b'' + +b = raw(EDNS0ExtendedDNSError(info_code="DNSSEC Bogus", extra_text="proof of non-existence of example.com. NSEC")) + +p = EDNS0ExtendedDNSError(b) +assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-existence of example.com. NSEC' + +rropt = DNSRROPT(b'\x00\x00)\x04\xd0\x00\x00\x00\x00\x001\x00\x0f\x00-\x00\x06proof of non-existence of example.com. NSEC') +assert len(rropt.rdata) == 1 +p = rropt.rdata[0] +assert p.info_code == 6 and p.optlen == 45 and p.extra_text == b'proof of non-existence of example.com. NSEC' + +p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0ExtendedDNSError(), EDNS0ClientSubnet(), EDNS0TLV()]))) +assert len(p.rdata) == 3 +assert all(Raw not in opt for opt in p.rdata) + +for opt_class in EDNS0OPT_DISPATCHER.values(): + p = DNSRROPT(raw(DNSRROPT(rdata=[EDNS0TLV(), opt_class(), opt_class()]))) + assert len(p.rdata) == 3 + assert all(Raw not in opt for opt in p.rdata) diff --git a/test/scapy/layers/dot11.uts b/test/scapy/layers/dot11.uts index 7bed426a9f2..0640980633d 100644 --- a/test/scapy/layers/dot11.uts +++ b/test/scapy/layers/dot11.uts @@ -18,6 +18,11 @@ len(dpl_ether) == 1 and Ether in dpl_ether[0] s = raw(Dot11()) s == 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' +pkt = Dot11(ID=0x1205) +raw_data = raw(pkt) +expected = b'\x05\x12' +assert raw_data[2:4] == b'\x05\x12', f"Encoded Dot11 ID field is {raw_data[2:4]}, expected {repr(expected)}." + = Dot11 - dissection p = Dot11(s) Dot11 in p and p.addr3 == "00:00:00:00:00:00" @@ -26,6 +31,10 @@ assert "DA" in p.address_meaning(1) assert "SA" in p.address_meaning(2) assert "BSSID" in p.address_meaning(3) +pkt = b'\x00\x00\x05\x12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +decoded_pkt = Dot11(pkt) +assert decoded_pkt.ID == 0x1205, f"Decoded Dot11 ID field is {hex(decoded_pkt.ID)}, expected 0x1205." + = Dot11QoS - build s = raw(Dot11()/Dot11QoS(Ack_Policy=1)) assert s == b'\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 \x00' @@ -61,6 +70,18 @@ assert Dot11Elt(info="scapy").summary() == "SSID='scapy'" assert Dot11Elt(ID=1).mysummary() == "" assert Dot11(b'\x84\x00\x00\x00\x00\x11\x22\x33\x44\x55\x00\x11\x22\x33\x44\x55').addr2 == '00:11:22:33:44:55' += Dot11 - type 1 subtype 4, 5, 6 + +assert raw(Dot11(type=1, subtype=4, addr2="ff:ff:ff:ff:ff:ff")) == b'D\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff' +assert raw(Dot11(type=1, subtype=5, addr2="ff:ff:ff:ff:ff:ff")) == b'T\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff' +assert raw(Dot11(type=1, subtype=6, addr2="ff:ff:ff:ff:ff:ff", cfe=3)) == b'd0\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff' +assert raw(Dot11(type=1, subtype=6, addr2="ff:ff:ff:ff:ff:ff", cfe=6, addr3="aa:aa:aa:aa:aa:aa")) == b'd`\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xaa\xaa\xaa\xaa\xaa\xaa' + +assert Dot11(type=1, subtype=5).address_meaning(1) == 'RA' +assert Dot11(type=1, subtype=6, cfe=5).address_meaning(2) == 'TA' +assert Dot11(type=1, subtype=6, cfe=6).address_meaning(2) == 'NAV-SA' +assert Dot11(type=1, subtype=6, cfe=6).address_meaning(3) == 'NAV-DA' + = Multiple Dot11Elt layers pkt = Dot11() / Dot11Beacon() / Dot11Elt(ID="Supported Rates") / Dot11Elt(ID="SSID", info="Scapy") assert pkt[Dot11Elt::{"ID": 0}].info == b"Scapy" @@ -124,6 +145,22 @@ assert not a.answers(b) assert not (Dot11()/Dot11Ack()).answers(Dot11()) assert (Dot11()/LLC(dsap=2, ctrl=4)).answers(Dot11()/LLC(dsap=1, ctrl=5)) +# SAE +a = Dot11()/Dot11Auth(algo=3, seqnum=1) # non-AP STA --> AP STA COMMIT +b = Dot11()/Dot11Auth(algo=3, seqnum=1) # AP STA --> non-AP STA COMMIT +c = Dot11()/Dot11Auth(algo=3, seqnum=2) # non-AP STA --> AP STA CONFIRM +d = Dot11()/Dot11Auth(algo=3, seqnum=2) # AP STA --> non-AP STA CONFIRM +e = Dot11()/Dot11Auth(algo=0, seqnum=1) + +assert b.answers(a) +assert c.answers(b) +assert d.answers(c) + +assert not a.answers(e) +assert not c.answers(e) +assert not e.answers(a) +assert not e.answers(c) + = Dot11Beacon network_stats() data = b'\x00\x00\x12\x00.H\x00\x00\x00\x02\x8f\t\xa0\x00\x01\x01\x00\x00\x80\x00\x00\x00\xff\xff\xff\xff\xff\xffDH\xc1\xb7\xf0uDH\xc1\xb7\xf0u\x10\xb7\x00\x00\x00\x00\x00\x00\x00\x00\x90\x01\x11\x00\x00\x06SSID76\x01\n\x82\x84\x0c\x12\x18$0H`l\x03\x01\x080\x18\x01\x00\x00\x0f\xac\x04\x02\x00\x00\x0f\xac\x04\x00\x0f\xac\x02\x01\x00\x00\x0f\xac\x02\x0c\x00\x07\tUSI\x01\x18\x00\n\x05\xe7' @@ -166,8 +203,8 @@ assert pkt[Dot11EltCountry].pad == 0 assert pkt.getlayer(Dot11Elt, ID=11) * Country element: Secondary padding check -erp_payload = b'\x1e\x2a\x01\x62' -country_payload = b'\x07\x06\x55\x53\x20\x01\x0b' +erp_payload = b'\x2a\x01\x62' +country_payload = b'\x07\x06\x55\x53\x20\x01\x0b\x1e' bare_country = Dot11EltCountry(country_payload) country_nested = Dot11EltCountry(country_payload + erp_payload) @@ -431,6 +468,7 @@ assert f.RU_channel2 == [114, 114, 114, 114] assert f.lsig_data1.length assert f.lsig_length == 182 assert f.lsig_rate == 0 +assert f == eval(f.command()) = Reassociation request f = Dot11(b' \x00:\x01@\xe3\xd6\x7f*\x00\x00\x10\x18\xa9l.@\xe3\xd6\x7f*\x00 \t1\x04\n\x00@\xe3\xd6\x7f*\x00\x00\x064.2.12\x01\x08\x82\x84\x0b\x16$0Hl!\x02\x08\x1a$\x02\x01\x0b0&\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x01\x00\x00\x01\x00LD\xfe\xf2l\xdcV\xce\x0b7\xab\xc62\x02O\x112\x04\x0c\x12\x18`\x7f\x08\x01\x00\x00\x00\x00\x00\x00@\xdd\t\x00\x10\x18\x02\x00\x00\x10\x00\x00') @@ -471,7 +509,7 @@ assert isinstance(p, Dot11WEP) conf.crypto_valid = bck_conf_crypto_valid conf.wepkey = "Fobar" -r = raw(Dot11WEP()/LLC()/SNAP()/IP()/TCP(seq=12345678)) +r = raw(Dot11WEP()/LLC()/SNAP()/IP(src="127.0.0.1", dst="127.0.0.1")/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) @@ -542,6 +580,11 @@ assert r.dBm_AntSignal == -30 assert r.Lock_Quality == 100 assert r.RXFlags == 0 +data = b'\x00\x00\x0f\x00\x00\x00\x00\x02\xff\x7f?\x00\x00\x04\x00' +r = RadioTap(data) +repr(r) +assert list(r.hemuou_per_user_known) == ['NSTS'] + = 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) @@ -626,3 +669,139 @@ assert Dot11EltMicrosoftWPA(raw(Dot11EltMicrosoftWPA())).nb_akm_suites == 1 assert Dot11EltMicrosoftWPA(raw(Dot11EltMicrosoftWPA(akm_suites=[AKMSuite(suite="PSK")]))).nb_akm_suites == 1 assert Dot11EltMicrosoftWPA(raw(Dot11EltMicrosoftWPA(akm_suites=[AKMSuite(suite="PSK"), AKMSuite(suite="802.1X")]))).nb_akm_suites == 2 += Dot11BSSTMRequest - dissection + +pkt = RadioTap(b"\x00\x008\x00/@@\xa0 \x08\x00\xa0 \x08\x00\x00\x7f\x89&\x88\x00\x00\x00\x00\x10\x0c\xcc\x15@\x01\xe4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x8f\xe7&\x88\x00\x00\x00\x00\x16\x00\x11\x03\xe4\x00\xde\x01\xd0\x00<\x00\x92U\x1f\xe9g9J\xf2\x1c\x03)\x89J\xf2\x1c\x03)\x89\xc0\xce\n\x07\x01\x05\x05\x00\xff4\x10F\xf2\x1c\x03)\x89\x00\x00\x00\x00Q\x0b\x00\x03\x01\xff\xaaV\xdaY") +assert Dot11BSSTMRequest in pkt + +assert pkt[Dot11Action].category == 10 +assert pkt[Dot11Action][Dot11WNM].action == 7 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].token == 1 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].mode.Preferred_Candidate_List_Included +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].mode.Disassociation_Imminent +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].disassociation_timer == 5 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].validity_interval == 255 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].type == 52 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].len == 16 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].BSSID == "46:f2:1c:03:29:89" +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].AP_reach == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].security == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].key_scope == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].capabilities == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].mobility == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].HT == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].VHT == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].FTM == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].reserved == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].op_class == 81 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].channel == 11 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMRequest].neighbor_report[0].phy_type == 0 + += Dot11BSSTMResponse - dissection + +pkt = RadioTap(b"\x00\x00,\x00\xae@\x00\xa0 \x08\x00\xa0 \x08\x00\xa0 \x08\x00\xa0 \x08\x00\x00\x10\x0c<\x14@\x01\xce\x00d\x00\x00\x00\xd0\x00\xca\x01\xca\x02\xcc\x03\xd0\x00<\x00df$J\xe1\xc4\xa0\xcc+\xbe\xc9Odf$J\xe1\xc4p\x0c\n\x08\x01\x06\x004\rdf$J\xe1\xc3\x00\x00\x00\x00\x04\x0c\x00<\xdd\xdf=") +assert Dot11BSSTMResponse in pkt + +assert pkt[Dot11Action].category == 10 +assert pkt[Dot11Action][Dot11WNM].action == 8 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].token == 1 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].status == 6 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].termination_delay == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].type == 52 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].len == 13 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].BSSID == "64:66:24:4a:e1:c3" +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].AP_reach == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].security == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].key_scope == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].capabilities == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].mobility == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].HT == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].VHT == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].FTM == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].reserved == 0 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].op_class == 4 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].channel == 12 +assert pkt[Dot11Action][Dot11WNM][Dot11BSSTMResponse].neighbor_report[0].phy_type == 0 + += Dot11Ack + +pkt = Dot11(bytes(Dot11()/Dot11Ack())) +assert pkt.subtype == 13 +assert pkt.type == 1 + += Dot11CSA + +pkt = RadioTap(b"\x00\x008\x00/@@\xa0 \x08\x00\xa0 \x08\x00\x00\xfe\x83\x06\x10\x00\x00\x00\x00\x10\x02\x8a\t\xa0\x00\xfe\x00\x00\x00\x00\x00\x00\x00\x00\x00\x006\x07\x10\x00\x00\x00\x00\x16\x00\x11\x03\xf8\x00\xfe\x01\xd0\x00\x00\x00\xff\xff\xff\xff\xff\xff\x0cs)d\xa5\r\x0cs)d\xa5\r\xb0!\x00\x04%\x03\x01\x0b\x05\x0b\xb9<\x8c") +assert Dot11SpectrumManagement in pkt +assert Dot11CSA in pkt +assert Dot11EltCSA in pkt + +assert pkt[Dot11Action].category == 0 +assert pkt[Dot11Action][Dot11SpectrumManagement].action == 4 +assert pkt[Dot11Action][Dot11SpectrumManagement][Dot11CSA][Dot11EltCSA].ID == 37 +assert pkt[Dot11Action][Dot11SpectrumManagement][Dot11CSA][Dot11EltCSA].len == 3 +assert pkt[Dot11Action][Dot11SpectrumManagement][Dot11CSA][Dot11EltCSA].mode == 1 +assert pkt[Dot11Action][Dot11SpectrumManagement][Dot11CSA][Dot11EltCSA].new_channel == 11 +assert pkt[Dot11Action][Dot11SpectrumManagement][Dot11CSA][Dot11EltCSA].channel_switch_count == 5 + += Dot11OBSS + +data = b'\x00\x008\x00/@@\xa0 \x08\x00\xa0 \x08\x00\x00\x7fB\xe9\n\x00\x00\x00\x00\x10\x16l\t\xa0\x00\xc3\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbf\x9b\xe9\n\x00\x00\x00\x00\x16\x00\x11\x03\xc3\x00\xbf\x01\x80\x00\x00\x00\xff\xff\xff\xff\xff\xff`\x8d&\xa6\xd6\x04`\x8d&\xa6\xd6\x04@S\xe2\xb0\x04\x00\x00\x00\x00\x00d\x00\x11\x14\x00\rArc-QA-Lab-2G\x01\x08\x82\x84\x8b\x96$0Hl\x03\x01\x01\x05\x04\x02\x03\x00\x00\x07\x06AE \x01\r\x14#\x02\x19\x00*\x01\x042\x04\x0c\x12\x18`0\x18\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x02\x00\x00\x0f\xac\x02\x00\x0f\xac\x04\x0c\x00\x0b\x05\x00\x00\xc1\x00\x00F\x053\x00\x00\x00\x006\x03d\x00\x00-\x1a\xef\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\x01\x08\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00J\x0e\x14\x00\n\x00,\x01\xc8\x00\x14\x00\x05\x00\x19\x00\x7f\n\x05\x00\x08\x80\x00\x00\x00@\x00@\xff #\x05\x00\x08\x12\x00\x10" \x02\xc0\x0fB\x85\x10\x00\x0c\x00\xea\xff\xea\xffz\x1c\xc7q\x1c\xc7q\x1c\xc7q\xff\x07$\xf4?\x00\x02\xfc\xff\xff\x0e&\x00\x00\xa4\x08 \xa4\x08@C\x08`2\x08\xdd\x1d\x00P\xf2\x04\x10J\x00\x01\x10\x10D\x00\x01\x02\x10<\x00\x01\x03\x10I\x00\x06\x007*\x00\x01 \xdd\x1e\x00\x90L\x04\x18\xbf\x0c\xb1i\x8a\x0f\xea\xff\x00\x00\xea\xff\x00\x00\xc0\x05\x00\x01\x00\x00\x00\xc3\x02\x005\xdd\n\x00\x10\x18\x02\x00\x00\x1c\x00\x00\x01\xdd\x18\x00P\xf2\x02\x01\x01\x80\x00\x03\xa4\x00\x00\'\xa4\x00\x00BC^\x00b2/\x00l\x02\x7f\x00 \x8d\xf4\xe1' +pkt = RadioTap(data) + +assert Dot11EltOBSS in pkt + +assert pkt[Dot11EltOBSS].ID == 74 +assert pkt[Dot11EltOBSS].len == 14 +assert pkt[Dot11EltOBSS].Passive_Dwell == 20 +assert pkt[Dot11EltOBSS].Active_Dwell == 10 +assert pkt[Dot11EltOBSS].Scan_Interval == 300 +assert pkt[Dot11EltOBSS].Passive_Total_Per_Channel == 200 +assert pkt[Dot11EltOBSS].Active_Total_Per_Channel == 20 +assert pkt[Dot11EltOBSS].Delay == 5 +assert pkt[Dot11EltOBSS].Activity_Threshold == 25 + += Dot11VHTOperation + +pkt = RadioTap(b"\x00\x008\x00/@@\xa0 \x08\x00\xa0 \x08\x00\x00K\x1178\x00\x00\x00\x00\x10\x0c<\x14@\x01\xba\x00\x00\x00\x00\x00\x00\x00\x00\x00\xffj78\x00\x00\x00\x00\x16\x00\x11\x03\xb6\x00\xba\x01\x80\x00\x00\x00\xff\xff\xff\xff\xff\xff`\x8d&\xa6\xd6\x05`\x8d&\xa6\xd6\x05\xb0i~\x96\x9e\x03\x00\x00\x00\x00d\x00\x11\x11\x00\rArc-QA-Lab-5G\x01\x08\x8c\x12\x98$\xb0H`l\x05\x04\x00\x03\x00\x00\x07\x9b\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\x16\xdd\x14\x00\x0f\xac\x04\x03\xca?d\xca\xed\xdd\xef\xf69;\xefX\xd4\x97w' +wifi = Dot11(s) +assert wifi[EAPOL].key_descriptor_type == 2 +assert wifi[EAPOL].encrypted_key_data == 0 +assert wifi[EAPOL].key_ack == 1 +assert wifi[EAPOL].key_type == 1 +assert wifi[EAPOL].key_descriptor_type_version == 2 +assert wifi[EAPOL].key_replay_counter == 4 +assert wifi[EAPOL].has_key_mic == 0 +assert wifi[EAPOL].key_data_length == 22 +assert len(wifi[EAPOL].key_data) == 22 + += EAPOL-Key - Key 1 - Dissection (1) +s = b'\x02\x03\x00\x75\x02\x00\x8a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x12\x6a\xce\x64\xc1\xa6\x44\xd2\x7b\x84\xe0\x39\x26\x3b\x63\x3b\xc3\x74\xe3\x29\x9d\x7d\x45\xe1\xc4\x25\x44\x05\x48\x05\xbf\xe5\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\x16\xdd\x14\x00\x0f\xac\x04\x05\xb1\xb6\x8b\x5a\x91\xfc\x04\x06\x83\x84\x06\xe8\xd1\x5f\xdb' +eapol = EAPOL(s) +assert(eapol.version == 2) +assert(eapol.type == 3) +assert(eapol.len == 117) +assert(eapol.haslayer(EAPOL_KEY)) +eapol_key = eapol[EAPOL_KEY] +assert(eapol_key.key_descriptor_type == 2) +assert(eapol_key.key_descriptor_type_version == 2) +assert(eapol_key.key_type == 1) +assert(eapol_key.key_length == 16) +assert(eapol_key.install == 0) +assert(eapol_key.key_ack == 1) +assert(eapol_key.key_mic == b"\x00" * 16) +assert(eapol_key.secure == 0) +assert(eapol_key.key_data_length == 22) +assert(eapol_key.guess_key_number() == 1) + += EAPOL_KEY - Key 2 - Dissection (2) +s = b'\x02\x03\x00\x75\x02\x01\x0a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x60\x5e\x85\xa7\x9c\xfa\xfd\xb0\xea\xa0\x50\x68\x3f\x97\xbe\x1b\x66\xde\xf7\xbc\x65\x20\x57\x31\x68\x71\xc2\x73\xc5\xae\x47\x7f\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\x91\x89\xcd\xf1\x88\x54\x8e\x73\xcd\x37\xd5\x78\x52\x66\x05\x88\x00\x16\x30\x14\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x04\x01\x00\x00\x0f\xac\x02\x28\x00' +eapol = EAPOL(s) +assert(eapol.version == 2) +assert(eapol.type == 3) +assert(eapol.len == 117) +assert(eapol.haslayer(EAPOL_KEY)) +eapol_key = eapol[EAPOL_KEY] +assert(eapol_key.key_descriptor_type == 2) +assert(eapol_key.key_descriptor_type_version == 2) +assert(eapol_key.key_type == 1) +assert(eapol_key.key_length == 16) +assert(eapol_key.install == 0) +assert(eapol_key.key_ack == 0) +assert(eapol_key.has_key_mic == 1) +assert(eapol_key.secure == 0) +assert(eapol_key.key_data_length == 22) +assert(eapol_key.guess_key_number() == 2) + += EAPOL_KEY - Key 3 - Dissection (3) +s = b'\x02\x03\x00\x97\x02\x13\xca\x00\x10\x00\x00\x00\x00\x00\x00\x00\x01\x12\x6a\xce\x64\xc1\xa6\x44\xd2\x7b\x84\xe0\x39\x26\x3b\x63\x3b\xc3\x74\xe3\x29\x9d\x7d\x45\xe1\xc4\x25\x44\x05\x48\x05\xbf\xe5\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\xce\x1f\x1e\x80\xe7\x6c\xbf\x4a\x5c\xe9\xce\x84\x6d\x20\x7f\x7d\x00\x38\x10\xcc\x53\x66\x65\x5f\x7f\xf5\xd5\x5a\xf8\xc3\x87\x69\x85\xde\x7d\x96\xaa\xfd\x2b\x93\x48\x9f\x6c\xdf\x5f\x9c\x26\x2b\xe1\xad\x21\xeb\xce\x62\xc9\x4d\x88\x97\x1f\xd7\x5e\x23\xf6\x96\xf6\xc0\xe0\x1e\xf3\x52\x85\xe2\xf2\xcc' +eapol = EAPOL(s) +assert(eapol.version == 2) +assert(eapol.type == 3) +assert(eapol.len == 151) +assert(eapol.haslayer(EAPOL_KEY)) +eapol_key = eapol[EAPOL_KEY] +assert(eapol_key.key_descriptor_type == 2) +assert(eapol_key.key_descriptor_type_version == 2) +assert(eapol_key.key_type == 1) +assert(eapol_key.key_length == 16) +assert(eapol_key.install == 1) +assert(eapol_key.key_ack == 1) +assert(eapol_key.has_key_mic == 1) +assert(eapol_key.secure == 1) +assert(eapol_key.key_data_length == 56) +assert(eapol_key.guess_key_number() == 3) + += EAPOL_KEY - Key 4 - Dissection (4) +s = b'\x02\x03\x00\x5f\x02\x03\x0a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x01\x60\x5e\x85\xa7\x9c\xfa\xfd\xb0\xea\xa0\x50\x68\x3f\x97\xbe\x1b\x66\xde\xf7\xbc\x65\x20\x57\x31\x68\x71\xc2\x73\xc5\xae\x47\x7f\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\x27\x95\xe1\x76\xeb\x6b\xba\xc1\x6e\x06\x16\xb4\x14\x94\xd6\x0a\x00\x00' +eapol = EAPOL(s) +assert(eapol.version == 2) +assert(eapol.type == 3) +assert(eapol.len == 95) +assert(eapol.haslayer(EAPOL_KEY)) +eapol_key = eapol[EAPOL_KEY] +assert(eapol_key.key_descriptor_type == 2) +assert(eapol_key.key_descriptor_type_version == 2) +assert(eapol_key.key_type == 1) +assert(eapol_key.key_length == 16) +assert(eapol_key.install == 0) +assert(eapol_key.key_ack == 0) +assert(eapol_key.has_key_mic == 1) +assert(eapol_key.secure == 1) +assert(eapol_key.key_data_length == 0) +assert(eapol_key.key_data == b'') +assert(eapol_key.guess_key_number() == 4) + ############ ############ diff --git a/test/scapy/layers/http.uts b/test/scapy/layers/http.uts index aaf3035cf35..2d2093b976d 100644 --- a/test/scapy/layers/http.uts +++ b/test/scapy/layers/http.uts @@ -33,7 +33,7 @@ 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) +assert a[29].load == base64.b64decode(wireshark_data) # This a valid JPEG image: try it out # open("image.jpg", "wb").write(a[29].load) @@ -68,6 +68,39 @@ assert HTTPResponse in pkt print(pkt[Raw].load, expected_data) assert pkt[Raw].load == expected_data += TCPSession - Invalid Content-Length + +pkts = [ + IP()/TCP(seq=1)/HTTP()/Raw(load=b'GET / HTTP/1.1\r\nContent-Length: bad\r\nCoo'), + IP()/TCP(seq=41)/HTTP()/Raw(load=b'kie: cookie\r\n\r\n'), +] +a = sniff(offline=pkts, session=TCPSession) + +assert HTTPRequest in a[0] +assert a[0].Cookie == b"cookie" + += TCPSession - dissect HTTP 1.0 HEAD response +~ http + +load_layer("http") + +a = sniff(offline=scapy_path("/test/pcaps/http_head.pcapng.gz"), session=TCPSession) + +assert HTTPRequest in a[3] +assert a[3].Method == b"HEAD" +assert a[3].User_Agent == b'curl/7.88.1' + +assert HTTPResponse in a[6] +assert a[6].Content_Type == b'text/html; charset=UTF-8' +assert a[6].Expires == b'Mon, 01 Apr 2024 22:25:38 GMT' +assert a[6].Reason_Phrase == b'Moved Permanently' +assert a[6].X_Frame_Options == b"SAMEORIGIN" + += HTTP build with 'chunked' content type + +pkt = HTTP()/HTTPResponse(Content_Encoding="chunked", Date=b'Sat, 22 Jun 2024 10:00:00 GMT')/(b"A" * 100) +assert bytes(pkt) == b'HTTP/1.1 200 OK\r\nContent-Encoding: chunked\r\nDate: Sat, 22 Jun 2024 10:00:00 GMT\r\n\r\n64\r\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n0\r\n\r\n' + = HTTP decompression (gzip) conf.debug_dissector = True @@ -100,6 +133,25 @@ pkts[2].show() assert HTTPResponse in pkts[2] assert pkts[2].load == b'' += HTTP decompression (gzip) with retransmission + +# pcap from GH4340 + +import io +import gzip + +pcap = gzip.decompress(bytes.fromhex("1f8b080062b92e6602ff9d586bace4e659f6d964d3e5244b93c08f003f3a52b2e96e9573d69799738e376cd8f15ced33f6dc3cbe0954c6f68cc733b6c739e3b9d840d956085445229590902284442fb4db42aab42ab4a04a5c052d95aa52d01209352090b854a8093fd24a9ba0f2bef69973dbed36b0dad1ccb1fd3ddf7b79dee77dfdfdfd5f7cfea3e7880789f5bfef7f9f2036e0fbb56463d8f8f94d8283dff8c96b370a073ff191773ff2d77ff5b90b44052ec4c10de2e643db1ffffd9ffcee537f7aebf5bff9ca9344ebe557aa9f42949be7eebc617f9420ce3d78fe0b1b0f3c706163e3c17388f8bee78f1133ac0c374324881bc4730f7dfb31444354a2f595275f5a7cf82d40bd75f3f13b6f149f4c115f4134407d172292079bc40ea0eddccbc6cbf134b5f1b3676cbc05a86fb71e3d773ef956e6e95bf126f16fe789f47316e5fcab71882897ffe707a16cfd2b41b49a5d3977f580d9d15a96193bc1b0336e1487d586392c6f87a3305797e5d6556a9bbab8599fcea26bb9e76776b2bdf0b767c97cdb9f0e2e6e162d6b10465b95c09ada6ee05ccb39891b3e93b30743af1fc1fdde6c70b0557406012c16a789eb79fdabf96d3277d99afa613f724d6ff06c4eecf2951c456e93cfe65437b0a7cb594e92733bdbd4b339f9c0b561f1d5c23679e5e266691a44f0d7961c87836bb97e187aae0520d3e0ea6a6bb95c6e0da707fed6fcc01ba03903fb7841631038d1e85a2e9f67a9f46a30b070ddb59ce54d6760e7c5cdb1cd30e4deee4e9f64acfcf0ba5169d04268ba6cd22f5e62caf0ffe980de2587ec80b4eddd7cdebcde3b781ac311c7f6ecfaa53c3958f4bdcbf06df667839dfcfbed019a70f9129d7f3f06f952817befaa6031e490b1f3145328e4df7ba950be72e512c33d3d2fecb014651776addd9d3dfa7ab2d0e9b6c3bb2cad6bfcc2666ca6418f48535dba4d5a19f76b7b0eef8f463a1d7946a9e2f2e5bc63f8ab9191449e4e8f167cb948e97265d92c77dc66395ada747bd750a5a919176971dc2625b5478ab2e01b631d7d3a6dd3f576cd9bf7153631b4f694f73ba1c570335df3868626c4262324bcbb74f871d1dd97a3a2452ba4c6744253558666cd8bfab0462c4d76851abb346a5edcea71a1e9c35a786ebf242cac9a32d7686104eb267d8d478cd0709d89c970f877c8d76713939618a34cc1dfc25c57296fbfd419f648a5ab4c94ae1a0b729b12845eb533ec4c3cb1d32b7072afe76add6c4f1d6c6c695c6ca8f610ed467b74bf9a18b24e6a7407a2c34e8caee3caf5906c56d8b05926ddfdeef268ff26c98dac40180d1476696bd214f6069babb1e52e33bb4aab64a049a93fe0b7676adc10fddc2f4dc2266d2c2c5f81ac38d3b52d7cad9af0a54e68d79468bfcbedf6557dca434c2c8a652cdf23755598813db037607527e18096166600f10c14afc990bb42bd13a571ec66180d975f34fc765ef77946572b243f8e8aa61aa1edd3f5b387311db56b4668d614c8db0872d849c0176010e4d3bd6b9f895dc378f32cda6fd6b9915d734ee215815567f728f6d542a8319267971c17e2e7991eebea9ae4e1fd94236e7ecebb9817696e630cc754d1aa0b9e41b189555b85b8ae41ad2cadbadad34a7b0b58b3b6238b25f0a22f93cb66dc095b75291cf8ca02b9a2d3051263bb2fcf26fdeeec605f3bb66fbde7f087dbe736e234df711613e7c4de02f0a77a80f11fcab35d2350e63a729cce3bb8def445cc2f6597bc5d881769034ff90a97624be3ced0500b6313b886dcb1b57688386730e07a07f8d59eee6bc0774621adba42b654d6d568dbb3ab2cac2f9080435ab103f6e075656ee0f59a327a07aaff7caafa6f9f56fd86b656fd97b709c2741179ad259d51b35671815d94984c9866973fd40bae78cae33a075e4fd65ae24ae5912b96f8393f162662592c001b599316a0f23a5045e09d1c9156e0018b8becdd55384a6c5548ec3ab0aacab6654aea41753f6f94c5442c3b8ce45796222d92862bd88d321f62b6ad843a8e724dc9a25c97283d9048cca04671ad1ed531f9a04749e3c94a54db94448b8c347662de236722301fd8e4998181fbb2ed1a2a518f96aa608f2a2d8c5a6f6a318a0bf6c7a9ea545859a9ce5c73cc33624d2f18323731ca1629d578509c65bc9faa4e9b4546997e35c24ad628a9daf30cf8ccdc1e2975bb0ad7d34843902b4ab3dda3aa6007da306e69c7761ba5e5b251ae842d9944565ea2abbc4b8df9ead21dc49d64a82c5d7e3c751bb4e8682501a229b15a89874ee0f927bd3f83120e480e3886de82e6781ca7541c96770b8171fa1ae85fde6dba2c6531fcc2acb1635d5d2e2cec7be329f0548a1bf40aba5175d638798d91dcbe9adf69304a62b9ece1efc36b69cf843a6228cfac4bdeffc90e52dc695036f40ca81d465c588c97a0ee8909bfdb4ea60b85f6e6460db4336665d0614da62c5a2a4765b05f837a875ab293b318bd638ce3f514b7b06925eed123cfac2d17f6b832174b7bbba976a810d75215b59d1c4046a10ee994115d6032f42fdb4fb5cc1bd4b95956e7c2deb0be0226ad604f057e83b6948ac7ebe4c887bee9e96ada4f907d501d3ce880e808751d7a1eea7571caab4a64d53b05788606663927e386381a60806e427fe94df960b53784ded2a79502ecbd97f56432ddd7469633143bec76a06f205f3816fbaace90c01983327d89ecabec9caf19a8513e6a10f4db13fef033de5d65fa897a04fa090c4fd25ea5b0be0dfa66d73ce81fa28b15803dcd50a9a55d9f804ab4c343bcac8f769d631ec49d494b85d8f9d58246c3fd9213d875e04ad79aa19ee38c61aad5e57ec986ea14481378873912e225703bf52906ce50969f9f1eeb69e1647ff3f7bbbadf87fe8c7c356b55d7848a166a7a28b8fa08fc1966fba09dfa2cb5558ed85331a90b941948e0cf6a66c124a5d333f469063d7c7cd8bf9d3335e788f8817c41dd41cfdb73f91274e734b6f63a7fc17eb7e80b9063c3077bc027cb67630dfc33620bf20ff9cb9e8f74d59b09f1243cb1172828eef50e54ff2055fdb74eab7ee7c25af5ffe821821861679b9838e1c5d07dd7d36d890f81392e6a34fee64b1596af189c5c95aa6809767dab540416a5130f4c65e4142bf3b05b91987dd075eca2881300b30388ecc9aae77b55568466cec2a4e40c622edf8c39b61917277a5cbc4473873bc42d575f610da6be6731f180470bcb63819b5690d6c5a9f839aede05be82ee419f75206f890116038f421378abf78a2b716c416de7978d7105ebc3cd66135853e2233dd07106855ae1528e422dae39e674fc2a70a0b70b75b4eed74eabcb150ddfc33a40fd029eb130db8026d4c569e6c364b75d57e68711c6996299cd63c5131db4386a75d10ef085e162e02599cd0ae91ee087e0d97525365dae67a5f319c51ecf19c608ed3c3b5358f89ba626fbc853a83f7cbe2517ff7fdd09fb4a167b9c7521bb6480dad7ae4bf0a280732acc605093b807c62e8db97ab406e65e09f47614db5d2bc4d90ce6e704d64dad006b519c093003e21431541588af02b119f9605b60a55abcc27af6f4385dbb8419796e9746c5d3f770ffc991aea4fa93d555de50c5ccd65afa3bdbafb677b4a7857a039349df6781f98e5b021ec0fdf05e7ea433a36b657a1614d9769d4bdf5f30c68d920dba34d9b5e951881a0333bd6fa8ac8f1a041c088ff25a81ebf0ae00731c62ccc471916c665a0671916626cca7876bee8e475d58e8149bee899a00fab9e4cba40376c4f02e32ccec4b73e1ac754dec16d996cbadb537b06aded2007de54b36031850273ce854fee879818177915a2fc0aab531e6dda52380b6433c987ecdc377137c8ff04c5a9fe27535a9da18df751ce11ed6016a6f6852b3154e4562b99dc7be30d4ec695f5d817f55a8996c76078d3545e0d8c9dc1a3ea808d64f8a9f4e7b4ba9eca439017c9879e15db47bb856217166f775a87da8cda335e9bb6816afa3e7ef15531d67eac379208d29bcefc17bd09ae7c89753bcc2ba3ae45ed287d9086b1a351f9f43653caacb520774a37d72bfe3e916fa07f488d57e175475dd1f2a1cd8097541b1de007a2bfacf4fa496dce553bed909353734cb59abfe973788f47356f5373e1bcf50f5b79e3aadfaea9752d57fe2dcf9fc3f12449b642560b406936edc87a9a671d8e190c142adc70a752b6af9471583f369622624cce82468738fe94c6406dfdfe1e9b48ef2b0425a34e44217bb491b90b27b9207efa91e5c97ed5aa70a33ce349dda21468d3139817c452d0fe7a00e073317be1d1c72c039ad2587fa855c17659e39d1f79d74ff32f4ec388d37d4a480bd3b3210c3cdbbc06fec26a0f4c7e8a0bca9d515562c4d70b60e1083ca7ca9c34c044adc4c2216660612dfdd3bda686c4094d27900e635e06a76aa5212403785560f666098afe71039881a70b7427916238d0ce063364f5493f53b1dc605f4d7c36fcce537878f107b04917eb22c46dffedace9fdcc8cefc6e7f0c4f117fec17ce9c22be8def6ead4737de9c5f270ecf3711e7f469e4099cfced8fa738c1bd706e020efd7582d8d838bf893c79f915fdb97780f88914d1bf1bb17301105fdf7aeade88f7f1f57753c4d5dd88fa73e0eb1ba39f3de9eb6f03f7f1731667e3c5db9f449cc71fbd27ce131b6fdefc2d82589f60e66892cc35f72f6e96fbd100cf22836772742127f60fe00e9dcf51cc359ab996a7733551beb8d91d1c2c0607d772c5b06f8d0657e9edfc7681ce5dee99f3209a5fb9b8a9f40f62b87bfaf0f3f8b8f1f471e8ddc790543e7fcf53c8d3e79bd160155d1d45bef76cce1af50f6683e87a4fae6eede169e57b5eb8909d7c3f70fecfbef5a31fca3d417c43bdf3d01fbcbd3dffe487b9bff32e2f7fe7a7be9afbe36f7ce017ef7cfda5f9c75efcf1e6ab8f7e977ef83bd75efccdeffdedaf7fe2d5c68d5bc27f773f4fccbff6a12b2ffcfbfcab46e195debbcd7f78fc8b9ff9af2fffc86bbf97ffc0affcf42fc7413ffac3e056d278f89b2f49d35ffa4fe537aa7ffe730f73af7df07bcf5cdebdfd6b7f49bcf4c17f79cf67e4e1e0574f24eb3e49ff549af4c5e964bd60a5c97a6ce3cde1ebc40fc43971dc7ef9f6ad1467fec371d8d123f73b648fd243f64f9f164ffd39c0b261647e70f8cf19ca9de0be28f314e5d63d511e03947fca50fee323f7f5e9d3a94fd1199f6c40fa192cfee177d0a7ff055f2af0bdf5180000")) + +pkts = sniff(offline=io.BytesIO(pcap), session=TCPSession) + +assert HTTPRequest in pkts[3] +assert pkts[3].Method == b"POST" +assert len(pkts[3].load) == 4491 + +assert HTTPResponse in pkts[8] +assert pkts[8].Http_Version == b'HTTP/1.1' +assert len(pkts[8].load) == 134 + = HTTP decompression (brotli) ~ brotli @@ -173,7 +225,7 @@ filename = scapy_path("/test/pcaps/http_tcp_psh.pcap.gz") pkts = sniff(offline=filename, session=TCPSession) -assert len(pkts) == 15 +assert len(pkts) == 14 # Verify a split header exists in the packet assert pkts[5].User_Agent == b'example_user_agent' @@ -185,7 +237,7 @@ assert int(pkts[7][HTTP].Content_Length.decode()) == len(pkts[7][Raw].load) 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' +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\nContent-Length: 0\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 @@ -212,11 +264,152 @@ for i in range(3, 10): = Test chunked with gzip conf.contribs["http"]["auto_compression"] = False +conf.contribs["http"]["auto_chunk"] = False z = b'\x1f\x8b\x08\x00S\\-_\x02\xff\xb3\xc9(\xc9\xcd\xb1\xcb\xcd)\xb0\xd1\x07\xb3\x00\xe6\xedpt\x10\x00\x00\x00' a = IP(dst="1.1.1.1", src="2.2.2.2")/TCP(seq=1)/HTTP()/HTTPResponse(Content_Encoding="gzip", Transfer_Encoding="chunked")/(b"5\r\n" + z[:5] + b"\r\n") b = IP(dst="1.1.1.1", src="2.2.2.2")/TCP(seq=len(a[TCP].payload)+1)/HTTP()/(hex(len(z[5:])).encode()[2:] + b"\r\n" + z[5:] + b"\r\n0\r\n\r\n") xa, xb = IP(raw(a)), IP(raw(b)) conf.contribs["http"]["auto_compression"] = True +conf.contribs["http"]["auto_chunk"] = True c = sniff(offline=[xa, xb], session=TCPSession)[0] -assert gzip_decompress(z) == c.load +import gzip +assert gzip.decompress(z) == c.load + ++ Test HTTP client/server + += Util function to launch HTTP_server +~ http-client https-client + +from scapy.layers.http import ( + HTTP_Server, + HTTPS_Server, + HTTP_AUTH_MECHS, +) + +class run_httpserver: + def __init__(self, mech=None, ssp=None, ssl=False, **kwargs): + self.server = None + self.mech = mech + self.ssp = ssp + self.ssl = ssl + self.kwargs = kwargs + def __enter__(self): + if self.ssl: + cls = HTTPS_Server + self.kwargs["cert"] = scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem") + self.kwargs["key"] = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") + print("@ Starting https server") + else: + cls = HTTP_Server + print("@ Starting http server") + # Start server + self.server = cls.spawn( + 8080, + iface=conf.loopback_name, + mech=self.mech, ssp=self.ssp, + bg=True, + **self.kwargs, + ) + # wait for it to start + for i in range(10): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + try: + sock.connect(("127.0.0.1", 8080)) + break + except Exception: + time.sleep(0.5) + finally: + sock.close() + else: + raise TimeoutError + print("@ Server started !") + def __exit__(self, exc_type, exc_value, traceback): + print("@ Stopping http server !") + try: + self.server.shutdown(socket.SHUT_RDWR) + except OSError: + pass + self.server.close() + if traceback: + # failed + print("\nTest failed.") + raise traceback + print("@ http server stopped !") + + += HTTP - HTTP_client fails to ask HTTP_server that required authentication +~ http-client + +from scapy.layers.http import HTTP_Client + +with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")})): + client = HTTP_Client() + resp = client.request("http://127.0.0.1:8080") + client.close() + +assert resp.Status_Code == b"401" + += HTTP - HTTP_client asks HTTP_server with NTLMSSP +~ http-client + +from scapy.layers.http import HTTP_Client + +with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")})): + client = HTTP_Client( + HTTP_AUTH_MECHS.NTLM, + ssp=NTLMSSP(UPN="user", PASSWORD="password"), + ) + resp = client.request("http://127.0.0.1:8080") + client.close() + +assert resp.load == b'

OK

' + += HTTP - HTTP_Server with native python client with Basic auth +~ http-client + +import urllib.request +from scapy.layers.http import HTTP_Client + +# https://docs.python.org/3/howto/urllib2.html#id5 (this is so complicated...) +password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() +password_mgr.add_password(None, '127.0.0.1:8080', "user", "password") +handler = urllib.request.HTTPBasicAuthHandler(password_mgr) +opener = urllib.request.build_opener(handler) + +with run_httpserver(mech=HTTP_AUTH_MECHS.BASIC, BASIC_IDENTITIES={"user": "password"}): + with opener.open('http://127.0.0.1:8080/') as f: + html = f.read().decode('utf-8') + +assert html == "

OK

" + + += HTTP - HTTP_Server with native python client without auth +~ http-client + +import urllib.request + +with run_httpserver(mech=HTTP_AUTH_MECHS.NONE): + with urllib.request.urlopen('http://127.0.0.1:8080/') as f: + html = f.read().decode('utf-8') + +assert html == "

OK

" + ++ Test HTTPS client/server + += HTTPS - HTTPS_client asks HTTPS_server with NTLMSSP and CBT +~ https-client + +from scapy.layers.http import HTTP_Client + +with run_httpserver(mech=HTTP_AUTH_MECHS.NTLM, ssp=NTLMSSP(IDENTITIES={"user": MD4le("password")}), ssl=True): + client = HTTP_Client( + HTTP_AUTH_MECHS.NTLM, + ssp=NTLMSSP(UPN="user", PASSWORD="password"), + no_check_certificate=True, + ) + resp = client.request("https://127.0.0.1:8080") + client.close() + +assert resp.load == b'

OK

' diff --git a/test/scapy/layers/inet.uts b/test/scapy/layers/inet.uts index 6a3a0c7628c..ab5cbd56781 100644 --- a/test/scapy/layers/inet.uts +++ b/test/scapy/layers/inet.uts @@ -82,6 +82,16 @@ sniff(offline=tmp_file, session=IPSession, prn=callback) assert len(dissected_packets) == 1 assert raw(dissected_packets[0]) == raw(packet) += IPSession - contains non-IP packets + +pkts = fragment(IP(dst="10.0.0.5")/ICMP()/("X"*1500)) +pkts.insert(1, ARP()) +assert len(pkts) == 3 + +pkts = sniff(offline=pkts, session=IPSession) +assert len(pkts) == 2 +assert pkts[1].load == b"X" * 1500 + = StringBuffer buffer = StringBuffer() @@ -97,6 +107,10 @@ assert bytes_hex(bytes(buffer)) == b'0070696e6b696500706965' assert len(buffer) == 11 assert buffer +buffer = StringBuffer() +buffer.append(b"") +assert not buffer +assert bytes(buffer) == b"" ############ ############ @@ -150,6 +164,25 @@ assert len(frags2) == 2 assert len(frags2[0]) == 20 + paylen - paylen % 8 assert len(frags2[1]) == 20 + 1 + paylen % 8 += fragment() with fragsize lower than 8 +paylen = 5 +fragsize = paylen +frags1 = fragment(IP() / ("X" * paylen), paylen) +assert len(frags1) == 1 +assert bytes(frags1[0].payload) == b"X" * paylen + +fragsize = paylen + 1 +frags2 = fragment(IP() / ("X" * paylen), fragsize) +assert len(frags2) == 1 +assert bytes(frags2[0].payload) == b"X" * paylen + +paylen = 16 +fragsize = 5 +frags3 = fragment(IP() / ("X" * paylen), fragsize) +assert len(frags3) == 2 +assert bytes(frags3[0].payload) == b"X" * 8 +assert bytes(frags3[1].payload) == b"X" * 8 + = defrag() nonfrag, unfrag, badfrag = defrag(frags) assert not nonfrag @@ -161,7 +194,7 @@ 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)) +assert bytes(defrags[0]) == bytes(pkt) = defragment() uses timestamp of last fragment payloadlen, fragsize = 100, 8 @@ -191,10 +224,8 @@ b = base64.b64decode('bnmYJ63mREVTUwEACABFAAV0U8UgrDIR+kEEAgIECv0DxApz1F5olFRytj 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 +with no_debug_dissector(): + plist = PacketList([Ether(x) for x in [a, b, c, d]]) left, defragmented, errored = defrag(plist) assert len(left) == 1 @@ -332,7 +363,7 @@ assert opt.rnextkeyid == 2 assert opt.mac == b"FAKE" = TCP Authentication Option: parse from TCP -p = IP(bytes(bytearray.fromhex("45e0004cdd0f4000ff06bf6b0a0b0c0dac1b1c1de9d700b3fbfbab5a00000000e002ffffcac40000020405b4010303080402080a00155ab7000000001d103d542ee437c6f8ede6d7c4d602e7"))) +p = IP(bytes.fromhex("45e0004cdd0f4000ff06bf6b0a0b0c0dac1b1c1de9d700b3fbfbab5a00000000e002ffffcac40000020405b4010303080402080a00155ab7000000001d103d542ee437c6f8ede6d7c4d602e7")) tcpao = get_tcpao(p[TCP]) assert isinstance(tcpao, TCPAOValue) assert tcpao.keyid == 61 @@ -361,6 +392,13 @@ pkt = fuzz(pkt) options = pkt.options._fix() options += TCP random options - MD5 (#GH3777) +random.seed(0x2813) +pkt = TCP(options=RandTCPOptions()._fix()) +assert pkt.options[0][0] == "MD5" +assert pkt.options[0][1] == (b'\xe3\xa0,\xdc\xe4\xae\x87\x18\xad{\xab\xd0b\x12\x9c\xd6',) +assert TCP(bytes(pkt)).options[0][0] == "MD5" + = IP, TCP & UDP checksums (these tests highly depend on default values) pkt = IP() / TCP() bpkt = IP(raw(pkt)) @@ -414,6 +452,9 @@ pkt = IP(len=28, ihl=5) / UDP() bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x7cce and bpkt.payload.chksum == 0x0172 +* Invalid territory +conf.debug_dissector = False + pkt = IP() / UDP() / ("A" * 10) bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x7cc4 and bpkt.payload.chksum == 0xbb17 @@ -438,6 +479,8 @@ pkt = IP(len=42, ihl=6, options=[IPOption_RR()]) / UDP() / ("A" * 10) bpkt = IP(raw(pkt)) assert bpkt.chksum == 0x70bd and bpkt.payload.chksum == 0xbb17 +conf.debug_dissector = True + = IP with forced-length 0 p = IP()/TCP() p[IP].len = 0 @@ -453,6 +496,74 @@ pkt2.len = 0 pkt3 = IP(raw(pkt2)) assert pkt3.load == data += TCPSession: test tcp_reassemble with variable orders + +class CustomPacket(Packet): + fields_desc = [ + ByteField("len", 0), + StrLenField("a", 0, length_from=lambda pkt: pkt.len - 1), + ] + @classmethod + def tcp_reassemble(cls, data, metadata, session): + length = struct.unpack("!B", data[:1])[0] + if len(data) < length: + return None + return CustomPacket(data) + + +# above we have a CustomPacket that is X bytes long. +bind_layers(TCP, CustomPacket, sport=12345) + +with no_debug_dissector(reverse=True): + # incremental order + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd", "incremental failure" + # same with a pcapng + tmp_file = get_temp_file() + wrpcap(tmp_file, [ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ]) + pkts = sniff(offline=tmp_file, session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd", "pcapng failure" + # messed up order: fragments 2 and 3 arrive in the wrong order + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x05a", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcd", "messed up order 1 failure" + # messed up order: fragment 1 arrives not in first position + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=6)/"e", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x06a", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcde", "messed up order 2 failure" + # retransmitted packets + pkts = sniff(offline=[ + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=6)/"e", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=4)/"c", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=6)/"e", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=3)/"b", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=5)/"d", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=6)/"e", + IP(dst="1.1.1.1", src="2.2.2.2")/TCP(sport=12345, dport=12, seq=1)/b"\x06a", + ], session=TCPSession) + assert pkts[0][CustomPacket].a == b"abcde", "retransmitted failure" + +split_layers(TCP, CustomPacket, sport=12345) + = Layer binding @@ -476,6 +587,82 @@ assert isinstance(pkt, UDP) and pkt.dport == 5353 pkt = pkt.payload assert isinstance(pkt, DNS) and isinstance(pkt.payload, NoPayload) += Layer binding with show() +* getmacbyip must only be called when building + +from unittest import mock + +def _err(*_): + raise ValueError + +with mock.patch("scapy.layers.l2.getmacbyip", side_effect=_err): + with mock.patch("scapy.layers.inet.getmacbyip", side_effect=_err): + # ARP who-has should never call getmacbyip + pkt1 = Ether() / ARP(pdst="10.0.0.1") + pkt1.show() + bytes(pkt1) + # IP should only call getmacbyip when building + pkt2 = Ether() / IP(dst="10.0.0.1") + pkt2.show() + try: + bytes(pkt2) + assert False, "Should have called getmacbyip" + except ValueError: + pass + += GRE binding tests + +* Test GRE-in-IP +pkt = Ether(raw(Ether()/IP()/GRE()/IP()/UDP())) +assert isinstance(pkt, Ether) +pkt = pkt.payload +assert isinstance(pkt, IP) and pkt.proto == 47 +pkt = pkt.payload +assert isinstance(pkt, GRE) and pkt.proto == 0x0800 +pkt = pkt.payload +assert isinstance(pkt, IP) +pkt = pkt.payload +assert isinstance(pkt, UDP) + +* Test GRE-in-IPv6 +pkt = Ether(raw(Ether()/IPv6()/GRE()/IPv6()/UDP())) +assert isinstance(pkt, Ether) +pkt = pkt.payload +assert isinstance(pkt, IPv6) and pkt.nh == 47 +pkt = pkt.payload +assert isinstance(pkt, GRE) and pkt.proto == 0x86dd +pkt = pkt.payload +assert isinstance(pkt, IPv6) +pkt = pkt.payload +assert isinstance(pkt, UDP) + +* Test GRE-in-UDP +pkt = Ether(raw(Ether()/IP()/UDP()/GRE()/IP()/UDP())) +assert isinstance(pkt, Ether) +pkt = pkt.payload +assert isinstance(pkt, IP) +pkt = pkt.payload +assert isinstance(pkt, UDP) and pkt.dport == 4754 +pkt = pkt.payload +assert isinstance(pkt, GRE) and pkt.proto == 0x0800 +pkt = pkt.payload +assert isinstance(pkt, IP) +pkt = pkt.payload +assert isinstance(pkt, UDP) + +* Test GRE-in-UDP (IPv6) +pkt = Ether(raw(Ether()/IPv6()/UDP()/GRE()/IPv6()/UDP())) +assert isinstance(pkt, Ether) +pkt = pkt.payload +assert isinstance(pkt, IPv6) +pkt = pkt.payload +assert isinstance(pkt, UDP) and pkt.dport == 4754 +pkt = pkt.payload +assert isinstance(pkt, GRE) and pkt.proto == 0x86dd +pkt = pkt.payload +assert isinstance(pkt, IPv6) +pkt = pkt.payload +assert isinstance(pkt, UDP) ############ ############ @@ -488,7 +675,8 @@ value == 26908070 test.i2repr("", value) == '7:28:28.70' = IPv4 - UDP null checksum -IP(raw(IP()/UDP()/Raw(b"\xff\xff\x01\x6a")))[UDP].chksum == 0xFFFF +with no_debug_dissector(): + IP(raw(IP()/UDP()/Raw(b"\xff\xff\x01\x6a")))[UDP].chksum == 0xFFFF = IPv4 - (IP|UDP|TCP|ICMP)Error query = IP(dst="192.168.0.1", src="192.168.0.254", ttl=1)/UDP()/DNS() @@ -507,6 +695,10 @@ query = IP(dst="192.168.0.1", src="192.168.0.254", ttl=1)/ICMP()/"scapy" answer = IP(dst="192.168.0.254", src="192.168.0.2")/ICMP(type=11)/IPerror(dst="192.168.0.1", src="192.168.0.254", ttl=0)/ICMPerror()/"scapy" assert answer.answers(query) == True += IPv4 - TCPError parsing +pkt = Ether(bytes.fromhex('005056a4302ffcbd676360c908004500003800000000f80164b6682ce6b70ad504560b004f410000000045000028400e00000106fdae0ad50456681204d7f73100507d4430f8')) +assert TCPerror in pkt and pkt[TCPerror].sport == 63281 and pkt[TCPerror].dport == 80 + = IPv4 - mDNS a = IP(dst="224.0.0.251") assert a.hashret() == b"\x00" @@ -524,7 +716,7 @@ for x in ICMP(type=range(0,40),code=range(0,40)): (IP()/x).hashret() = IPv4 - traceroute utilities -ip_ttl = [("192.168.0.%d" % i, i) for i in six.moves.range(1, 10)] +ip_ttl = [("192.168.0.%d" % i, i) for i in range(1, 10)] tr_packets = [ (IP(dst="192.168.0.1", src="192.168.0.254", ttl=ttl)/TCP(options=[("Timestamp", "00:00:%.2d.00" % ttl)])/"scapy", IP(dst="192.168.0.254", src=ip)/ICMP(type=11)/IPerror(dst="192.168.0.1", src="192.168.0.254", ttl=0)/TCPerror()/"scapy") @@ -565,13 +757,13 @@ def test_summary(): "IP / ICMP 192.168.0.9 > 192.168.0.254 time-exceeded " "ttl-zero-during-transit / IPerror / TCPerror / " "Raw" % (ftp_data, http) in result_summary - for ftp_data in ['21', 'ftp_data'] + for ftp_data in ['20', 'ftp_data'] for http in ['80', 'http', 'www_http', 'www'] )) test_summary() -import mock +from unittest import mock import scapy.libs.matplot @mock.patch("scapy.libs.matplot.plt") @@ -610,7 +802,7 @@ assert "192.168.0.254" not in [p[IP].src for p in new_pl] = IPv4 - reporting ~ netaccess -import mock +from unittest import mock @mock.patch("scapy.layers.inet.sr") def test_report_ports(mock_sr): @@ -629,7 +821,7 @@ def test_IPID_count(): random.seed(0x2807) IPID_count([(IP()/UDP(), IP(id=random.randint(0, 65535))/UDP()) for i in range(3)]) result_IPID_count = cmco.get_output() - lines = result_IPID_count.split("\n") + lines = [x.strip() for x in result_IPID_count.split("\n")] assert len(lines) == 5 assert(lines[0] in ["Probably 3 classes: [4613, 53881, 58437]", "Probably 3 classes: [9103, 9227, 46399]"]) @@ -644,3 +836,82 @@ assert no_sr[UDP].chksum == sr[UDP].chksum sr = IP(raw(IP(options=[IPOption_LSRR(routers=["1.1.1.1"]), IPOption_SSRR(routers=["8.8.8.8"])])/UDP()/DNS())) assert no_sr[UDP].chksum != sr[UDP].chksum + +# GH4174 +sr = Ether(src="de:ad:be:ef:aa:55", dst="ca:fe:00:00:00:00")/IP(src="20.0.0.1",dst="100.0.0.1")/ \ + IP(src="20.0.0.1",dst="100.0.0.1", options=[IPOption_SSRR(copy_flag=1, pointer=4, routers=["1.1.1.1", "8.8.8.8"])])/ \ + UDP(sport=1111, dport=2222) / VXLAN() / \ + Ether(src="de:ad:be:ef:aa:55", dst="ca:fe:00:00:00:00")/IP(src="20.0.0.1",dst="100.0.0.1") / \ + TCP() +bytes(sr[UDP]) +assert sr[IP:2].dst == "100.0.0.1" + + +############### +############### ++ ICMPv4 extensions + += Build ICMP extension from scratch + +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' + +assert bytes(pkt) == b'E\x00\x00\xb0\x00\x01\x00\x00@\x01|J\x7f\x00\x00\x01\x7f\x00\x00\x01\x0b\x00\x12/\x00\x00\x00\x00E\x00\x00(\x00\x01\x00\x00@\x11]\xbb\x0c\x04\x04\x04\x0c\x01\x01\x01\xa5K\x82\xa0\x00\x14\xba\xd0\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\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 \x00u\x00\x00\x10\x02\x0e\x00\x00\x00\x00\x00\x00\x00\x00\x03hey' + += Check dissection and rebuild of MPLS ICMPv4 extension + +# GH4281 + +load_contrib("mpls") +pkt = Ether(b'\x00\x15]\x94AY\x00\x15]\x07\xcb\x04\x08\x00E\x00\x00\xb0?2\x00\x00\xe6\x01\x1b\xabh,\x1f\x1d\xac\x1cF\n\x0b\x00Ll\x00\x11\x00\x00E \x00<\x96\xdf\x00\x00\x02\x11\xa7\xc6\xac\x1cF\n(Q_t\xb8-\x82\xb3\x00(xt@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\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\x00\x00\x00\x00\x00 \x00\x02\xff\x00\x10\x01\x01\tp2\x01\x05\xde\xd2\x01\x05\x9c\xc3\x02') + +assert isinstance(pkt[ICMP].ext, ICMPExtension_Header) +assert ICMPExtension_MPLS in pkt[ICMP].ext +assert all(isinstance(x, MPLS) for x in pkt[ICMP].ext.stack) +assert [x.label for x in pkt[ICMP].ext.stack[0].iterpayloads()] == [38659, 24045, 22988] + +# Build +pkt.clear_cache() +pkt.ext.chksum = None # Check that chksum rebuilds +pkt[IP].chksum = None +assert bytes(pkt) == b'\x00\x15]\x94AY\x00\x15]\x07\xcb\x04\x08\x00E\x00\x00\xb0?2\x00\x00\xe6\x01\x1b\xabh,\x1f\x1d\xac\x1cF\n\x0b\x00Ll\x00\x11\x00\x00E \x00<\x96\xdf\x00\x00\x02\x11\xa7\xc6\xac\x1cF\n(Q_t\xb8-\x82\xb3\x00(xt@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_\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\x00\x00\x00\x00\x00 \x00\x02\xff\x00\x10\x01\x01\tp2\x01\x05\xde\xd2\x01\x05\x9c\xc3\x02' + += ICMPv4 extension - Other dissection example + +# GH1773 + +load_contrib("mpls") +pkt = Ether(b'\x00\x1cs\x03\x12\x06t\x83\xef\x00\n\xd5\x08\x00E\x00\x00\xa8H\x1e\x00\x00\xfb\x01\xf0\xe3\xc0\xa8\x02\x01\xc0\xa8\x03\x01\x0b\x00rr\x00 \x00\x00E\x00\x00 b) == True -= ICMPv6EchoRequest and ICMPv6EchoReply - live answers() use Net6 += ICMPv6EchoRequest and ICMPv6EchoReply - answers() test 7 - IPv6ExtHdrDestOpt +b = IPv6(b'`\x0f\\\xe3\x00\x08:@\xfe\x80\x00\x00\x00\x00\x00\x00\x02PV\xff\xfe\x84\x1c\x14\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\x81\x00\r\xad\x00\x00\x00\x00') +a = IPv6(b'`\x00\x00\x00\x00\x10<\xff\xfe\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00&\xfe\x80\x00\x00\x00\x00\x00\x00\x02PV\xff\xfe\x84\x1c\x14:\x00\x00\x00\x00\x00\x00\x00\x80\x00\x0e\xad\x00\x00\x00\x00') +assert a.hashret() == b.hashret() +assert b.answers(a) + += ICMPv6EchoRequest and ICMPv6EchoReply - answers() test 8 - (live) use Net6 ~ netaccess ipv6 a = IPv6(dst="www.google.com")/ICMPv6EchoRequest() @@ -569,6 +578,25 @@ raw(RouterAlert(optlen=3, value=0xffff)) == b'\x05\x03\xff\xff' a=RouterAlert(b'\x05\x03\xff\xff') a.otype == 0x05 and a.optlen == 3 and a.value == 0xffff +############ +############ ++ Test RPL Option (RFC 6553) + += RplOption - Basic Instantiation +raw(RplOption()) == b'c\x04\x00\x00\x00\x00' + += RplOption - Basic Dissection +a=RplOption(b'c\x04\x00\x00\x00\x00') +a.otype == 0x63 and a.optlen == 4 and a.Down == False and a.RankError == 0 and a.ForwardError == 0 and a.RplInstanceId == 0 and a.SenderRank == 0 + += RplOption - Instantiation with specific values +a=RplOption(RplInstanceId=0x1e, SenderRank=0x800) +a.otype == 0x63 and a.optlen == 4 and a.Down == False and a.RankError == 0 and a.ForwardError == 0 and a.RplInstanceId == 0x1e and a.SenderRank == 0x800 + += RplOption - Instantiation with specific values +a=RplOption(Down=True, RplInstanceId=0x1e, SenderRank=0x800) +a.otype == 0x63 and a.optlen == 4 and a.Down == True and a.RankError == 0 and a.ForwardError == 0 and a.RplInstanceId == 0x1e and a.SenderRank == 0x800 +raw(a) == b'c\x04\x80\x1e\x08\x00' ############ ############ @@ -625,6 +653,9 @@ raw(IPv6ExtHdrHopByHop(options=[HAO()])) == b';\x02\x01\x02\x00\x00\xc9\x10\x00\ = IPv6ExtHdrHopByHop - Instantiation with RouterAlert option raw(IPv6ExtHdrHopByHop(options=[RouterAlert()])) == b';\x00\x05\x02\x00\x00\x01\x00' + += IPv6ExtHdrHopByHop - Instantiation with RPL option +raw(IPv6ExtHdrHopByHop(options=[RplOption()])) == b';\x00c\x04\x00\x00\x00\x00' = IPv6ExtHdrHopByHop - Instantiation with Jumbo option raw(IPv6ExtHdrHopByHop(options=[Jumbo()])) == b';\x00\xc2\x04\x00\x00\x00\x00' @@ -777,19 +808,43 @@ b.answers(a) + ICMPv6NDOptUnknown Class Test = ICMPv6NDOptUnknown - Basic Instantiation -raw(ICMPv6NDOptUnknown()) == b'\x00\x02' +b = b'\x00\x01\x00\x00\x00\x00\x00\x00' + +raw(ICMPv6NDOptUnknown()) == b = ICMPv6NDOptUnknown - Instantiation with specific values -raw(ICMPv6NDOptUnknown(len=4, data="somestring")) == b'\x00\x04somestring' +raw(ICMPv6NDOptUnknown(data="somestring")) == b'\x00\x02somestring\x00\x00\x00\x00' = ICMPv6NDOptUnknown - Basic Dissection -a=ICMPv6NDOptUnknown(b'\x00\x02') -a.type == 0 and a.len == 2 +b = b'\x00\x01\x00\x00\x00\x00\x00\x00' + +p = ICMPv6NDOptUnknown(b) +p.type == 0 and p.len == 1 and p.data == b'\x00' * 6 + +p = ICMPv6NDOptUnknown(b + b'\x00') +assert Raw in p and raw(p[Raw]) == b'\x00' + +p = ICMPv6NDOptUnknown(b + b'\x00\x00') +assert raw(p[ICMPv6NDOptUnknown:2]) == b'\x00\x00' = 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" +p = ICMPv6NDOptUnknown(b'\x00\x01string') +assert p.type == 0 and p.len == 1 and p.data == b'string' + +p = ICMPv6NDOptUnknown(b'\x00\x04somestring') +assert p.type == 0 and p.len == 4 and p.data == b'somestring' + += ICMPv6NDOptUnknown - Instantiation/Dissection with unknown option in the middle +b = b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x02somestring\x00\x00\x00\x00%\x01\x00\x00\x00\x00\x00\x00' + +p = ICMPv6NDOptSrcLLAddr()/ICMPv6NDOptUnknown(data='somestring')/ICMPv6NDOptCaptivePortal() +assert raw(p) == b +p = ICMPv6NDOptSrcLLAddr(b)[ICMPv6NDOptUnknown] +assert p.type == 0 and p.len == 2 and p.data == b'somestring\x00\x00\x00\x00' + += ICMPv6NDOptUnknown - fuzz +assert isinstance(fuzz(ICMPv6NDOptUnknown()).type, RandByte) ############ ############ @@ -874,12 +929,16 @@ assert a.pkt == b"" = ICMPv6NDOptRedirectedHdr - Disssection with specific values ~ ICMPv6NDOptRedirectedHdr -a=ICMPv6NDOptRedirectedHdr(b'\x04\xff\x11\x11\x00\x00\x00\x00somerawingthatisnotanipv6pac') +with no_debug_dissector(): + 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') +with no_debug_dissector(): + 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 @@ -993,9 +1052,9 @@ a=ICMPv6NDOptSrcAddrList(b'\t\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\ 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 +with no_debug_dissector(): + 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') + 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' @@ -1021,9 +1080,9 @@ a=ICMPv6NDOptTgtAddrList(b'\n\x05BBBBBB\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\ 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 +with no_debug_dissector(): + 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') + 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' @@ -1198,7 +1257,39 @@ 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." +ICMPv6NDOptDNSSL(searchlist=["home.", "office.", "{"]).mysummary() == "ICMPv6 Neighbor Discovery Option - DNS Search List Option home., office., {" + + +############ +############ ++ ICMPv6NDOptCaptivePortal Class Test + += ICMPv6NDOptCaptivePortal - Basic Instantiation +raw(ICMPv6NDOptCaptivePortal()) == b"\x25\x01\x00\x00\x00\x00\x00\x00" + += ICMPv6NDOptCaptivePortal - Instantiation with captive portal URI +raw(ICMPv6NDOptCaptivePortal(URI="https://example.com")) == b"\x25\x03https://example.com\x00\x00\x00" + += ICMPv6NDOptCaptivePortal - Instantiation where total length is already a multiple of 8 bytes +p = ICMPv6NDOptCaptivePortal(URI="abcdef") +len(p) == 8 and raw(p) == b"\x25\x01abcdef" and ICMPv6NDOptCaptivePortal(raw(p)).URI == b"abcdef" + += ICMPv6NDOptCaptivePortal - Basic Dissection +p = ICMPv6NDOptCaptivePortal(b"\x25\x01\x00\x00\x00\x00\x00\x00") +p.type == 37 and p.len == 1 and p.URI == b"" + += ICMPv6NDOptCaptivePortal - Basic Dissection with captive portal URI +p = ICMPv6NDOptCaptivePortal(b"\x25\x03https://example.com\x00\x00\x00") +p.type == 37 and p.len == 3 and p.URI == b"https://example.com" + += ICMPv6NDOptCaptivePortal - Dissection with zero length +p = ICMPv6NDOptCaptivePortal(b"\x25\x00abcdef\x00\x01") +p.type == 37 and p.len == 0 and p.URI == b"abcdef" +pay = p.payload +assert pay.type == 0 and pay.len == 1 and pay.data == b"" + += ICMPv6NDOptCaptivePortal - Summary Output +ICMPv6NDOptCaptivePortal(URI="https://example.com").mysummary() == "ICMPv6 Neighbor Discovery Option - Captive-Portal Option b'https://example.com'" ############ @@ -1213,6 +1304,32 @@ a=ICMPv6NDOptEFA(b'\x1a\x01\x00\x00\x00\x00\x00\x00') a.type==26 and a.len==1 and a.res == 0 +############ +############ ++ ICMPv6NDOptPREF64 Class Test + += ICMPv6NDOptPREF64 - Basic Instantiation +raw(ICMPv6NDOptPREF64()) == b'\x26\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + += ICMPv6NDOptPREF64 - Basic Dissection +p = ICMPv6NDOptPREF64(b'\x26\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') +assert p.type == 38 and p.len == 2 and p.scaledlifetime == 0 and p.plc == 0 and p.prefix == '::' + += ICMPv6NDOptPREF64 - Instantiation/Dissection with specific values +p = ICMPv6NDOptPREF64(scaledlifetime=225, plc='/64', prefix='2003:da8:1::') +assert raw(p) == b'\x26\x02\x07\x09\x20\x03\x0d\xa8\x00\x01\x00\x00\x00\x00\x00\x00' + +p = ICMPv6NDOptPREF64(raw(p)) +assert p.type == 38 and p.len == 2 and p.scaledlifetime == 225 and p.plc == 1 and p.prefix == '2003:da8:1::' + +p = ICMPv6NDOptPREF64(raw(p) + b'\x00\x00\x00\x00') +assert ICMPv6NDOptUnknown in p and len(p[ICMPv6NDOptUnknown]) == 4 + += ICMPv6NDOptPREF64 - Summary Output +ICMPv6NDOptPREF64(prefix='12:34:56::', plc='/32').mysummary() == "ICMPv6 Neighbor Discovery Option - PREF64 Option 12:34:56::/32" +ICMPv6NDOptPREF64(prefix='12:34:56::', plc=6).mysummary() == "ICMPv6 Neighbor Discovery Option - PREF64 Option 12:34:56::[invalid PLC(6)]" + + ############ ############ + Test Node Information Query - ICMPv6NIQueryNOOP @@ -1694,7 +1811,7 @@ 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 +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 == b"" ############ @@ -1706,7 +1823,7 @@ raw(ICMPv6NIReplyUnknown(nonce=b'\x00'*8)) == b'\x8c\x02\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 +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 == b"" ############ @@ -1787,15 +1904,20 @@ pkts = fragment6(IPv6()/IPv6ExtHdrFragment()/UDP(dport=42, sport=42)/Raw(load="A pkts = [IPv6(raw(p)) for p in pkts] assert defragment6(pkts).plen == 1508 += defragment6 - discard payload +pkt = Ether() / IPv6() / ICMPv6EchoRequest(data='b'*100) +frags = fragment6(pkt, 100) +pkt = defragment6(Ether(raw(frag / Padding(b'a' * 8))) for frag in frags) +assert b'a' not in pkt.data ############ ############ + Test Route6 class = Fake interfaces -IFACES._add_fake_iface("eth0") -IFACES._add_fake_iface("lo") -IFACES._add_fake_iface("scapy0") +conf.ifaces._add_fake_iface("eth0") +conf.ifaces._add_fake_iface("lo") +conf.ifaces._add_fake_iface("scapy0") = Route6 - Route6 flushing conf_iface = conf.iface @@ -1853,7 +1975,7 @@ r6.ifdel("scapy0") = IPv6 - utils -import mock +from unittest import mock @mock.patch("scapy.layers.inet6.get_if_hwaddr") @mock.patch("scapy.layers.inet6.srp1") def test_neighsol(mock_srp1, mock_get_if_hwaddr): @@ -1885,7 +2007,9 @@ from scapy.layers.inet6 import _ICMPv6Error assert _ICMPv6Error().guess_payload_class(None) == IPerror6 assert _ICMPv6Error().hashret() == b'' -= Windows: reset routes properly += reset routes properly + +conf.ifaces.reload() if WINDOWS: from scapy.arch.windows import _route_add_loopback @@ -1952,7 +2076,7 @@ assert a.answers(q) = Define test utilities -import mock +from unittest import mock @mock.patch("scapy.layers.inet6.sniff") @mock.patch("scapy.layers.inet6.sendp") @@ -2832,7 +2956,7 @@ p[MIP6MH_BE].cksum=0xba10 and p[MIP6MH_BE].len == 1 and len(p[MIP6MH_BE].options + TracerouteResult6 = get_trace() -ip6_hlim = [("2001:db8::%d" % i, i) for i in six.moves.range(1, 12)] +ip6_hlim = [("2001:db8::%d" % i, i) for i in 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") diff --git a/test/scapy/layers/ipsec.uts b/test/scapy/layers/ipsec.uts index abdbb27fe39..0af1eefdc0b 100644 --- a/test/scapy/layers/ipsec.uts +++ b/test/scapy/layers/ipsec.uts @@ -1506,6 +1506,31 @@ try: except IPSecIntegrityError as err: err +####################################### += IPv4 / ESP - Transport - AES-CBC - HMAC-SHA2-256-128 -- ESN + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('hello world') +p = IP(raw(p)) +p + +enc_key = bytes.fromhex("85ee354b4675a9c5d16e3d6f4118043b") +auth_key = bytes.fromhex("6f79bf94da7dde3c86009934d9258f1b3fc2f5382aca9c9cb8e216eed235f34c") + +sa = SecurityAssociation(ESP, spi=0xcf54ccdf, crypt_algo='AES-CBC', + crypt_key=enc_key, + auth_algo='SHA2-256-128', auth_key=auth_key, + esn_en=True, esn=68) +e = sa.encrypt(p, iv=bytes.fromhex("11223344112233441122334411223344")) + + +assert bytes(e) == bytes.fromhex("4500006c000100004032745a0101010102020202cf54ccdf0000000111223344112233441122334411223344f5bda519c9ae64f283f0fc18a8d253eca8b34c2120c8958a97ec9d8e67756da2523fce9b5541c57fddf090afc2bfd97e8703203953f853eb61482e4c1384d4c8") + +* integrity verification should pass +d = sa.decrypt(e) +d + ####################################### = IPv4 / ESP - Transport - AES-GCM - NULL @@ -1683,6 +1708,183 @@ try: except IPSecIntegrityError as err: err +####################################### += IPv4 / ESP - Transport - AES-NULL-GMAC - NULL + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-NULL-GMAC', crypt_key=b'16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +assert e.proto == socket.IPPROTO_ESP +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +* AES-NULL-GMAC is integrity only, the original packet payload should be readable +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption original packet should be preserved +assert d[TCP] == p[TCP] + +# Generated with Linux 5.15.0-1034-azure #41-Ubuntu +# ip xfrm state add src 10.125.0.2 dst 10.125.0.1 proto esp spi 0x222 reqid 1 \ +# mode tunnel aead 'rfc4543(gcm(aes))' '0x3136627974656b65792b34627974656e6f6e6365' 128 flag align4 +ref = IP() \ + / ESP(spi=0x222, + data=b'\x54\x70\x6c\x6a\x9f\xba\xa6\x18\x45\x00\x00\x54\xbc\x53\x00\x00' + b'\x40\x01\xa9\x59\x0a\x7d\x00\x01\x0a\x7d\x00\x02\x00\x00\xad\x53' + b'\xa8\x83\x00\x01\x02\xe6\x09\x64\x00\x00\x00\x00\xd9\x0a\x06\x00' + b'\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b' + b'\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b' + b'\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x01\x02\x02\x04' + b'\x9b\x76\x32\x30\xf6\x49\x92\xa8\x8f\x6a\x20\x87\x2c\x74\x0c\x18', + seq=22) + +d_ref = sa.decrypt(ref) +d_ref + +* Check for ICMP layer in decrypted reference +assert d_ref.haslayer(ICMP) + +####################################### += IPv4 / ESP - Transport - AES-NULL-GMAC - NULL -- ESN + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-NULL-GMAC', crypt_key=b'16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None, esn_en = True, esn = 0x1) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +assert e.proto == socket.IPPROTO_ESP +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +* AES-NULL-GMAC is integrity only, the original packet payload should be readable +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption original packet should be preserved +assert d[TCP] == p[TCP] + +# Generated with Linux 5.15.0-1034-azure #41-Ubuntu +# ip xfrm state add src 10.125.0.2 dst 10.125.0.1 proto esp spi 0x222 reqid 1 replay-oseq-hi 0x1 \ +# mode tunnel aead 'rfc4543(gcm(aes))' '0x3136627974656b65792b34627974656e6f6e6365' 128 flag align4 esn +ref = IP() \ + / ESP(spi=0x222, + data=b'\x43\xe6\xa1\xce\x70\x9d\x67\xf4\x45\x00\x00\x54\x2e\x4a\x40\x00' + b'\x40\x01\xf7\x62\x0a\x7d\x00\x02\x0a\x7d\x00\x01\x08\x00\xd3\x32' + b'\x8f\x4c\x00\x02\x8d\xec\x09\x64\x00\x00\x00\x00\x3c\x5b\x03\x00' + b'\x00\x00\x00\x00\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b' + b'\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b' + b'\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x01\x02\x02\x04' + b'\x76\xd4\x93\x90\x75\xee\x3f\xa3\xf3\xcf\xcc\x27\xf5\x5b\x12\xb6', + seq=5) + +d_ref = sa.decrypt(ref) +d_ref + +* Check for ICMP layer in decrypted reference +assert d_ref.haslayer(ICMP) + + +####################################### + += IPv4 / ESP - Transport - AES-NULL-GMAC - NULL - altered packet + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-NULL-GMAC', crypt_key=b'16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +assert e.proto == socket.IPPROTO_ESP +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +* AES-NULL-GMAC is integrity only, the original packet payload should be readable +assert b'testdata' in e[ESP].data + +* simulate the alteration of the packet before decryption +e[ESP].seq += 1 + +* integrity verification should fail +try: + d = sa.decrypt(e) + assert False +except IPSecIntegrityError as err: + err + +####################################### + += IPv4 / ESP - Transport - AES-NULL-GMAC - NULL - altered packet -- ESN + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='AES-NULL-GMAC', crypt_key=b'16bytekey+4bytenonce', + auth_algo='NULL', auth_key=None, esn_en = True, esn = 0x200) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +assert e.proto == socket.IPPROTO_ESP +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +* AES-NULL-GMAC is integrity only, the original packet payload should be readable +assert b'testdata' in e[ESP].data + +* simulate the alteration of the packet before decryption +* integrity verification should fail +try: + d = sa.decrypt(e, esn = 0x201) + assert False +except IPSecIntegrityError as err: + err + ####################################### = IPv4 / ESP - Transport - AES-CCM - NULL ~ crypto_advanced @@ -3100,6 +3302,129 @@ try: except IPSecIntegrityError as err: err +############################################################################### ++ IPv4 / UDP / ESP - NAT-Traversal + +####################################### += IPv4 / UDP / ESP - NAT-Traversal - Tunnel +~ -crypto + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='NULL', crypt_key=None, + auth_algo='NULL', auth_key=None, + tunnel_header=IP(src='11.11.11.11', dst='22.22.22.22'), + nat_t_header=UDP(dport=5000)) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +* after encryption packet should be encapsulated with the given ip tunnel header +assert e.src == '11.11.11.11' and e.dst == '22.22.22.22' +assert e.chksum != p.chksum +* the encrypted packet should have an UDP layer +assert e.proto == socket.IPPROTO_UDP +assert e.haslayer(UDP) +assert e[UDP].sport == 4500 +assert e[UDP].dport == 5000 +assert e[UDP].chksum == 0 +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption the original packet payload should be unaltered +assert d[TCP] == p[TCP] + +####################################### += IPv4 / UDP / ESP - NAT-Traversal - Transport +~ -crypto + +import socket + +p = IP(src='1.1.1.1', dst='2.2.2.2') +p /= TCP(sport=45012, dport=80) +p /= Raw('testdata') +p = IP(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='NULL', crypt_key=None, + auth_algo='NULL', auth_key=None, + nat_t_header=UDP(dport=5000)) + +e = sa.encrypt(p) +e + +assert isinstance(e, IP) +assert e.src == '1.1.1.1' and e.dst == '2.2.2.2' +assert e.chksum != p.chksum +* the encrypted packet should have an UDP layer +assert e.proto == socket.IPPROTO_UDP +assert e.haslayer(UDP) +assert e[UDP].sport == 4500 +assert e[UDP].dport == 5000 +assert e[UDP].chksum == 0 +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi +assert b'testdata' in e[ESP].data + +d = sa.decrypt(e) +d + +* after decryption the original packet payload should be unaltered +assert d[TCP] == p[TCP] + +############################################################################### += IPv6 / ESP - NAT-Traversal - Transport +~ -crypto + +import socket + +p = IPv6(src='11::22', dst='22::11') +p /= TCP(sport=3333, dport=55) +p /= Raw('testdata') +p = IPv6(raw(p)) +p + +sa = SecurityAssociation(ESP, spi=0x222, + crypt_algo='NULL', crypt_key=None, + auth_algo='NULL', auth_key=None, + nat_t_header=UDP(dport=5000)) + +e = sa.encrypt(p) +e + +assert isinstance(e, IPv6) +assert e.src == '11::22' and e.dst == '22::11' +assert e.chksum != p.chksum +* the encrypted packet should have an UDP layer +assert e.nh == socket.IPPROTO_UDP +assert e.haslayer(UDP) +assert e[UDP].sport == 4500 +assert e[UDP].dport == 5000 +assert e[UDP].chksum == 0 +assert e.haslayer(ESP) +assert not e.haslayer(TCP) +assert e[ESP].spi == sa.spi + +d = sa.decrypt(e) +d + +* after decryption the original packet payload should be unaltered +assert d[TCP] == p[TCP] +assert not d.haslayer(UDP) +assert d[Raw] == p[Raw] ############################################################################### + IPv6 / ESP diff --git a/test/scapy/layers/isakmp.uts b/test/scapy/layers/isakmp.uts index 2cb3cbf2cca..ea35d58eae2 100644 --- a/test/scapy/layers/isakmp.uts +++ b/test/scapy/layers/isakmp.uts @@ -4,32 +4,90 @@ ############ ############ + ISAKMP tests +~ ISAKMP + += ISAKMP - Phase 1 - Aggressive Security Association dissection +pkt = UDP(b'\x01\xf4\x01\xf4\x02\xf0\x01\xca/\xa8\xd0\xc9\x15zT\xc0\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x04\x00\x00\x00\x00\x00\x00\x00\x02\xe8\x04\x00\x008\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00,\x01\x01\x00\x01\x00\x00\x00$\x01\x01\x00\x00\x80\x01\x00\x07\x80\x0e\x00\x80\x80\x02\x00\x01\x80\x04\x00\x10\x80\x03\x00\x01\x80\x0b\x00\x01\x80\x0c\x00\x84\n\x00\x02\x04n[}p2s\xf3\x91H=\xea\xafhV\xb1\xec\x01\xf0\x1b\xdfG[\x1c\xbd\x07\xa6\xb7\xe9\xc6P2i\\\xbd\xdf\xefI\xe1\\\x04\xd8L\xdd\xbb7\xc8,\xd0G\x12x\x82t\x9f\x8c\xee\xcd\xad\x16P\x7f%\xc6|G\xf2\x8f\x14\xa7\xa0w\x1ax\x87\x8b\x80\xaa\xf2\x0b\x82\xb5k\xcc\xcb\xdb5\xc0j\xc0\xb1\xd2\x0e\xb3\x05\xd3\x9d\x0bY\xb4}[~\n,W;]\xe0|\x08\xed\xe6\xb4\x82QoDE\xa7\xd5\x91\x92j@\xa1vb\xdd\xc3\xc8%\x81\xaf\xcd\xc2$V\xd90d\xc4\x06$\xd1\xce\x92\xe0:\x0fQ\xa2\xdb\xd8\x11\xaf\xf5\xeb\xde\xbcih\xc1n\x80\xe4\x8a\t\xa2\xcd{\x7f\xa3\t)\x9b\xbc\xe2v3\xa6>9\x87D"\x1a9\xad\x9b\x16q\xbe\x02\xb0\x1f/\xe6\xd7\x81\xeb\x98j\x91\xdf\xabf\xa9M+1\xdc\xc5\xc5\xd71\xc7\x11\xc5\xdcU\xe9L\x10\x9f\x00\xc2\x97S\x90\'\xa8\xd6dNy})F\x99Z\x82\xa7\x1a\t\x03\xa4\xe5\xb5M\x9b$\x9a\x10fX\x10\xa6\xc6\xdf#\xe1\xc7E2\xdf\xc2\x1d}\xd7\x90820b\xcd`\xc7\x1f\xca\xde\xa0\xd7\xb6\x87\xe4\xad\xc4-\xe9\xce\xd9Rx\xc8\xab\xeaI+;\x07\x07-\xaa\xb4\xa2\xd1\xd7-\xe0\x85\x93\xbe\x1dqw\xff\x17\x97\xecku\xf3H%\x9e\x95,W\xa7\xbaU\xc7*\xcd!\xdb\x83\x8dNv~\x1cq\xc8~S\xd1"\xbf\x03(\xac\xf5\xec\xeb+*\xfd:\x9d.h\xcb\x15;\xf1_E\x02(:\xab\xa0}d\xb2\xce\x1d\xff4\xc7\x15{\x80Iy.\t7\x96\x95\x96\xda\x1f\xcf\xab\x03P=\xd0\t\x05!\x904\xaf\xdb\xfa\xcc6k"\xffB##\x8a\xacWx\xf3J\xe6[\xe0\x80\x0b\xc8\x9a\x9a\x87gS\xac\xd6<\r\x1f\x10%\x14\x90}\x94m\xd78$\x95\xf3>>i\x15\x1f\x9ax\x00\xbc\x14\xcf\xd0\xbe;XLl\xfa\xa1\x8f\x8c\xa6\xc5\x03\xcd\xc38\xf6\xb3V\xf0|5&\xf7\xb3\x99\x8f\x81\x9a\x93G\xf3\xf4S\xddl\x08-\xec\xa2\x87\xcf\x14x\xdc\xef\x0326\x82J\x05\x00\x00$\xb0G9\xbdI[@\xedT\x81\xa0\xe5\\]\xd2\x03}+\x1c\xfd\x1b\x88\xed\xa5\xb0y\xfd\x8d&\xe3\x08\x98\r\x00\x00\x0c\x01\x00\x00\x00\x02\x02\x02\x02\r\x00\x00\x0c\t\x00&\x89\xdf\xd6\xb7\x12\r\x00\x00\x14\xaf\xca\xd7\x13h\xa1\xf1\xc9k\x86\x96\xfcwW\x01\x00\r\x00\x00\x18@H\xb7\xd5n\xbc\xe8\x85%\xe7\xde\x7f\x00\xd6\xc2\xd3\x80\x00\x00\x00\r\x00\x00\x14J\x13\x1c\x81\x07\x03XE\\W(\xf2\x0e\x95E/\x00\x00\x00\x14\x90\xcb\x80\x91>\xbbin\x08c\x81\xb5\xecB{\x1f') + +assert pkt.prop.proto == 1 +assert pkt.prop.trans.transforms == [ + ('Encryption', 'AES-CBC'), + ('KeyLength', 128), + ('Hash', 'MD5'), + ('GroupDesc', '4096MODPgr'), + ('Authentication', 'PSK'), + ('LifeType', 'Seconds'), + ('LifeDuration', 132) +] +assert ISAKMP_payload_KE in pkt +assert pkt[ISAKMP_payload_KE].length == 516 +assert len(pkt[ISAKMP_payload_KE].load) == 512 +assert ISAKMP_payload_ID in pkt +assert pkt[ISAKMP_payload_ID].IdentData == "2.2.2.2" +assert pkt.getlayer(ISAKMP_payload_VendorID, 5) -= 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 - Over NAT-Transversal - dissection +pkt = UDP(b'\x11\x94\x11\x94\x01H4\xea\x00\x00\x00\x00/\xa8\xd0\xc9\x15zT\xc0\x95Y\x06\xaf\x97\x1fd\x8d\x08\x10 \x01\xa8!\x97U\x00\x00\x01<\xc8\xba\x8434r\xf8\xc5J\x84W:v4\x1e\x05\x10\xcc.\xd8\xb6\tC\x01~\xad\xd7l\x9c^\x06\tc\xadL\xc4\xc6\xd0P\x98\xb1~\x05\x07\xa0\x0b2&\x05\xa7\xa3\x8c*: \xbe\xa4F\x9d\xa5\xa9\xf7T\x88.\xa9\xe1K\xa29N3%\x19\x80\xd8!\x12^)\x1cJt\xfb\xe1\xca\xab\xb5\xf2\x01\xe83T\x0f\xd4\xfd\xb6\xc4\xe4z\x03`\xd0t\xbc3\xa9\x9b\x8d\xac\x89\x7f\xad\xc2|\x82\x8a\xe4`d\xe6I\xfcVS\x17c7\xce\x13\xd0\x1b\x05\x00\x00\x84\x80\x9cNz\x14\x93\xe7\xb1\x03\x97y\x16\x1f/\x08\x98uE}\xc0\xc3\xe3\x18c\x80w\x13\xad\x96\xe2N*+d%\x9d7\xff\xf1\xd4\xb21\xca\x19E\x98\x96Xil\xf0\x7fN\x80\xf8qc\x10\x96M}\xa5_\x06\xf4"A1\xd5%{\xab\x1ePc\xfa\xa0n\x1c\xd3R\xaeT\x87d\x86\xdf,?\x9e\x88\xb5l\xfaI\xc2v\xcb\xf6\xae1\\i\x07\xf5\xac]@9\xd3\xd7\x8a\xc0\xda\xde\xb2\x97\x8b\x7f\xe8\xfa\xa5V\x80\x0c\xf0o\x0b\x05\x00\x00\x10\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\x04\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') +assert ISAKMP_payload_SA in pkt +assert pkt[ISAKMP_payload_SA].prop.proto == 3 +assert pkt[ISAKMP_payload_SA].prop.trans.transforms == [ + ('AuthenticationAlgorithm', 'HMAC-SHA'), + ('GroupDesc', '1024MODPgr'), + ('EncapsulationMode', 'Tunnel'), + ('LifeType', 'seconds'), + ('LifeDuration', 33) +] +assert ISAKMP_payload_ID in pkt + += ISAKMP_payload_Transform +p=IP(src='192.168.8.14',dst='10.0.0.1')/UDP()/ISAKMP()/ISAKMP_payload_SA(doi=0, 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)]))) -= ISAKMP manipulation -~ ISAKMP r = p[ISAKMP_payload_Transform:2] r r.res2 == 12345 -= ISAKMP assembly -~ ISAKMP += ISAKMP_payload_Transform build 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" +assert 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\x1f\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\x00\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 += ISAKMP_payload_Transform dissection q=IP(raw(p)) q.show() r = q[ISAKMP_payload_Transform:2] r r.res2 == 12345 += ISAKMP_payload_Notify + +pkt = ISAKMP()/ISAKMP_payload_Notify( + notify_msg_type="INVALID-FLAGS", + notify_data="Erreur", +)/ISAKMP_payload_Notify() + +assert bytes(pkt) == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00:\x0b\x00\x00\x12\x00\x00\x00\x00\x01\x00\x00\x08Erreur\x00\x00\x00\x0c\x00\x00\x00\x00\x01\x00\x00\x00' + +pkt = ISAKMP(bytes(pkt)) +assert pkt[ISAKMP_payload_Notify].notify_data == b"Erreur" +assert not pkt[ISAKMP_payload_Notify:2].next_payload + += ISAKMP_payload_delete +pkt = ISAKMP()/ISAKMP_payload_Delete() +pkt.SPIs = [b"A" * 16, b"B" * 16] +assert raw(pkt) == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00H\x00\x00\x00,\x00\x00\x00\x00\x01\x10\x00\x02AAAAAAAAAAAAAAAABBBBBBBBBBBBBBBB' +pkt = ISAKMP(raw(pkt)) +assert pkt.SPIcount == 2 +assert pkt.SPIsize == 16 +assert pkt.length == 72 +assert pkt[ISAKMP_payload_Delete].length == 44 diff --git a/test/scapy/layers/kerberos.uts b/test/scapy/layers/kerberos.uts index 234c4447ffd..0080964f995 100644 --- a/test/scapy/layers/kerberos.uts +++ b/test/scapy/layers/kerberos.uts @@ -4,7 +4,7 @@ # https://www.cloudshark.org/captures/fa35bc16bbb0?filter=kerberos -= AS-REQ += Parse AS-REQ pkt = IP(b'E\x00\x00\xd9\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x00\x00\x00\x00;o\x00X\x00\xc5\x00\x00j\x81\xba0\x81\xb7\xa1\x03\x02\x01\x05\xa2\x03\x02\x01\n\xa3\x0e0\x0c0\n\xa1\x04\x02\x02\x00\x95\xa2\x02\x04\x00\xa4\x81\x9a0\x81\x97\xa0\x07\x03\x05\x00\x00\x01\x00\x10\xa1\x150\x13\xa0\x03\x02\x01\x01\xa1\x0c0\n\x1b\x08LOCALDC$\xa2\x13\x1b\x11SAMBA.EXAMPLE.COM\xa3&0$\xa0\x03\x02\x01\x02\xa1\x1d0\x1b\x1b\x06krbtgt\x1b\x11SAMBA.EXAMPLE.COM\xa5\x11\x18\x0f20150130151703Z\xa7\x06\x02\x04\x14\xe1\x18\xa7\xa8\x1d0\x1b\x02\x01\x12\x02\x01\x11\x02\x01\x10\x02\x01\x17\x02\x01\x19\x02\x01\x1a\x02\x01\x01\x02\x01\x03\x02\x01\x02') @@ -15,7 +15,7 @@ assert pkt.root.reqBody.sname.nameString[0] == b"krbtgt" assert pkt.root.reqBody.nonce == 0x14e118a7 assert pkt.root.reqBody.etype == [0x12, 0x11, 0x10, 0x17, 0x19, 0x1a, 0x1, 0x3, 0x2] -= KRB-ERROR += Parse KRB-ERROR pkt = IP(b'E\x00\x02c\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x7f\x00\x00\x15\x00X;o\x02O\x00\x00~\x82\x02C0\x82\x02?\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x1e\xa2\x11\x18\x0f19810206083031Z\xa4\x11\x18\x0f20150129151703Z\xa5\x05\x02\x03\t\xae\xc0\xa6\x03\x02\x01\x19\xa7\x13\x1b\x11SAMBA.EXAMPLE.COM\xa8\x150\x13\xa0\x03\x02\x01\x01\xa1\x0c0\n\x1b\x08LOCALDC$\xa9\x13\x1b\x11SAMBA.EXAMPLE.COM\xaa&0$\xa0\x03\x02\x01\x02\xa1\x1d0\x1b\x1b\x06krbtgt\x1b\x11SAMBA.EXAMPLE.COM\xab\x10\x1b\x0eNEEDED_PREAUTH\xac\x82\x01\x84\x04\x82\x01\x800\x82\x01|0\n\xa1\x04\x02\x02\x00\x88\xa2\x02\x04\x000\x82\x01R\xa1\x03\x02\x01\x13\xa2\x82\x01I\x04\x82\x01E0\x82\x01A07\xa0\x03\x02\x01\x12\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com07\xa0\x03\x02\x01\x11\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com07\xa0\x03\x02\x01\x03\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com07\xa0\x03\x02\x01\x01\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com07\xa0\x03\x02\x01\x01\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com0"\xa0\x03\x02\x01\x17\xa1\x1b\x1b\x19SAMBA.EXAMPLE.COMLOCALDC$0\t\xa1\x03\x02\x01\x02\xa2\x02\x04\x000\r\xa1\x04\x02\x02\x00\x85\xa2\x05\x04\x03MIT') @@ -32,7 +32,7 @@ assert pkt.root.eData.seq[3].padataValue == b"MIT" etype_info2 = pkt.root.eData.seq[1] assert etype_info2.padataValue.seq[0].salt == b'SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com' -= AS-REP += Parse AS-REP pkt = IP(b'E\x00\x05\x95\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x7f\x00\x00\x15\x00X;p\x05\x81\x00\x00k\x82\x05u0\x82\x05q\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0b\xa2H0F0D\xa1\x03\x02\x01\x13\xa2=\x04;0907\xa0\x03\x02\x01\x12\xa10\x1b.SAMBA.EXAMPLE.COMhostlocaldc.samba.example.com\xa3\x13\x1b\x11SAMBA.EXAMPLE.COM\xa4\x150\x13\xa0\x03\x02\x01\x00\xa1\x0c0\n\x1b\x08LOCALDC$\xa5\x82\x03\xafa\x82\x03\xab0\x82\x03\xa7\xa0\x03\x02\x01\x05\xa1\x13\x1b\x11SAMBA.EXAMPLE.COM\xa2&0$\xa0\x03\x02\x01\x02\xa1\x1d0\x1b\x1b\x06krbtgt\x1b\x11SAMBA.EXAMPLE.COM\xa3\x82\x03a0\x82\x03]\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x01\xa2\x82\x03O\x04\x82\x03K\t\x05\xd7\x91\xdc\x14\xaa\xe2\xfb\xcc\x85\x1f*?\xbau\xbc0\x0f\x80\x8bc\x87\xe5z\x1a4i\xa3\x9bL[-\xb1\xb7\xaa\xd9-\x01\xc2\xf2\xdfs\x17<\xf3&\x99\'1\xfa\x80\xd9\x02\xae\xf5\xb3S\x14\xc2L\xc3e\xc9\x94\x03dH\xe2\xa9\xfd\x9a\xc6\xffs\x10\xf3er\xbd\xa0\xfep[~\x82+\xde0\x91%tc\xdcx\xfe\xd0\xd8\xc4\xb6u\x91\xe7\xe1C\x00y\xb8\x15\xd9\x91j\x0f\xe7\xa0\xe24m\xd94\xe5.I\xc51\x8f\x1do\t\xe9\x98\xb8\xad\xa6\x92\xf3\x15f\xc98o\x92\x0ch\x08\\\x8f\xab\xfau\xaf\x19v\xcc\xcb!v\xb5v2\xeb(h\x1c+o\xea\xc3\x0b\xcf\x81\xc8\x89\xe8i\xdd?\xd1\xaa\x0f3\xc9\xe9\xf2\xd7\x8a\x93`\x02\x9d\xb2 LV\xda\x0f&>,~\xb3\xecK\xe76v\x9a\xc3\x88\xe3\rj\\/\xd6\x9e_X\x14z\xc2w\x1d.|\xbf\x18\x01\xc8`].\xd2\xc2\x1e\xd0\x89\x8f\xd2\x18\xb9U\xaf\x98\xe9V\xe2\x19\xa1\xbb\xc45\xd9\x16\x08c\xaf$\xef\xf2\xf4S\xeco\xa1\xa1\xe5)\x99\xc9b#[\xd1:O\xbej\xb91\xb3i\xbepb\x06\xd8\x14\xc3\xdf\xbb\x18\xbf]\xf1\x82+\x18*\x85D\xecy\x0eu_\xe2\xfa\xbcd\x82A>\x88p\xa2\xc1\xf6\x9c\x89Qj\xfdM\x99\xd1\x84r\x0fp\x06$\xab\xc2\xb5\xae4\xe8\xf1\xbb}\x98\xedWX\xe2*uB\x93\x11\x1c\xc7f\x1c\xce\xc9\xff\t\x88\x94\xddN\xcf\xa68O\x0c^I\x9ew\x81\xba\xc3\xbc\xa8\x07\x8b\xd4\xdf\x7f(\xc2\x15gX\xd0oN\x00u\x1aU@\xbd\xb8\xa9)Ur\x94\xc1\xcf\xa1\xd8k\xc1F\x19\xd3rR\xaa\x93\xe2\x06D#\x12\x07M\xe3\x15\xd6\xd0\xb3\xa6\x89\x0c\xfeLO6\xe6\xf0w\x1a\x80\x0f\xffO\xf2N\xf4(\n\xdb-\x96`\xa4\xb7\xd3g\x16\xbfY\xff\xad\x95\x19\xd9\x9cS\xaa\xe3\x06W\xf3\xc2\x18it5\xda\x1c\x99\x8a\xaf\xfa"MT\xc7$#j,P\x9b\xf9\r\xbbA\xd0w\x15.\xc3PC\xc4\xe7vL/\xca0h7\x1c4z\x8bS@\x0ej\xb4q\xde\x19\xd8so\x9c\xea\x8f^w7\x1e\x92\x1c\xcc\xe2\xa60\xe8\xce}\xee\xb1\x87F!n\x80\xe4l"\xed\xc2fI \xb9\t\x14\t\x8d\xect\xa4\xb48\xe0\xfd\xf3\xe5\x8es\xd2\x08;\x9f\xb2\xb8q\x1bX\xadd\xbb\x07z\x16\tZ\xb0z1+h\x0e\xf7\x98w\x0bX\xf0W\t\xa6\x86.\x1e\x9c\xc2\x9d\xac+\xca\xdf&\xa9\xf3\xcb\xa7\xca\x1fn\xe8\x8a]h\xf6\xeb\xe9\xd4\xa0\x16\x1b\xb4\x8d\xc7\xaf\xe3\xf0.\x85\x1e\xc2\xa5\xf2DhhgQ\xe0\xb8y\xb8\xbd\x98\xf8\xa0\rW\x93/\x07>0\xf5\x92Y\x15Y\x0bD\xdb\xd6\xac#\xd8z\xbdeY\x87\xf2\x97\xfdZ\x0c\x1d\xbc\xefXONv\xc9\xfdp\xdd^\x16\x83\xc3\xeb\x9e\x96+\xe8\xed\x0c<$\x83A\xeb\xc6e\x94\x0c\x11\x19\xb4\x99\xcd\x17\xeb\xcb.\x0b}\x01i\x88\x03R\xde\x1a\xea\x03\x10\xa9Z\x8e\xf7\x87\r\xa6\x08@\xf7\x96\xc8\xa5g\xde\x8dE\xf8\xb0\xe8\xe6T\x80=\x0cm\xe0z\xa5\x03\xa2X\xed\'\x17\x001O\xee\xfb\x87\xbe\xf7\xbbS\xc1p\xaeZ\x17\x92}\xc2\x07\x01\x81\xaew\xd9\xc5\x9c\xe5k\x8d+\x13\xd2\x00Q\xd4\xe5M\x9d\x06\xc7)\xac\x06\xb2+\xd1\x83\xcb\xfe\xb9\xf9\x0bbRN\x04\xe7\xd8\xa0\xf9\xe3\xc3m\x18\xc4\x108\xfa\xa6\x82\x01:0\x82\x016\xa0\x03\x02\x01\x12\xa2\x82\x01-\x04\x82\x01)/pDi\x13\xee\x0b\x8ehN2\x01P\x19|\xda\x1a\xde\xec\xde\rt\xcbe7\x00-sG&\x8b\xfc\xa4\x92~~[,\xd5\rAj\xd6[\xbe\xeeB\xf8X\\x\xa6$Z\x83\xf6\x1bq\xc5\x8fm\\\x94\xd7l\xc5\x89#\xcb\xcd\xaf\xff\x15\x1b\x8f;7\xb0\xc8u\x19\xb1\xd0\xb0\x93\xa7z\x9cz\x14\x0b\x86q\x01\xb8<\xa7\xa4\xceb\x1f\x88\x14\xe3S0\xe3]\xa5\x9b\xa0\x0e\x97#\x87\x9a\xe0\x90a\xdfj.\x1e6x\x87GV\xc0/\xa4\xab}\xdbS\xd5\xff\xc1\x9f\xeb\xae\xcb\x04\x071\xf1x\xff\xe5M\xfc\xbct\xea^e!\xce!|\x893/\xa1\n.\xb7T\xc5Ph\t\xf1\xbak\xcd\xdb\xff+c\xab\xcfY\x8a;*/\xd8\xa5\xd0\xd7c\xc6\x02B\xed\x82\xcf\xa0\xe5\xdf@rq\x8cRG\x1a\xdey_#\x18\t\x9d\xac\xa4\xfe\xd0\xeb{\xcb(E\xb8\xac\xc9\xe3\x06\xe0\x15}\xb89\xb1L>\x060\x93\x1dtl\x1f\xa0\\s\xdb\x85\x82\xdf\xb3L\x80\xe7/\xae\x0e\x11V\xdeH:J K\xb1g\x95\n\xc2\xd2\xc2\x83k\\6\x0eg\xd0{v\'\xa4\x1c\xe2\x10-\xeb\'\xc7?F\xd8J\xe8\x90Z4V\x12\\\x9e\xc2\x05\xfc|\xb3\x01\xe5\x1b\x14\n\xaa\xff\xb9\xff\x07\x03L\x10\x1d\xc8\xa8\xed\x00A\xf3\xf2\x16\xa3\xd8":!\x04m\x10Uo\x11\xa5d5\xc1\x1es\xde=\xa6\xdd\x9b\'\x03(L(*\x92C\xca\xc8\x92\x1b\x08\x06z/\xb4=\xd8Mz\x816\x9f-\xc0\xe8\xcf\xd2A\xfeyk)WH\x11\xdf\'\xf4\xefG\xfc\xef\xd0\xb5\xec\x91\x87\xf4}b\xb2\x1e>\x1f\x9d4~h\xa0=\xfd(i0|\x03\x98k\x05#Y\xe35\x1c\x7fn\xac\xf2\x896\xa6p\x13\xc1\x94&Q\x8f\x1c\x07\x8cN\xb0\xb6=\x83R46\x04\xfa\x86\xbc\xc1UO\x03\xd8\x0e\x0c\x9f\xbd/\x02f\x90\xa8\x9e\xd3 \xb4\\\n!\xf9"\xc3\n\xe7\xe2\x92\x05t\x11\xa1\x9e<$i+U\\d1\t^\'\xb7\x12\xfd\xe5\xd7\xc4\xd4\xb2\xa9!`\xd8\x97\x8b\x9a\x0c:\xcc\x85\x90)_\x11\xefR\x00\xe5k\x12I\xe2\xf6\xf4h\xa4.\x97\xf2\xea?\x1e\xf9\xcf\xe6\xac\xc7\xdd\xd0\x8f\x0bml\xcb[\x801\xce\xae\xd28\xc0\xe9\xb1\xb0\x19\xc9r\xd2\xd4=\xdaw\xff\xc7\xbd\xe7\xf8\xa9\x8d\xc6\xda\xa9y\x9b\x98\x19\x05\xb1]\xbc\xe2\xe3\xaf\x8c8\xcd\x12\xf8\x90\xea\xd0\xe3\xc3\xba|\xe28(\x8f\x99\xba\xden\xefJ\xc4r\x9e\x17\xe8&\xd6\xe4\x83 \x92\x19d?\xa6\xcc\xbd\xff\xa5\x83@\x17\x13\xefY\xd7\xa7\x1e\xe4\r\xd2\x846\xf8~!L\xe5\xdd\xb3\xb4(\x14\x1e\x1a\xfcP\x8ezE\x1ffFJ.\x82\x1f\xd3\xc5l\x9e\x0b3u4b\x0c\x94\xd6R\xc0\xe5\x96\x83\x95\xa1\x12\xa2\x18;\x96\x9di\xca\xc8\xd9\x15\x81\n\xa9\xc3\xe8\x1eS \x93j\xeb\xa4\x81\xc60\x81\xc3\xa0\x03\x02\x01\x12\xa2\x81\xbb\x04\x81\xb8-Y=\xd3\xfc\xeb \xd8\x16\xd9\xb2O\xfc1\xc9\xd5\'zN\xd2\xb6\xf4\xc6Q7\xaa"B\xe7\xac3\x19\x86\xad\xd5@\xa6\x1f\xd8a#EN\n\xba\xc3\xd95\xe5\x93\x07,j\x97V [o\xe3\x91!d\xe6|\xa4\x94\x14\x9dj1J\x82as[\x83\x80\x99\xa3\xec\xc1\xda_\xe7\nLej\\\x9eW\x11\'7\xfeq=)\xef-\xf5K\x15\x8e\xbf\xb8]m\xb6\xc2\xce\xb4xN,\xdb\xbeaB\x86\'\x068\x05\\\xafF\x08DFpJtX\x0c\xc1\xdfw\x9b\xb1\xf8x\x93\xac\xf9\x14X;h\xe3E\xc0\xe4i\x19\xe5:\xe7\xe5\x86\xa7{\x96\t|\x9aG\xc0\x169\x08\x03A\xa6\xc4j\'-\x07\xf4\x9c\x88"\xc00\x81\xe0\xa1\x04\x02\x02\x00\x88\xa2\x81\xd7\x04\x81\xd4\xa0\x81\xd10\x81\xce\xa1\x170\x15\xa0\x03\x02\x01\x10\xa1\x0e\x04\x0cW\xb7\xdc~\x96.\'\x92\x1a\xdfh\xb9\xa2\x81\xb20\x81\xaf\xa0\x03\x02\x01\x12\xa2\x81\xa7\x04\x81\xa4\x9b\xfc\xb3\x8c\xc5\x1e\xa1q\x19"\xf0\\\xa7\xa6`\xc9:\xd6KA\xd5\xac\xa9$\x8a\x18z\x81\xce\xc9\x0f\xe0\xd5\xad\x848t\xb7\xe3\xf1\xffC\'\x16Z\xc6\xe1of5\xf2R\xb31\xbf\xfa\xaf$\xe5\x1d\xa8\xd3sf\xbb$\xc5%\x17\x0c\x98\x98\x08\x85\xd18\x91o\x8d\x83\x86P\x9e\t\xd9V\xd1\xe4\xeb\xa8\x11\xd6\xaa\xb7\x88\xde\xbe2\xbf7\xb8\xca\x1c\x90\x10GB\x06\x046\xc8\xff\n\x02$_\xce\xcfk\xc9xd\xe5\xbf!4q\x83*/B[\x8fJ\xfa\xf4\xad97\xd8\x8f,3b\xb7\xe0\x94\xca\n\x12]\xc9\xfc\x7f\xbb{2p\xa0\x8f1e6$\xa4v0t\xa0\x07\x03\x05\x00@\x81\x00\x00\xa2\x13\x1b\x11SAMBA.EXAMPLE.COM\xa3,0*\xa0\x03\x02\x01\x01\xa1#0!\x1b\x04ldap\x1b\x19localdc.samba.example.com\xa5\x11\x18\x0f20150130011709Z\xa7\x06\x02\x04T\xcaN\xf5\xa8\x0b0\t\x02\x01\x12\x02\x01\x11\x02\x01\x17') @@ -63,7 +63,7 @@ assert pkt.root.reqBody.kdcOptions.val == '01000000100000010000000000000000' assert pkt.root.reqBody.sname.nameString == [b'ldap', b'localdc.samba.example.com'] assert pkt.root.reqBody.till.val == '20150130011709Z' -= TGS-REP += Parse TGS-REP pkt = IP(b'E\x00\x06V\xff\xff@\x00\xff\x11\x00\x00\x7f\x00\x00\x15\x7f\x00\x00\x1d\x00X;\x97\x06B\x00\x00m\x82\x0660\x82\x062\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\r\xa2\x81\xe90\x81\xe60\x81\xe3\xa1\x04\x02\x02\x00\x88\xa2\x81\xda\x04\x81\xd7\xa0\x81\xd40\x81\xd1\xa0\x81\xce0\x81\xcb\xa0\x03\x02\x01\x12\xa2\x81\xc3\x04\x81\xc0\x8cqa\xdf\xfe\x13<7\xc1:\x8d\x0bshxOC\xd6\xcb\xbdz\x1a\xf5\xaa\x9c8\xce\x9f\xed\x99\xeb\xd8A\xba\xdcj\xffF4|\xc7\xab\x84~\xb9\x8f\x04\x0e<\xf1p#\xf7kK\x86\x05+%\\:\xcb^\xc8e\xeb\x0f\x81\x92\xa0\xf3"\xcd\xbb\xf3\xb9\x91\xc8\x94\xa27\x8c\xae\xc44\xa8\xd27\xd1J`K\x93M\xe3\xefUy\xda\xc6\xb7\xe6\xc8\xed\xa79\xd4\xd5\x9a\x12f\t\x1c\xb5\xa7A\x95\xaf\xa1\xac\x1d\xde\xfb\x1c\x0ec<5\t\xabYU\xd4\xd4\r\xf4]\xec\x00t^K\xed\xca\x81\xad\xbe\x99\xdc\x10g\x9c$\xfb\x82s?\xf4\xb9\xa5\x8eW\x02\x7f\x87A\xf7\xc4;2q \xd2\xbc\x10\x13\xc9\xa0w[\r\x01Pt\x7f\x95^\\\x8e\xbe\xee+\xa3\x13\x1b\x11SAMBA.EXAMPLE.COM\xa4\x1a0\x18\xa0\x03\x02\x01\x01\xa1\x110\x0f\x1b\rAdministrator\xa5\x82\x03\xe5a\x82\x03\xe10\x82\x03\xdd\xa0\x03\x02\x01\x05\xa1\x13\x1b\x11SAMBA.EXAMPLE.COM\xa2,0*\xa0\x03\x02\x01\x01\xa1#0!\x1b\x04ldap\x1b\x19localdc.samba.example.com\xa3\x82\x03\x910\x82\x03\x8d\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x01\xa2\x82\x03\x7f\x04\x82\x03{\x97\x9c\xac\xf1\n\xe6;\xd8\xe28m\xba\xb7\xea#\x19\xd3Zf\x1c@\x00H\xf9"\xe7\xb4\xf3&3\x02X\xb5\xc0{e\xffm\xc8\xcf\xe2\xf9p\xb57~\xd8\x91?/5\x7f\xde\xc4/\xaa\x1c\x08pQ(\xff@\x8e\xb7\xf0\x91N\xbcK&0\xbdWo_W\xf8\xbe\xd6(\xd1`\xba\x8f.\x86\xc29\x88\xe5:,\x16ui\x98y\x100Q\xf6k1\xe6\xe5-e\xdc\x80\xc0@\x87i9Z\x7f\x07\xeb\xf2\x8f\xb1\xc4\x83*z\xbbq\xbfZs\xd7\xefFAZ\x84w\xa2-\xc8\xca\xa3\x84\xa2\x0bm\xce7 pIX\xa1\x05\x83\x01t\x06\xabI\xa3dp\xe3\xaa\xd0\xd6\xb0!\xfd\xbea\x9buL\x0f\x99\xbfg\x11|J?\xfdl\xcd\xb6\xae\n\xdc\x06kS\xc60\xad\xf3\xacq\x0f\xd5lbX\x8d^\xf9\x83\x80ax\x1c\x12\xaa\xe3\x07Y\x1ef\xae\xd6\xc9\xd4y\x94\xb5\x93\x83\x03m\x03U\xf3\x9a}L3Xi \xf94\xffFf}\x99\xfd\x04I\xe3\xcd\x9f\r\xb7>r\x0e\xcf\xeb$\xc8\xdcO\x95\x88\x04\x1c\xf0\xf9\t2\x92\xc4\xe3\x10\xfa\xb0\x14\xb5\xfb\xf0.\xcc\xa3\xdc\xab\x0f\xd76\x8e\xbf\xd8\x7f@U-x\xc8 \xd42\xf8\xfd\xce8\xdbl\x16\xc1\xaa\xb3\xe32\x87\xd3\xecIc-\xcf\xab7\x0b\xd9b\x9f9\x06\x88|q\xca[\xb8\n\xfb\xf7\x0bl]:\xbc\xe1\xab:K;w6\xcf\x1c\xa6\x1a\xec\xc0\xe2\xea\x89\xe6u\xe4(\xec\xec\xda!\x06\xfd\x9c\xeeZb4\xeb\xff\x06j\xbc\xfe\x90\xb6\x93\x0b:t\xf1|\xa3`\xfb\xc5\x9a\xa5\x11w\xb2}oP\xccj\x10M\xf3\x98\xbdCj\xa9\xcd\x93\x83\xf9N"\xbc!z8\xf6\xca\xe3\xbc\x04\x92\x14\x16i\xa40\xbf~\xb5\x12\xbeC\x83\x9e\xbdH\x13\xcasxFM\\\xd7\xc9\xd3B\xacM\xe7\x1c\x8ej\x12\x197\x06\xae\xbd\x1c\x84J}\xab\x8b\x05F\x8a\x13\xbe@]\r\xc2-\x9fA\x19\x94Jl\x12\xba\n\xad\x16T\x94\xb85U\xc1o\t\x04\xb2F\xa1\x17M4\xc3\xb2N\x17\x8f\xfe\x190\xc2\x11q\xc3A\xd9\xafn\xc8\xc909\xc4\x05\x03\xf3\xb2\x8e\x97\xfcL>E`\x11`\xce\xe5n\x15\x84\x84~\xdfZ\x98S\x0f[\xc3\xaa\x8e\xcf\x9cU\x93\x94\x04>\x05\x90\x1c\x00\x1a7\xb7\xe9\xc9\xc9\xb6Eq\x13\x1e\xb5\x86\xc3}&\xe7\x1b\xe5(\xce\xe3b\xd5\t\x11\x1f\x1e\xe3;O\xd9J\x85\xc5\xfa\x82\xd2\xc9\x88\xc5\xa8\t\xf5\xdb\x85vi\x1d\x97\x12j\xe8\xabL\xf0J\xd3\xbe\x1c\x7f\x1a\xb7$k\x87\x9e\xc3\x9aH\x1e\x96>\x19\x0fE\xff\xe2\xc8\xc2|W4\x12\xe4\xc7G[\xdc\x93\x17E%ur\xcem\x169\xf2I\xab\xbb\x8d\xca\x0fM0n\x19\x06\xeb<\x03\xa7fw^\xdd(V:\xc0\x14+\x08L\x17\xbe\xc9\xa6\x82\x01\x1e0\x82\x01\x1a\xa0\x03\x02\x01\x12\xa2\x82\x01\x11\x04\x82\x01\r\xeeN\xd0\x1b\xa0\xc4\xb0C\x12,\xdd\xbd\x96\xe8\xbai"j\xbc[O\xff}Z\n5%\x98\xfc{`Q\x92\xe4\x95\x1azM\x15b\x98Ah\x02\xb2V\xd5\x0f9\xb3\xd5\xcf!\xdf\x1e\x9c\xd4\xc08\xc0|\x10\xc8\xb0ol\xcd\xa6?\x19\xfa\xb9\x0b\x9d\x96\xaa_,O\xe2 @4;\x1f!\x12\x8e\xf3h\xbc\x95\xa2\xcfE\xaey\\U\xdcc\xbe\xecN\x9e\xaa\x9d\x83\x1a\x9ad\x11\x15X\xdf)L\xd8Z\xe3\xa2&\x1c\x1b\xf8\xd1\x8e\xfb~\xdd\x16^\xfa\xf9\x15\x96s\x03\xf8T\x86\x12B\xdf\xf7m@\xfa\xf5L\xdd\xb6\xa8\x9af\x90\x90\xcd\xa9\xdf\x97`\xd3\x1c)\xc5n\xe8\xc1\xe0\xb4\xc7"\x16\x91<}\n\x94\xec\x8d\xc6.d\xe1\xf5/i\x89$\x9a\xebW\x0c\xf7\xfe\xc5\x12\x10\xb8\xa5\x193\x88hR\xa0\xf7t\xa9\xc6\xc2\x15E\xbd\xd6\xf09\x1d\x12\x83o\xb35>o\xa0\x98\xda\xf2\xad-1\xd0\x94\x12Be\xe0\x04\xe0\xf7\xcf\xbbAZ\xf5\x1c\x88\xf5\xef\xb2\x9bi\xdc\xd0\x07\x8f\xca\r^\x92\x02\x15\x87\xef\xd5\x90\xb5') @@ -74,19 +74,185 @@ assert pkt.root.ticket.sname.nameString == [b'ldap', b'localdc.samba.example.com assert len(pkt.root.ticket.encPart.cipher.val) == 891 assert pkt.root.encPart.etype == 0x12 ++ Kerberos dissection and decryption tests + +# For the following tests, we use an account with no preauth and request a DES-CBC-MD5 sessionkey on Windows. +# (unconventional but allows us to test edge cases) + += Create Key (RC4_HMAC) + +from scapy.libs.rfc3961 import EncryptionType, Key +key = Key.string_to_key(EncryptionType.RC4_HMAC, "Password1!", None) +assert key.key == b'\x7f\xac\xdcI\x8e\xd1h\x0cO\xd1D\x83\x19\xa8\xc0O' + += Parse AS-REQ (no preauth) + +pkt = KerberosTCPHeader(b'\x00\x00\x00\xd4j\x81\xd10\x81\xce\xa1\x03\x02\x01\x05\xa2\x03\x02\x01\n\xa3\x150\x130\x11\xa1\x04\x02\x02\x00\x80\xa2\t\x04\x070\x05\xa0\x03\x01\x01\xff\xa4\x81\xaa0\x81\xa7\xa0\x07\x03\x05\x00@\x81\x00\x00\xa1\x120\x10\xa0\x03\x02\x01\x01\xa1\t0\x07\x1b\x05User1\xa2\x0e\x1b\x0cDOMAIN.LOCAL\xa3!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xa5\x11\x18\x0f20231213110146Z\xa6\x11\x18\x0f20231213110146Z\xa7\x06\x02\x048\xa6\xb8x\xa8\x080\x06\x02\x01\x03\x02\x01\x17\xa9\x1d0\x1b0\x19\xa0\x03\x02\x01\x14\xa1\x12\x04\x10WIN10 ') + +assert pkt.len == 212 +assert pkt.root.padata[0].padataValue.includePac +assert pkt.root.reqBody.etype == [0x3, 0x17] + += Parse and decrypt AS-REP (no preauth, RC4) + +pkt = KerberosTCPHeader(b'\x00\x00\x06\x1dk\x82\x06\x190\x82\x06\x15\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0b\xa3\x0e\x1b\x0cDOMAIN.LOCAL\xa4\x120\x10\xa0\x03\x02\x01\x01\xa1\t0\x07\x1b\x05User1\xa5\x82\x04\xa0a\x82\x04\x9c0\x82\x04\x98\xa0\x03\x02\x01\x05\xa1\x0e\x1b\x0cDOMAIN.LOCAL\xa2!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xa3\x82\x04\\0\x82\x04X\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x03\xa2\x82\x04J\x04\x82\x04Fm[\x1a\xa0G\xd5 \xee\x9c\x0c\t\xfb\xc3\xee\xd8Ki\xca\xaa6~\x87\x0fu\xde\xfd\x8d9\trl\x9d\xe9\xf0\x10\x0b\x85SO\xc2\xae0\xb1\xc1\x9a\x8c\xa0\xcb/\xad\x94\xaa\xe0\xb1R\'C\xd0uqw\'\xa6zF\x9d7\xf7\x08\xd8[(\xd5\x11\xc6:\xf5\r:\xde\xf9\xdd\xd9/T\xaa\xe1Q/\x9eD\x91\x01\xa8X\xf0O\xde\x88\xcb\xc4\xc7\x87\xb1pv\xd4\xb0r\xc1\x10\x80W9\xf7\xe7+\xd9M:\xf2\x8f\xdf\xa4\xc1\xa5\x95lU\xc02A\rf\x0b\xef\xc8\xc9A\'\x87\xff\x92W\xd4\xed\xb9\xd0|{\\\xbd\xf2\xfb%h\xe3\xb8\xccs\xec_\xe7\xf9\x90\xae\xb8E\xab\xf6!\xe6z@\xf1-nO\xcf X\x1eh\x86L\xba\x0ef_\xde]\xe2_\x94\xb0\x13\xccN\r/\xd3\xf2\x81\x07\x1b\x14\xfd6\x00Y~\xc0?\xaeYb\x7f\x16\x139\xe5P:\x93\xe3N3\x08iB\xc5m\xa3\xb5\x10d\xd1~\x0eb~wk{u\xec\xbe_!w{\xb7Z\\\xcf\xf5\xd9\xc3\xea\xe5\xfd\xfd\x03\x18\x07\xab\xe3\x06\x07\x9a\xa1\x9c\xc2C.\x0e\xb7c\x14\xf6\\\xd2\x82\xf2\xfc\x01>\xed\xfb6&<\x8f\xab\xe0\xfe5\x86!e{\xadr\xa3\xab\x87\xbc;p\xbdh|\x04\xf5\xffJ6\x94\xca\xacLc\xeb\x91\x14\xb94\xe7\xf4k+_V\xefh\xd4G@\x16\xc7?\x92\x94\xa3\x87\x81#\xbc\xa6>\xefh\xdd\x91\xe2\xce\x06\xba+\x96\x83\xb5n\xb2\x0c\xc3\xf9\x1f\x15\xe8\xba\x10\xf7V\x8b\xf4\xc1Rg\x86S\xfa\x89\x90\xe4\xceJ\x8d4\xc1Bh\xb5S\xa8\']8z,j-z\x0c\xc28Z\x06d\xd9\x90\x19\xf4\xc2)\xc7\x86\x9dk\x17{\x12/\t\x8a.\xc4\xe7\xdb~t\x92\xadx\xb2\x91\xb5\x96@\xf6\xa8ftuM\xdf\x17\xc4V\xa0y\xd0\xdf\x1f\x1a\xc9y>\xc0\xd1\x85\xde\xf4\xee#\xc8\x82F\xc8H\xa6h\xe8\x02H\x9bE5U`o\x98\xc0P\x9c\xd9L\xb9D\xff\xd8G\xd0k\xc0\x07\xda\xd2#\xc3"\xb7\xb8\xf2)\x9c\x164\xaa\xe4\x18-i(\xabn\xb7\xeaB5\xe4\xb7\xdc$$\x9e|\xcdA\x03\xf3\xd7n\xd3\xc1\xd7\xe6e\xb6\\\xd3)\xfah\xb7\x88\x0e\xeby \xfe\xd2!.Q\xa0\x97\xa8\xe2O\x1d\x99\x02#9\xf4\x1c\x0e\x1fN\xc9;\xd5?\x0fm=\xee\x0efj\xc1\xcb\x14\xb5\xa9}\xe2:F\xd7\x1d\x07\xfd\xaf\x96D\xfc\x007q\x11\xe1\xf6\x12\xdc%\xf7\x92ML\xbfH$\x10\x8a\xb9\xfbp\x9b\xff\x07\\N\x83\xf5\x11\xaex\xf2\x171F\xe3\xfc\xf6\x89\xc3\xdf]\xaa:\x8f\x99\'\x16` P\xe6X\x04\xe9@\x89\x90\x8cP\xc5b\xf82\t+\x14+\xb7\xa3\xfa\xba\xa4*r\xb41i\x070!\xba\xc8\xb17\x06\x12\xf2\xce\xa0\t9P\xd9]\xe4p1i\xf3\xed\xc0oT\'\x99\xc0\x7f\xa8s\x0bW\xc7S\x90w\xe6\xa7\x91\xe1\x84\xd3V5$\x92\xa3\x81\x90\x02\xdfVu\xd7\xb7x\x13+p\x8djP\xfa\x0eL\xc5}=\x12t\xc3\xa6\xa5\x12\xd9H+w\xea\t\x92km\xf9$\x0c\xa0Y\xda\xea\x15\xd0\xa1\xbe\x85\xa3\xd3\x9fQ\x1a\xd8A\xabf\x9d\x9c \x19\xa5\x8e\t\xb4c\xac\xe3\x99\x00\xf4i\xc4\x14c\xd7h\xd3\xc6x\x11\xa5\xa0`\xe5\x8d"\xae\xa3\xa7\xba\xb8\xc4~\x87\xad\x1d\xa6\x19\xe3v\xdd^(-w7d\xd1\xb0D<\xeaW\x84\x90=\x9e\xee\xa3\xe3u\xa7\x074\xf3:6{\xbd-\x87\xfee\xd6b\x8a\xe5\xa9v\x0c\xe8N\x1c\x10\x12\x91\x1e~\x92\x02Uh)\xdd\xb5f\xf9\xcc\xadf\xf3:\xa7\x9f\xfd\xe1>\xd19\x10U1\xf0\xf8\xb1G\xe8H\xcb!h\xab\x14q\xe51d\xb2A\xf07\xda\x11\x81\xd9\xff') + +assert pkt.root.cname.nameString[0].val == b'User1' + +asrep = pkt.root.encPart.decrypt(key) +sessionkey = asrep.key.toKey() +assert asrep.encryptedPaData[0].padataValue.flags == 0x5001f + += Parse and decrypt TGS-REQ (DES-CBC-MD5) + +pkt = KerberosTCPHeader(b'\x00\x00\x05\xd1l\x82\x05\xcd0\x82\x05\xc9\xa1\x03\x02\x01\x05\xa2\x03\x02\x01\x0c\xa3\x82\x05=0\x82\x0590\x82\x055\xa1\x03\x02\x01\x01\xa2\x82\x05,\x04\x82\x05(n\x82\x05$0\x82\x05 \xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0e\xa2\x03\x03\x01\x00\xa3\x82\x04\xa0a\x82\x04\x9c0\x82\x04\x98\xa0\x03\x02\x01\x05\xa1\x0e\x1b\x0cDOMAIN.LOCAL\xa2!0\x1f\xa0\x03\x02\x01\x02\xa1\x180\x16\x1b\x06krbtgt\x1b\x0cDOMAIN.LOCAL\xa3\x82\x04\\0\x82\x04X\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x03\xa2\x82\x04J\x04\x82\x04Fm[\x1a\xa0G\xd5 \xee\x9c\x0c\t\xfb\xc3\xee\xd8Ki\xca\xaa6~\x87\x0fu\xde\xfd\x8d9\trl\x9d\xe9\xf0\x10\x0b\x85SO\xc2\xae0\xb1\xc1\x9a\x8c\xa0\xcb/\xad\x94\xaa\xe0\xb1R\'C\xd0uqw\'\xa6zF\x9d7\xf7\x08\xd8[(\xd5\x11\xc6:\xf5\r:\xde\xf9\xdd\xd9/T\xaa\xe1Q/\x9eD\x91\x01\xa8X\xf0O\xde\x88\xcb\xc4\xc7\x87\xb1pv\xd4\xb0r\xc1\x10\x80W9\xf7\xe7+\xd9M:\xf2\x8f\xdf\xa4\xc1\xa5\x95lU\xc02A\rf\x0b\xef\xc8\xc9A\'\x87\xff\x92W\xd4\xed\xb9\xd0|{\\\xbd\xf2\xfb%h\xe3\xb8\xccs\xec_\xe7\xf9\x90\xae\xb8E\xab\xf6!\xe6z@\xf1-nO\xcf X\x1eh\x86L\xba\x0ef_\xde]\xe2_\x94\xb0\x13\xccN\r/\xd3\xf2\x81\x07\x1b\x14\xfd6\x00Y~\xc0?\xaeYb\x7f\x16\x139\xe5P:\x93\xe3N3\x08iB\xc5m\xa3\xb5\x10d\xd1~\x0eb~wk{u\xec\xbe_!w{\xb7Z\\\xcf\xf5\xd9\xc3\xea\xe5\xfd\xfd\x03\x18\x07\xab\xe3\x06\x07\x9a\xa1\x9c\xc2C.\x0e\xb7c\x14\xf6\\\xd2\x82\xf2\xfc\x01>\xed\xfb6&<\x8f\xab\xe0\xfe5\x86!e{\xadr\xa3\xab\x87\xbc;p\xbdh|\x04\xf5\xffJ6\x94\xca\xacLc\xeb\x91\x14\xb94\xe7\xf4k+_V\xefh\xd4G@\x16\xc7?\x92\x94\xa3\x87\x81#\xbc\xa6>\xefh\xdd\x91\xe2\xce\x06\xba+\x96\x83\xb5n\xb2\x0c\xc3\xf9\x1f\x15\xe8\xba\x10\xf7V\x8b\xf4\xc1Rg\x86S\xfa\x89\x90\xe4\xceJ\x8d4\xc1Bh\xb5S\xa8\']8z,j-z\x0c\xc28Z\x06d\xd9\x90\x19\xf4\xc2)\xc7\x86\x9dk\x17{\x12/\t\x8a.\xc4\xe7\xdb~t\x92\xadx\xb2\x91\xb5\x96@\xf6\xa8ftuM\xdf\x17\xc4V\xa0y\xd0\xdf\x1f\x1a\xc9y>\xc0\xd1\x85\xde\xf4\xee#\xc8\x82F\xc8H\xa6h\xe8\x02H\x9bE5U`o\x98\xc0P\x9c\xd9L\xb9D\xff\xd8G\xd0k\xc0\x07\xda\xd2#\xc3"\xb7\xb8\xf2)\x9c\x164\xaa\xe4\x18-i(\xabn\xb7\xeaB5\xe4\xb7\xdc$$\x9e|\xcdA\x03\xf3\xd7n\xd3\xc1\xd7\xe6e\xb6\\\xd3)\xfah\xb7\x88\x0e\xeby \xfe\xd2!.Q\xa0\x97\xa8\xe2O\x1d\x99\x02#9\xf4\x1c\x0e\x1fN\xc9;\xd5?\x0fm=\xee\x0efj\xc1\xcb\x14\xb5\xa9}\xe2:F\xd7\x1d\x07\xfd\xaf\x96D\xfc\x007q\x11\xe1\xf6\x12\xdc%\xf7\x92ML\xbfH$\x10\x8a\xb9\xfbp\x9b\xff\x07\\N\x83\xf5\x11\xaex\xf2\x171F\xe3\xfc\xf6\x89\xc3\xdf]\xaa:\x8f\x99\'\x16` P\xe6X\x04\xe9@\x89\x90\x8cP\xc5b\xf82\t+\x14+\xb7\xa3\xfa\xba\xa4*r\xb41i\x070!\xba\xc8\xb17\x06\x12\xf2\xce\xa0\t9P\xd9]\xe4p1i\xf3\xed\xc0oT\'\x99\xc0\x7f\xa8s\x0bW\xc7S\x90w\xe6\xa7\x91\xe1\x84\xd3V5$\x92\xa3\x81\x90\x02\xdfVu\xd7\xb7x\x13+p\x8djP\xfa\x0eL\xc5}=\x12t\xc3\xa6\xa5\x12\xd9H+w\xea\t\x92km\xf9$\x0c\xa0Y\xda\xea\x15\xd0\xa1\xbe\x85\xa3\xd3\x9fQ\x1a\xd8A\xabf\x9d\x9c \x19\xa5\x8e\t\xb4\xcc@\xf6_\xdd\x85\xb9\\\x9f\xf5P\'\x9ae\xf0\x925\x884W\xde\x9fn\xb3q.\x08e\xd4\t\xf2;\xb5\xd0\xcb\xe8\x1b\x9e\x15\x83~ q]\xdaw\xd2X\xac\t=aV\xa7\x9c\xfb\xee\xe2n\xf7\x9a\xf1\'t[\xe2\xcc\xaeL\xb9\xe1\xbc\x87C\xddG-\xdeJ\x9d\x8d\xa4\xb4W\x83\xb8\xf0(\xa4\x92\xf9\xa9OJ\xb2s\x07\xfa*\x0f\xf9\xbf\x17Z\x15\xd5\x867\xe3\xfd\xa6r\xb3\x9f\xca\xb5\x9dth\n\xc4\xe3\xc4P\x08\xfe\xd6Fd=R\xde\xe6\x80CC,\xe9l=\x89,\x82\xed\xc5<\xec \x8b\x19\xe1\x88\xaf\xf2\x8b\xbby\x8f\xf1\x88\x84?\xcc\xa4\xb5\x7f\x84\x99\x9d\x85\xedEs\xfc\xc6f\xfc\xb8\x04=\xa5\xcf\x0f3\xb3\xed\'\x01\xa2(\xb5\xec\x1d9\xcd\x88%\x86\xf4u\x91\x11\xe6O\xfc:I7\x1b\xd4\xc0\x11u\x80\x1dt\xc1\x81\xd5#\x10\xff4\x03Fs;O^\x0c\xfb9v\xcb\rt\xd2\xfb\xa3-\x01\\\xa4\xd2\x07\xcdm\xe4*\x85)A\xf6[\xf7\xbbOarb\x0f\xd8\xbaq2LL%0\x1c\xc5\xfa\x94L-M\xab\x90<\xb1\x0e`\x81%\xc3\x1b\xe9\x80\n\xf2\x89}t\x07\xe6\x9e\x02\x80\x998@\xd6G>\x88\x18\x0e\xdb\xc329\x7fD~\xbe\xac\xc1\xd9\x05z\x8aP\x175\xad\xf90\x13\xaa\x13/=|\xf6T\xb9\xf5f\x95\xe1?\xaf\xca\xbfq\\^\xa2\t\xe9G\x81\xbd\x01\'\x9a\xed\xe4\x87\xee\xee\xd1\xaa\xd4\x1b\xd45\xa9\xb1\x14\xc4\x98)0\xde9/\xfe{~/\xd3\x05:|\xd4\x9d~\xde\xce\x8a\xd8\x80\xad\xc6\x19\xddzk\\\xb8$\xafY/\x90\xd3*L\xf7\xf5V\xd3\xa7E\x86\xf1Y=\x81\xfd\xcd\xa6n\xd3\xe4\xa362\xb6\xed\xa5\x8e\xa4\xb3\x0eC\xee^i^_\xaa\xf8\xc1\x93f\x7f\xb1\xdcr\xd8\xcc\x9bV\x17\xec\x14W\x0e\xbcUPw\x02"/L\xbc\x1b\xdb\x8c\x91G\xae\xfaI\xfbY\x8f\x9d\xa1\xab\xf0)\xb0J\x9b#\xc4a\xccw\xc9\xc3\x89A3\x9b\xcc\x87\xccx\xb2\x8c\xa4\xb4\xe6c\xc9\xd3Y:\x1d\xc8=\xd8K\x8bn\xe7\xf6\xa3\xf2\xc7\xe1\xffm\x14\xf1m\x80\xb91\x81`&\xc5\xab#Q+r\x14\xb4\xa6!tI\x8aNS\x179r9\x8b\x95\xbe\xf8\r\xd0P\x1f\x06\xe7\xd7V\xe3\x06\x98\xec\xa1\xeby\xe6cm\x88\xd3\xd6<\x1c\xea\x12%\xb5\x1b\x9b\r\xe6\xb4\xfba\x04\x81\xa2\xd1W-x\xe9\xb9\xc5e`\xf1\xcd\x9e\x83Z\x10\xeb-[\xa0\x95\xe1]\xf2)\x0f+{fW6C\x19$\xddd\x8a\n\xa4^\xbe\xf6\n\xe9\x1eI\x1fD\xf5\xdc9O\xe95!\xd9p\x87\x06\xbbgCh\x10\xebjI\xc9\x13n\x8e\xa0\x1bU\xf3./\xb1xU\xab\x1e\xe1\r\xcd\x8d\xa4Od\x14~R\x83\xe4F5r\xbb\xd8-{=\xb5\x9f<\x1er\xe7v\xf7&8\xdfD\x9f\xab/B\xcf\x0e\x87\xf4\xc9G\x8c\x1e\xf77Bem\x96D)!t\x1af\xbe\x84\x91\xe2\x10\x0bmb\xee\xa7%3\x95\xf6\xdc\xcd\xfc\xfd\x00S\xe3\xa13\xbc\xa33m\xfe\xa4\x91\xc7\xaeG%\\\x87)\xdc\xd2=\xef$\xb5\x8ew\x13\xba\xa2\xc0\xfc\xaal,!>\x17>\xd0D\xf7un\x8cI\x98D\x056@\x88y@"\x05T\xec\xd5a\xe66\x1d)\xf2\x80 \xf5&o\xa5\xda\xcd\xde_\x86-\x00\xcb\x02\xfa\xc7\t\x05\xfcX"\x9d\xb8\xbbSe=\xdey\x0e\xbb@\x00\xba\x9bpb\xbd\x98\xe1\x9az\xa9\xdd\xdd\xd5\x00B\xecu\xb0\x08\xf8\xbb\x0f\xf7z\xfb\xd8j\x14\xe9i]\xced\x00\xf7\xdb\x01\xe2\x03\xda\xf2\xbf)-\xad*,\x05\xd7\x11\xbc\xfc,[\x0f\xcb\x8b#\xfdt\x04A\x11\xfb\x95\xe5\xd1\x1e\xbf\x81\x16t\xa4\x81,\r\xb6\x02\x17\xcd\xa1t\xb4MX.\xbd\xcabFn\x0c\xa6\xb8g@\x0f\x14g~_"\xb9\xe9\x8cu\x94\xcc\x8dX~V\xacv\x86v\x98\t\x8d\xbc\xfe\x80\xee\x1c%\xcdJMj\x18\x90\xcf\t\xb4\x8d\rw\x1eK\xfd\xb3n\x0f\xf8|9/\x04\xd2\tIC\x8f\xfe%\xef;\x86\xb2Sm\x7f\x8f\x87\xb2\xa79(\x1a\x15\xb6\x80G\x81)\x9cg\xe0\x19# \xdd\x11Z)\x8f\x87\xc2s$.\xa89\xeb\xd8\x14\xbb#\x8a\xf0\xbc\xd5\xa9\x00\x10\xf9W[M\xf9\xc37B-.\xd9\x8e]\xfa \xf9\x01\x9b\x1fb\x13h~\x12\x11\x86\xf1\xd0\xcb\x8c>B\xf2\xfe\x82!\x8f\xb2\xa1vi\xf5i\\\xcfD\xcc\xb3\xfe\xda\xdcpin}\xa4t\xc9\x02\xa5\xe4\x1c\x17\xf9\x05H\xdf\x02\xf2\xa3n\xac(*\x9f\xb2\xec\xf0`\xbe\r\xb8\x04\xfd\x0f\x19\xd7&v\xd4\x9dA\xa5l\x01\xc7\xa7\xd8\x97B\x83\xe1\x9bD`v\xb4\xad\xe9\xcc+\xc1J\xa6\xb8\xe0\xc1\xf6\x9e\x8e@\xb3\x00\rc\x9e\x08\xbe\xedq%~"\xa0\x19J\x90\x96a\xb8\xc5\x8c\x012$M\x97K\x14e\x068\xda\x03D\x13On\xff\xd9\x1f\x88\xb6`\xe4K\xda\xed\x9b-\x02w,t\xc8\xd8\x18\xe9f\xfd\xa9\xc4\x82\xc9p\x04\xf9CJ\x18\x9e\x13\x07\xce>(') + +tgsrep = pkt.root.encPart.decrypt(sessionkey) +assert tgsrep.nonce == 0x7a33e06a +assert tgsrep.flags == '01000000100001010000000000000000' +assert tgsrep.renewTill == '20231213110146Z' +assert tgsrep.encryptedPaData[0].padataValue.flags == 0x1f + ++ Kerberos FAST tests + +% Same than in kerberos.rst + += FAST - Parse FAST AS-Req + +pkt = Ether(bytes.fromhex('52540013d0835254003ea3be08004502089636a1400080063ad3c0a87fd2c0a87fc8fecc0058eea93069573b278e50180402897400000000086a6a82086630820862a103020105a20302010aa38207a23082079e3082079aa10402020088a28207900482078ca082078830820784a082064a30820646a003020101a182063d048206396e82063530820631a003020105a10302010ea20703050000000000a38205796182057530820571a003020105a10c1b0a444f4d312e4c4f43414ca21f301da003020102a11630141b066b72627467741b0a444f4d312e4c4f43414ca382053930820535a003020112a103020102a282052704820523acc8b7671c0d50522f1a8d8452ce450aceb40fff0229e8ee546bccf1512e4877ef93dde465595260a6a5a8e85ea38600ce8dff7d510f3c744e2c43eb9d3187d638f716c29b6e7aa9eb407de28d0161f49013966eda0a161ff174dad42e7aa500cfe298541215448013ffe4883b6b1166f908f50de129487fe77fff874fd4102cdcce8db8dbeb8da02f08cc88b3790cdad5ec499959c7e79d6fef107d1e17ce80cc3df050b7e7a1c31f278e4fd4ea9523c950876f174be363234f8495b9550de1560ba17daeafbf133f78991053d929ad3fd668327d42288e6581671daaef908682ee282e17c31d8f8bb55d27fce155ee2e84a2ff8bc9600891be15e6ede3e1bbd2742a7af8b0a32c48973c9e3776a69647bab11592756c5a15b9101c392efa35d000abb3dabccd97e64426e3fd8d47e0e369c83b5391f38947d536d351c061081d654eef1a3861cdb2ea2bc48222b450d1b7d09c0670493bccc60dfcaa5cfe46fd50adf8e388204a4691dc5f0c3dbae0b4da6ac2dd781f149a444840aaa3a3c3befb5a5c04ee0405baed66afcf9b988d10ea14a955f43df79465e6fc02a12bce3870988950f1ab48e1a4f876f351671c5061e6399a63cb0479f7bd017dfd9bc5be192faf6d4f11e6ee6003933eeaf632f0056c4c1ccd183d7977cfca85419fe5b039674419d802068e792c9576ae2a88bfbeb1f59273226782c6efb288717d8f7a4bc3bf4c697fcac1adc1829f0a914f2559b278ccadd108eb87a11dacc88e4302e9af627474e57171192b94c6b358f8f98e308596215d2fb9d9c2b49c4cbedcb43fc231b86f0493d56b82962cf3383a84f8922c2b99f8fa8fdd85797b09a6e60f72007c0379988be2ff1cfc16f21300c1b4b784174005a9185f760e68ef94b9384eb24decee31b63d1b92278cd75b85d4d80c4e83306533a9d95aa6207cbfbeb0970a41c44aba59839f007923ecd8ff0de8314990a435dbea4dedbee16faf5ab2be9f96d691cfa983a6c843bd183f84c1b4998a3eaa907cae6b82b0ae8363f3edd8cb03d3c9c60ff55a84d8a292ea20555fbd6ce5ad4ad7a6b4bc5bff2e02c477a7a8a98d5a387d389caa172c400b151d95871b2aa16a040dc71a9be5f0774b06a5ca87674ccb4109a2c41db9e3160704218ad495d0751194fbef4becae4d7be24b9d968da592256a2b22cf724e989e71a60d0603b59bebd475285f793794b7a18af49a2b68670e3a6247c453274e35c863a16b5023c6c94659e25abb27c760f989ac0bbf9a5b125d0ea34fb03225cc93d5b8b6829e906883ee76cf8ee61dfacc488e8dc5cbc8ba9705a9e915a68f838232394f97fb1aac4a2a90fe17d46f9c51946a2bf9598df7f5b5e7ee692a78860eea3cef748a5be36529228e40b4aec83ebc8bb14176a4c565b06500e9517229b8340c55812101dbbc6bee693c35873082a5a1a53b35cf3509193d4dc5175c9360a00da71692ba205b3264aecc9ecc8bca31fec43efc8701423bb484f6f21699439dd30f71228f16eaab96b7de3547721d1635bbfe50678900ac378a4958b6c34964f3e0dc843880dbde57fb4a76ab85eba2b190bfdaefc7ba17e109f839493b0f2d6fc7ea17403bebe06f2809314ca514606f54668082364ed6752019f27e1df74f93fcf1c25630a29713a89d4a998c444bc91279c6fc66e0aa5dec72be316e1160cf9f90d5915c464b6bfec5216e901be4726db596a15745511c63736a69ac9ecb9e86601c631b4992653c320e6983562fa613134560cb606621e9661ac5961313ee70868ab48d6010173d8a96fffdb2baf4afe18c846d3fed6f30b9a809d72e647735fc536edec543abc232480d28660395a4819e30819ba003020112a281930481901273d5af61ad426d51d0757e897917caeb6fc1b6950554e8d750f95d27f444e3aaf7ae0bf4595b5e906d9682dbdeedcf6eb42a84ab8092997b783f57710127228165deeb2ce5e09e2ddc71555dc31970a8312d888b8ae766382098276d62b4bd76f34cbc889e24ad5405ec037ceb724fdb71fe247fe2a414a037ed33c796f4475fcfb5993eed147b6d63d740d58da5b0a1173015a003020110a10e040c75f02d8d2954e0ae1a9e0653a282011930820115a003020112a282010c04820108ae9bbc4629c80f4a383a69c4583824295c75f34b000b3fdbdaab073a042935e32c29e0ee2b2b446e4a6a2592362d0d593cddd74dacc24f16353776e1b5d192ad1cf5e63f66f40a134ecb87c077c30922bc0cab00ae23d187d56090d9098f843c54fabe7c012ff87e317dfe339c40911264609d489b041a4e9b52c0eb03ee88a393d17da92786bd1716b92eb0d7a5a24a64ade0870dea8a7e138acdf209ee277cb3fadeedab173fd64cc10a1004010774658b94852639bda10a5e8aff29174e3d2c7032c32631b074afdac0e6832bae74de9be19e522f63bc8499753a209291fee1861c29096cc8ee3cfda5be235b0aa95635916edcfcdaf90b896e2eaa5a57d5e4da0b00408f4201a481af3081aca00703050040810010a11a3018a003020101a111300f1b0d61646d2d302d66617374656e62a2061b04444f4d31a3193017a003020102a110300e1b066b72627467741b04444f4d31a511180f32303337303931333032343830355aa611180f32303337303931333032343830355aa70602043f58a7a0a81530130201120201110201170201180202ff79020103a91d301b3019a003020114a112041053525620202020202020202020202020')) + +fastreq = pkt.root.padata[0].padataValue + +assert isinstance(fastreq, PA_FX_FAST_REQUEST) + += FAST - Decrypt fast ticket in AS-REQ + +from scapy.libs.rfc3961 import Key, EncryptionType +krbtgt_hex = "ac67a63d7155791fe31dace230ab516e818c453dfdbd44cbe691b240725c4907" +krbtgt = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex(krbtgt_hex)) + +enc = fastreq.armoredData.armor.armorValue.ticket.encPart +encticketpart = enc.decrypt(krbtgt) +assert encticketpart.authtime == '20220712230225Z' +assert encticketpart.cname.nameString[0] == b"SRV$" + += FAST - Decrypt authenticator in AS-REQ + +ticket_session_key = encticketpart.key.toKey() +assert 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' + +enc = fastreq.armoredData.armor.armorValue.authenticator +authenticator = enc.decrypt(ticket_session_key) + +assert authenticator.crealm == b"DOM1.LOCAL" +assert authenticator.seqNumber == 0 +assert authenticator.ctime == "20220712235437Z" + += FAST - Compute the armor key + +subkey = authenticator.subkey.toKey() +assert 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' + +from scapy.libs.rfc3961 import KRB_FX_CF2 +armorkey = KRB_FX_CF2(subkey, ticket_session_key, b"subkeyarmor", b"ticketarmor") +assert 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' + += FAST - Decrypt KDC REQ BODY from AS-REQ + +enc = fastreq.armoredData.encFastReq +krbfastreq = enc.decrypt(armorkey) + +assert krbfastreq.padata[0].padataType == 0x80 +assert krbfastreq.padata[0].padataValue.includePac +assert krbfastreq.padata[1].padataValue.options == "10000000000000000000000000000000" +assert krbfastreq.reqBody.cname.nameString[0] == b"adm-0-fastenb" +assert krbfastreq.reqBody.etype == [0x12, 0x11, 0x17, 0x18, -0x87, 0x3] +assert krbfastreq.reqBody.addresses[0].address == b'SRV ' + += FAST - Check Fast Armor checksum + +data = bytes(pkt.root.reqBody) +fastreq.armoredData.reqChecksum.verify(armorkey, data) + += PKINIT - Parse AS-REQ with CMS structures (MIT Kerberos) + +pkt = Kerberos(bytes.fromhex('6a820df230820deea103020105a20302010aa3820d4b30820d4730820d2ba103020110a2820d2204820d1e30820d1a80820c4b30820c4706092a864886f70d010702a0820c3830820c34020103310f300d060960864801650304020105003082041c06072b060105020301a082040f0482040b30820407a0733071a00502030a8eb7a111180f32303235303932313130343332385aa20602045ba497a5a316041467a8b2f1aded7272d4840000331ffbbfc942a304a5353033a02204205aeb03e889e99fcd6c205ef484b9dd7b462b9e94c3fe68b115a71cd287fcd775a10d300b0609608648016503040201a182032a308203263082021906072a8648ce3e02013082020c0282010100ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff020102028201007fffffffffffffffe487ed5110b4611a62633145c06e0e68948127044533e63a0105df531d89cd9128a5043cc71a026ef7ca8cd9e69d218d98158536f92f8a1ba7f09ab6b6a8e122f242dabb312f3f637a262174d31bf6b585ffae5b7a035bf6f71c35fdad44cfd2d74f9208be258ff324943328f6722d9ee1003e5c50b1df82cc6d241b0e2ae9cd348b1fd47e9267afc1b2ae91ee51d6cb0e3179ab1042a95dcf6a9483b84b4b36b3861aa7255e4c0278ba3604650c10be19482f23171b671df1cf3b960c074301cd93c1d17603d147dae2aef837a62964ef15e5fb4aac0b8c1ccaa4be754ab5728ae9130c4c7d02880ab9472d455655347fffffffffffffff0382010500028201007b93ec38a6d3a2e5ea4776f7c942c54f06c334ea637cf45e59c21f6638f6b5baa23420d3229c4a418579db1ce3b956d12ec1bce6883621720f2e596a65dd05881745e7524c88447a5e7a45e149e09f163093088716808e6520a471b53631262a19dc4b3b896717ddca77e15c2d8cf31aa1c03a604834e5f852dc4ac86518f53de4d16101c7f26253973987e1f8c6e8298159ff039646052afe14d634891f57abe5787cb023481aceb65c6ee92b123dfd2ddd15f7dcd733be535d063c4d42a309cb7b84163f8924f88c1b3e400b7f78556ba27d0456b739fe261286cffe7ae404379bc2157bf49fc610e4d46339e0e0a380f8e3b818b0bd4f7a038644f12c77bfa2343032300a06082a8648ce3d040304300a06082a8648ce3d040302300b06092a864886f70d01010d300b06092a864886f70d01010ba42c302a300ca00a06082b06010502030602300ca00a06082b06010502030601300ca00a06082b06010502030603a08206243082062030820508a00302010202131b000000028b4c5c90b3392fca000000000002300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313135385a170d3236303932303232313135385a305731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e310e300c060355040313055573657273311630140603550403130d41646d696e6973747261746f7230820122300d06092a864886f70d01010105000382010f003082010a02820101009edc4865105bdbe4843dcb43a1ed273630d4bb84e2c6096cb8ef4d111da3dfc8ad78ff7a02a6ea6da16f2ecd0a7e4a85c7b685b02286298493834f8361a318864bea2f2faa92a3236cd1e373eb2874ff8e09468762de9af0a0881ea098fbeadccb9573e53c90da8398a9992e6e6a46081e23c31527453f9540ab4bca93d7b139a97c3a0392d8c035832005cc1ae2fdbfe098381e62b37cd6b94ea638fd06d2e2dfb4c1c35896d717188fa8c472a42aaf65c04ff1f2a55dbb0b02dcec1f9e07d7dd930ddec43947cf229324bfa5189bfc5a34a59864c95fa2351b506979cf1bc3529a7933be0f2004932490d1a250735bd692af367f5ca326d392c28c99bde1210203010001a38202f3308202ef301706092b0601040182371402040a1e08005500730065007230290603551d2504223020060a2b0601040182370a030406082b0601050507030406082b06010505070302300e0603551d0f0101ff0404030205a0304406092a864886f70d01090f04373035300e06082a864886f70d030202020080300e06082a864886f70d030402020080300706052b0e030207300a06082a864886f70d0307301d0603551d0e041604140a63d8a405fe59c3f3abbef3111f6f6a6a08a973301f0603551d23041830168014ab14d5ae948281f079726970b3b8f97003aa760c3081c80603551d1f0481c03081bd3081baa081b7a081b48681b16c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4443312c434e3d4344502c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f63657274696669636174655265766f636174696f6e4c6973743f626173653f6f626a656374436c6173733d63524c446973747269627574696f6e506f696e743081c006082b060105050701010481b33081b03081ad06082b060105050730028681a06c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4149412c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f634143657274696669636174653f626173653f6f626a656374436c6173733d63657274696669636174696f6e417574686f7269747930350603551d11042e302ca02a060a2b060104018237140203a01c0c1a41646d696e6973747261746f7240444f4d41494e2e4c4f43414c304e06092b06010401823719020441303fa03d060a2b060104018237190201a02f042d532d312d352d32312d313332323235373836362d343033353133333636322d313134303736393232322d353030300d06092a864886f70d01010b050003820101005b76869c48c9e4f28043253b8552a6017dc25f9dc990da86a79210f334c1a7e50b6125ab176bc7bb194b96a02736c9838117071d533e99467bf24219228bb40b6d410c8fb23f129010b68777acb83944842a0af694673206be22c0a0078ee0543962b31bae8d809ef553dbe858cd063a7a06f1ea7d026394ace39f294ad5d8c1b077e58e7d17f86eea918aa88ac09cf55ffcf147aa14a4c64f4216211e45fd8794b2906a29b97bcbd47a0b213768f5403f9aa08fd23ea92664fb9a0246ae75e34f939102fad7c48b8c5bb650203aa48b48bed4635bff4e3386e694d57a4e7e65939c5a5a72997176b5d0e50bd369e78bbf0cda53db204fbf37839223daff3a06318201d4308201d0020101305e304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434102131b000000028b4c5c90b3392fca000000000002300d06096086480165030402010500a049301606092a864886f70d010903310906072b060105020301302f06092a864886f70d010904312204200e44063cba7907120ced545618cd365edc5071fdc806e8fdb990a7c858d37ef9300d06092a864886f70d01010b0500048201008c8e52430905bb06e897cb5eda4a466ebc6bf980a997d662b9a6f94b88173bab6e8b76b375454c7e06f2091f1ef43165e378263290a1dae9243f58a0e234ed0a082364afe9529b8e5ffee1df77f67a448f6461fac44562ca919381146d5c73e5e643ef8936765cb45661dcf4cf8b7652eee81712037ab7f007046e62ee98ea5f9d3acf426462591e9726f8a50677d935ebaf2f1fbc046033b6cb601c67d1bfe0b4485ab99fb1862500e861a114a03f1b693dd674a28516a240698c516bf94f09dde7ef80772e5098083bf3916ced80118d8f9f0bc737ec15c3d65cfb85e0d186ab11e9c7ab9383ee8fb4a2b7681c6c97edc8fcd48b7bb2dac49c52d70fab5ec1a181c83081c53081c28049304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341815d305b304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434102106b671318bb858b8e437e4229b0d32f1282160414ab14d5ae948281f079726970b3b8f97003aa760c300aa10402020096a2020400300aa10402020095a2020400a4819230818fa00703050050000010a11a3018a003020101a111300f1b0d41646d696e6973747261746f72a20e1b0c646f6d61696e2e6c6f63616ca321301fa003020102a11830161b066b72627467741b0c646f6d61696e2e6c6f63616ca511180f32303235303932323130343332325aa70602045ba497a5a81a301802011202011102011402011302011002011702011902011a')) +assert isinstance(pkt.root.padata[0].padataValue, PA_PK_AS_REQ) + +pk_preauth = pkt.root.padata[0].padataValue +assert len(pk_preauth.trustedCertifiers) == 1 +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[0].rdn[0].type.oidname == "dc" +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[0].rdn[0].value.val == b"LOCAL" +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[1].rdn[0].type.oidname == "dc" +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[1].rdn[0].value.val == b"DOMAIN" +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[2].rdn[0].type.oidname == "commonName" +assert pk_preauth.trustedCertifiers[0].subjectName.directoryName[2].rdn[0].value.val == b"DOMAIN-DC1-CA" +assert pk_preauth.trustedCertifiers[0].issuerAndSerialNumber.serialNumber.val == 142762589450708598374370602088381230866 + +authpack = pk_preauth.signedAuthpack.content.encapContentInfo.eContent +assert [x.algorithm.oidname for x in authpack.supportedCMSTypes] == [ + 'ecdsa-with-SHA512', + 'ecdsa-with-SHA256', + 'sha512WithRSAEncryption', + 'sha256WithRSAEncryption', +] +assert [x.kdfId.oidname for x in authpack.supportedKDFs] == ['id-pkinit-kdf-sha256', 'id-pkinit-kdf-sha1', 'id-pkinit-kdf-sha512'] +assert authpack.pkAuthenticator.nonce == 0x5ba497a5 +assert authpack.pkAuthenticator.freshnessToken is None +assert authpack.pkAuthenticator.paChecksum2.checksum.val.hex() == "5aeb03e889e99fcd6c205ef484b9dd7b462b9e94c3fe68b115a71cd287fcd775" +assert authpack.pkAuthenticator.paChecksum2.algorithmIdentifier.algorithm.oidname == "sha256" + += PKINIT - Parse AS-REP with CMS structures (MIT Kerberos) + +from scapy.layers.tls.cert import Cert + +pkt = Kerberos(bytes.fromhex('6b82109730821093a003020105a10302010ba2820987308209833082097fa103020111a282097604820972a082096e3082096a808209663082096206092a864886f70d010702a08209533082094f020103310f300d060960864801650304020105003082012b06072b060105020302a082011e0482011a30820116a082010a03820106000282010100ffb1e474ea4bb6c9248cec29ddf54feac8bf6a3261fd25dfc32e258e0056fb2caf4fb76f90961d706b98c0b16fedadf049aa2c3dda6e5eb42933828b932b8dd10f2e00caa1eb2901df080805fbe8d00cae67e9e35e9c197c362416d09fbfa5ef10e556b7993c7501566156dd431e5ae35eb9d00b86ec529b1af887b7671de382ddf4ec2ce87d71fe1ab3fa6d0338bb9c9d2794feba33356b149bc3d1745f4f2feca0ae97f62aaf3314fa1464c844fc016dd82e3008e2cd3cc762d0cc264981497342820f8c8f4aef29147346aea727cc24c3e64f474a5a2448325123d8d217fb65eaabbb7aab36db0e905e5df46de3686bc94581580924f0226385d2db6c599ca10602044e744899a08206303082062c30820514a00302010202131b00000003b9cb9e577efbe605000000000003300d06092a864886f70d01010b0500304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d4341301e170d3235303932303232313232395a170d3236303932303232313232395a301b31193017060355040313104443312e444f4d41494e2e4c4f43414c30820122300d06092a864886f70d01010105000382010f003082010a0282010100e6832a3e3ca595057e9f70733d34ea5153e4769dc95eb98d56d7b28e8c0390cdd7bfecf01b5f12931afe8d0a1c2e69b83466b3f8ef88c4b31f2c0a49bcee9fb7b78a7e71c6c69c260e606b96d9d2ba534430c6b5cd3be7ef98110e92a0175b66b0b501d32a39dc17f30033fd0c8fa508e5c781c2d130bc8dfd7cb3a8982bd65c16a15f175e7205337dd17b6f4358644db4e6ad3a0f83a2c605275ef0cf4ca2cf974386283d141f4fb0b1f6d72720b83e4155bd0ac39f6ca7723ca317ae6340f746b4c82195addce715e31928ee0e67cb357d3200ee0b26ee422008c8c3de5c1a5acae88e10c89edff4ccd8543f6eaa551c15ed5d8e756567e39c5d56edb948fd0203010001a382033b30820337302f06092b060104018237140204221e200044006f006d00610069006e0043006f006e00740072006f006c006c00650072301d0603551d250416301406082b0601050507030206082b06010505070301300e0603551d0f0101ff0404030205a0307806092a864886f70d01090f046b3069300e06082a864886f70d030202020080300e06082a864886f70d030402020080300b060960864801650304012a300b060960864801650304012d300b0609608648016503040102300b0609608648016503040105300706052b0e030207300a06082a864886f70d0307301d0603551d0e041604148994b7358e085091a011cd226c9305cdcb6b82a2301f0603551d23041830168014ab14d5ae948281f079726970b3b8f97003aa760c3081c80603551d1f0481c03081bd3081baa081b7a081b48681b16c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4443312c434e3d4344502c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f63657274696669636174655265766f636174696f6e4c6973743f626173653f6f626a656374436c6173733d63524c446973747269627574696f6e506f696e743081c006082b060105050701010481b33081b03081ad06082b060105050730028681a06c6461703a2f2f2f434e3d444f4d41494e2d4443312d43412c434e3d4149412c434e3d5075626c69632532304b657925323053657276696365732c434e3d53657276696365732c434e3d436f6e66696775726174696f6e2c44433d444f4d41494e2c44433d4c4f43414c3f634143657274696669636174653f626173653f6f626a656374436c6173733d63657274696669636174696f6e417574686f72697479303c0603551d1104353033a01f06092b0601040182371901a01204101d9d5575a78e1740b7be50767138da8c82104443312e444f4d41494e2e4c4f43414c304f06092b060104018237190204423040a03e060a2b060104018237190201a030042e532d312d352d32312d313332323235373836362d343033353133333636322d313134303736393232322d31303030300d06092a864886f70d01010b05000382010100205571d8ddc2bbb8cfd56b0fbb8d8b6e38ce376c76135f51f25c3f3a98094d59fee193d678b1ff310effa092985394e84ff033094c1889309e29146d239178e2171e192a7c3ae0ce6c653790f9bef3f3281a238c264c5a944e13fa3e97b7ee21e0c22a74b8ab81f4d0d7dc9a592f55efad413ab5041b123f622537e13733eeda845541e5ff8c9973dc5b482701d579f53c67a5ac3fed6c37b1501154d17661f70a252211e9e320269ab7e468bf3d1f1d65f106818122d05d4e2d4db03f1670f66fd9e711970886c7dae937184256023782771d579795d1c331ecd737d0e9d7bb1b4ca24606302bc9c7c10d8aebcfc8a4a2d6beb3fbd3abf14c2680f665f04e35318201d4308201d0020101305e304731153013060a0992268993f22c64011916054c4f43414c31163014060a0992268993f22c6401191606444f4d41494e311630140603550403130d444f4d41494e2d4443312d434102131b00000003b9cb9e577efbe605000000000003300d06096086480165030402010500a049301606092a864886f70d010903310906072b060105020302302f06092a864886f70d010904312204200916c67ea99156a2738927fc51c6ebee43cdb65d715406d1fb6d40daf49c65ca300d06092a864886f70d0101010500048201007fa8498cf70e6f0f9763eaba1f050dda9ca79d343e93312319a457f157586ea849584da69ae8a3ffe26171a9a8cbd3a4b39fb7c8959ebadf42a69c4c626abcd59aac719042b2b9c90ea81bb7593618641d2b498cd6bd65322ed3dcde8895a68b0889c804ce8526cfee27d664a3cd0cc9f1a74531d029cafe4de15bb14bb4d36659fe276f126cccf421c91db7d5be02fb8185cd0de03bee08f424fc48cb4c3f4294f3225752c09abc33ca358b43b5cf3b7df109e37051f757e08a3caaad1d77d9f310a9bd8ea263a00431b57bf4b37c3b0f998a47209a406531c8fa3ff0fd4aec04e3574b2485bc6ac01d077064b67846a71600b65ff6d417441e034c7bd080eca30e1b0c444f4d41494e2e4c4f43414ca41a3018a003020101a111300f1b0d41646d696e6973747261746f72a58205806182057c30820578a003020105a10e1b0c444f4d41494e2e4c4f43414ca221301fa003020102a11830161b066b72627467741b0c646f6d61696e2e6c6f63616ca382053c30820538a003020112a103020102a282052a04820526690c97f510509c672dac4e1436da9aeb5ac4fde72b75b5c1fcfab03aef11139b30b9d32b62f9c4de8e97bc5148563c5bc5fb031e1b1869c69c2991778190d7f27eeeac2ba5e59b54b1b10af66e526fced04f1dd7edd81da93984962183a50e39f82d6f90c9cc9441fec556432ec776a4b595c459cdcfe45489d360f6c950185af170a24897a4eb1127f9c85772fe0417733b254604cd704a15993b77ac18fb1fd8fddb9e888f8c05ffd4c7a5e519593c9e7588c92345ca6ca2e04f0c83231bcc90adccb189e98c2afd53bdb8e665f91d5c3aafde51c60e9b42e88de76261060090483a8dc2d129b66cc3890524004295b5ba440c0a00b352ce91616df2c9a0153e5fd072a9a356dba5e44d79c032afc4d985a180d8fb1f9bba46be602a73ba7c4098683d6ffac4e456b0e51e9473f8ff40e50b437d370b087e1d41089d9f382a17e212d13244f95ad1fb629769c5d53b2ade80c6690fa845efb590a2c6e81851ea9ca1ba319c3a8f86bc62bbcbabf3ecc39d3f6988157a4c390ada2f42b7c577438bcfdb4136bbff92c3c32eb22213e14c51de330f4df4d493bcaa322a3eea01fe504bd03c18786ed385ca205af4396eb6c7b1cfa0e13bcf3e00452461b5c1f9761ef5edb35cfa79fdea04a5420b9762d6f2cf74d694b35c812ba62620c4e6e90ee6483768e38fa0011706d2c093a22202707c5c90cf3ea4c4f17c017f9d7e85a7cc555bf9c9bd4c1337282b5de0395d123ca25c2a9c9eca5cdc31dfd54db75f43eadadd7c7e3a3611a6c1a806c7ff5b0d0b102155978c745a9b4a022018009641ad9197492de70dee4248d159a2b2c5d6adbf253ac04dbe5713cf2878ef440aa68989b2655687a7b35f6a547a8c5a076004c58baa1b231bec7501f5f5bb9f1c66c8cc22d35641cd4442244f349d0351263bb2f1e11b4f4ec26044c87f93a1a963649f5be7a8d61306fe47a10427ac14ba6b9b09ec69950e5176e933cc1fdce258c62ccae4011e8eccab0a36b9ac1d21d36df38c32d65f438d25defa0eb5c577f5f2458304679a9934796be00d3335b7fee1f7616768f0547bd949b764ed2b4684951b35b57a7168fe79d8cd7580dfecddbc30afb8d47032f12cc5b2ee1c16a731dd977f2476989f465fdf6e08992ba566264d1e0a8f0f7274533293e2aa1c418397dc89f9f48d5d9dc3cd82548327be0ebc9b3edeb00f6cf0df8b339d10daae5bb02e9572881fbc5246d5db408fd9cf16209a4ef6b2fc6765d9d9092054b362494be360847b12927ea2fd73b89d345c22fe662f2096952edb69983f147eee5635f40c0bd7bca09b61f644a9df3ecccd0fd0d9c245b4fb224e415bfd58637d341214bc8b2e962df1e7845da642ee4b7a5343c8cee746f6da89cbe3c89f278c6d42f9a743d97ad2c767f1514458db99ca5f29175609e3a7a704b21de8c3cee0646eb60dfb5ddf6824008232dcbdf81b76b941c8af5c0788384bf61f694d80a00b9dc28ae8b0ff782d0b7fcefd60114518401534974c59b88ca89bb4f4a2a5e6e71d7f08342f830338981f534a1affc8ad28b9ec8c54b64321aaafb1d7f049719fda712def001492d6d404bf1da6043687f82378857453b0539a6cd7f452f4b789944a0ca6f238fd321687ac0685a81c9e77f113b7f5c1de3fba74b88f4e0c6cec90e230f16099f9fdb74c57879a98e4a1a85ad672021afc9199fba18c82a7e57c5655ee8d9e8e9cb6d31bc3920d7dbfe667c315b971f99ce2a3579cd1de9d7bd096dc443a1c0dc92e5f7e83657cf38020495585427720ecac4519162cced74d48c294fdb11086e97585d1a9bfaef4af44691f34341fb5e1f42113f82b597bc3e5f5e877116df8a682014a30820146a003020112a282013d048201396d08189c79793463088601b1497f77742a2980900f4c872aa340d07b36c4ff9f9b97740c6f18a15b2277c9c27f1ae7bad8a5899952ca36d3c2d0aa1e6c3137be54a8db29da9d4af03d72f7b9aea0b9f0a099a2421368e875b3cf6fc85454502e74635dd4683b061914c713cb2c551d8a46fdc47bd784c1d2925374cd0f48d4f917ca073563fe570f55f72d64f2de1776bc38e3aa79f0571ad7c64247d80d83fa3bef9f53dc3454634b78a5f207f01160fdd8a8ff4dbdfe2a2d46ccbf84eaf45299b9379b99a1eec016c143d3dc08dc3d9599e5aa62cdf5dc016fab8deba39d81c4d6c5eea684532bb6697ab61ab88d8ac43c5e36bcadd380b31d19dd8475ee68369ff5d8db5cf7733aafe82d96cc2ab68112216d37c44ddb37b336483d75f0566a713cd508a0c66ab7f13d70541e79e8d2038b42a415416d7d')) +assert isinstance(pkt.root.padata[0].padataValue, PA_PK_AS_REP) + +pk_preauth_resp = pkt.root.padata[0].padataValue +assert isinstance(pk_preauth_resp.rep, DHRepInfo) + +dhrep = pk_preauth_resp.rep +assert dhrep.kdf is None +assert dhrep.serverDHNonce is None + +dhkeyinfo = dhrep.dhSignedData.content.encapContentInfo.eContent +assert dhkeyinfo.subjectPublicKey.y.val == 32278489782659599666680674691617740192025480882925125716566496945858046289374524666228146919540757354337943084659625408278197912527087491522001624804516413386428300641892927787473470630419131055568103619174060490124485923206334065346522123445748745649691028061114330596909397680493778434408463632147264526545631660227144914565541288496092534758943967886391259750733078319727386349536272439561387290863606045665780539098807180454586714490639623651326318384483940150461818440884045020628878002871357420738487965588236164888287449564150835059541717449563619851058161535035543798732468578054040817729345202791857657764252 +assert dhkeyinfo.nonce == 0x4e744899 + +certificates = dhrep.dhSignedData.content.certificates +assert len(certificates) == 1 +cert = Cert(certificates[0].certificate) +assert cert.issuer_str == '/CN=DOMAIN-DC1-CA/dc=DOMAIN' +assert cert.subject_str == '/CN=DC1.DOMAIN.LOCAL' + + Advanced Kerberos tests -= Use ancient RFC1964 with InitialContext += Test Kerberos InnerToken wrapping (ancient RFC1964) pkt = GSSAPI_BLOB(b'`\x82\n\xc2\x06\x06+\x06\x01\x05\x05\x02\xa0\x82\n\xb60\x82\n\xb2\xa0\r0\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2\x82\n\x9f\x04\x82\n\x9b`\x82\n\x97\x06\t*\x86H\x86\xf7\x12\x01\x02\x02\x01\x00n\x82\n\x860\x82\n\x82\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0e\xa2\x07\x03\x05\x00 \x00\x00\x00\xa3\x82\x03\xf9a\x82\x03\xf50\x82\x03\xf1\xa0\x03\x02\x01\x05\xa1\x13\x1b\x11SAMBA.EXAMPLE.COM\xa2\x1a0\x18\xa0\x03\x02\x01\x01\xa1\x110\x0f\x1b\x04cifs\x1b\x07localdc\xa3\x82\x03\xb70\x82\x03\xb3\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\x01\xa2\x82\x03\xa5\x04\x82\x03\xa1\x8eA^\xd1\xa6!\x0f\x82\xb9\xbe\x82\xd0\xe8\x8c\xd7\x1bs\xb7\xb4&h\xec\xd6]\x0f\xdc\xc30n\x9f\xc2\xbb\xf03\x93\x027\x88_\xd7\x85I\x81\xf1\xba7\xcf \xa4\xf4\xa3\xc5C\x1d\xe8z\x1f\xb7\x97\xb1\x1e\x93\xcc\x1e\xc2\'\x94\xee\xf3v\xael\x95\x9d5x\xde\xcf\xad\x16\x1c=\x0eDbb\x9e\xbaE\xfc\x9d\xddnu\x19\x1c\xa4x\xf0#\xc8\x1fTI:\xfb\x94\xd7#,\x9f\xf8\xca\t\xf5\xdd\xcf\xd4\'qLy\x85\xac#\xcb\xde\xe1\xc1\x02+\xf8\xf4{.\xe6\xd7`)\x9d[\xfd\xb8\xc3+\xcaF\t\xa1\x97\xd4\x8c\xe3.\xa4\x80\xd1v2\xf8\xff\xb7\x89y\x98\x13&\x94\xe4\x95\\\x12l\xd8j)\xa7\xa4^\xed\xa9\xee\x92\xaf\x99a\x18\x08\x96M\x8d\xe2\xed\xf4J\xf9\xa8\xb9L0b6\xfc\xa6\x82\x84\xa5`Z\\\xe3\x8e\xaaW\xffj\x94\x05\x88(D$\x84\x11\xe3f1\xfb@\x05g\x00\xad\xf9\x92\x9a\x92^/\xe5\xd4J\xbd\x1bH\x98\xe4#\xb2\x87S^p\xb30\xe6hdK\x1fpp\xde\xf3\xf8\x1b1C\x9c\x9f^e\xfa\x1e\r%\xf6@\xe1=#\xd6\xbf\x82\x8c\'\xca\xcf\xf1\xda\xaa\xdch\x7f\x99\x8e\xa8{4_\xb6\xc1\x1a\xb2\xd0\x16Pfb"\x0b\xde\x02\xb8)=\xbbF\xdfg\xd3\xa4CGb\xfd\xe3\xc0\xff\x96\x8a)\xd9\xd4d\x15\xaa\x01\xa7\xa6\x8f\x81\xf3\xedl\xeb\x8a@\x86\xf6dv\x17\xc4\xda\x14a\xbb5\x80\x08\xa4BPR\xe3);\xb7I\xd3\x90\xaa\xb5\x02\xcb ?\xd2\xb5T\x9d\xd0Ho`\xb0r\xd9R\x9fI\x05\xf9b\xd9\xa6\xa8\xae2Q\xed\x1f/@\x1b=bC\xc8\x1d\xbb1\t\xc7\xabBNK\xf4\x0f0Q\x13\x8e\'\xf9\x91\n\x90\xa4\x97\x81S\xda7u\x92<\xa7@\xa0LO\xb7\xa5\x88\x0b\xa8\xd8p\xbbs\x97f\x17\x16\x87\xbe\xff\x84\xcf\xbf\xba=n\xd0w\xeb\x99x\x03\n\xb5\'\x0ewQ\x90;\xed~}}\x1a\xaf\xe5\x9d\xc4r\xe8\xa6\x97\x07AYl\xec\x8b\xc8\xf5I#\x0f\x04#\xf1\xf9\xec\xdf=\xd7\xc25\tC\xa2\x00\x0cr\xa7N\xfa\x1d\x18\x0es\x05\xef\x11\x84\xc2}\xee\xecKW\xc3\xaeo\x8eS\xa3\xa2n\xb3\xd3\xf1\xb0\xfc\xd8\xe8\xd7jp\xf7$\x11\xd2\xafZ\x83\';*\x87\xa6\xc2\n\xd9:\x8cy9d8\x1a\xf7B\n\nr\xa9M\xcf\xf5?\xe1\xa0\xdca\xd3\xc9\xdc\xc6\x04KyQ\x7f)g;\xc8s?0\xab\xf7\xd7\xd7\x85\xdd1]\xd2\x12\xb5\x1c\x87\x05/\xf4\xe4\x8ci\xe3+\xdeH"\xc2\xe7Z\x17\xaa \xd2\xbaKr\xcc\xd0\xa9\x1d\xe2u\xab\xcc\xd9\xc0\x05\xc5\xf2\t\xf5\xb1M\xa4\x84\x1fS\xfe\xb1\x18r\x81\xba\xc9\xfe\x8f\x01\x8c\x12\xd2\xa6Jy\n\x98\xe9\xd1\xfa\x89\x9c\x84\xf8\xd5\x7f3\x92\'\xed\xa9\xc3\xc1\xcd\xcd\xb9\x19\xec\xb2\x08\xa2\xd0\xc1@\x80\xf1\xc1\x1b(\\\xd3\x17\x04\xf8\xbf\x1a\xb4>.\xcbzP>R\xe9\x84V\x04\x92\xf3\r\x9a\xd2\x99\xf0q>K\\\xb5f\x8e\x9c\xc2\xb3\x1f\xebL\x19~\xda^\x1dY\n\x9d\xd11B;n\xcc\xd3\x1e\x1d\xe0\xe2o\x14\xd8_\xaf\'f\r\xe1 \xfaD\xaa\xad7\xac\x81\xd2\xfd\xf1-D\xba\xa8*\x07J\xbb4\x1b\x19ny\x81\x113\x0e]\xfa|T\x91ayS\xe8\xf6y\x9d\x8b1\xf5\xbb\\\xfb8JD\x17Fq\xd4\x8aF\x16\x9ed\x1cJ\x864p\x94k\xe2\xdd\xdc\x15\xb7\x0f*\xae\xa3@\xc2\x92\xcd\x17>|\xc8\xb7\xd7\x1ay \x8b\xbdZ\xef3*~S\x81D\x12}$\x0c\xce\xa7`\xcam\x9a4q\xdfK\x0eE\xbe\xbf,\xfe\x8a\xe6\xd0Q\x03\xe2\x19\xefx\xb6`%\xcb/\xfa&\\\x15\xc8\xa3\x83V\x18N\xad\xce|6r\x01tW\xa4\x82\x06n0\x82\x06j\xa0\x03\x02\x01\x12\xa2\x82\x06a\x04\x82\x06]\xbe\x88N^mh#\x18\xc2\xf0\x8e\xda\xe5E\xab\xe8\x811\xd2\x0e\xd2q\x96\xf3\xb6\r\xa2s\xcf\xe70s\x0eF\x1b\x01~\x9ev\xcc\xb0h`5\x11\x8d\xb4f}\xad\xc9\xbeGG\xe4\x1f,\x08\x8f\xde}\xad\x0f\xee\x00\n`j\xb2\x9fy]>\xd3)w)8\xc4\x88\xf3]2ea\xce\xf5.R1\xe5G\x87\xeb\xa8\x0f4\xcf\x13\xe7\x1d\xcd\x16\x00\xe8\xf5\xc4_1\x95\xb6\x16\xa0b*\xf6\x8e\xd2\xd5\x19s\x1b\xce\x86\xd4)R\xa9\x13i"\xe7}\xda\x8d_\x961\xb3\x8b=\xd3R\xa9\xb8c,\xb3\xb7#\xdbt*\x04\x15\xa5\xa8f\x80m\xe8m\x1b\xb2\xe9\x1f\x1f\\\x1a\xbb\x90x{&@\xc3v\xa5#>\xd2\xb7\xd1y\x1f\xf6&wz\x88\xe2\xdd\xdb\xc0\xbfP\xec\xbf\x9a\xff\xf0"\xdf\x9e\xdd\x87\xb4\x06)2\x12\xd7\xad\x99\xf0\x98\xfdB6<\x8d\x1e\xf5\x0c0\x9e+\x19\xa4\x91E\xcet5\xbbz@M\xd8\x18\t\xdd\xaa\x16V\x87Ii\x0f\xe5)P\x0e\xd32\xbfK\x06j\x14\xcc\x8e&TZ\xfa\x89\x87\xe6\xd0\xe5\xe5[`\x97\x13|0s\x1c\x841Y\xbcT\x19\xa1\x8b\xef\x16k\xde\xf6\x0e\x9fPA^\xfe\xa3S\xd9-\xab\xf2{Y#b(\xcb\x13\x1b\xae\xb0h\x91wy\xfd\xff\x01\x13\x92O\xcc<\xf1\x88\xb7\x07\xc5\xe8,\xa3\x8et\xe7\x186FP\xe9?\x862\x881\xd3E\x91\xea\xf0\xa3I\xba\xc1^\xa1\x1b\xce\xeftZn\xb1m\x1ah\xfa\xe8\xf2z\xb8\x11\xa19Z\x13Y{1\x8a\xa4\xc5LRl(\x91\xf7\xcaI7\x13\xf6\xe4\x1c\xb1\xf6!\xe9;/U~\r\x17\xcd5}J\xcd\x18\xe0\xae\x1a\xca\xdb\x99\x02\x13\xbc\x93\xff\xfe\x82\x90&|\xf4\xf2fI\xbb\xfc\x81m\xc0\x94\xcb\x9a\x0f{\xd3\xa2<\x86g N2\xd8\x8f]NA\x0c?\x8d\x80 S\r\xde\xa6\x87\xd4"W\x9c\xa1\x18p\xbf\xc5e(\x06Bc\x1c\x8e<\xf8D\xb8\xd8\x8b\x88_Q\nh\xb6xW\xd7\xc1l\x08t\xce\xc2\n\x06\xb1\x1b\xe1\x16x\xe6\xb9Q \xba\xdfa\x97\xa9\x9c\xf1\xf3N\x97w\xf8\xfd:!\x93\xa6\xc7\xfc\xcd\xf3\x12\x14\xe5\x8dB\x9d\xe2uY{3\xc8bukA\xfa\x95\xa5\xa3\xcc(-\xf6\\\x9f\x14OD\xef\x0f\x8c\xde\xd0B\'<\xd36hT\xbd\xa0\'\x89\x1f\'\x15`\xbb[\xf8Zx\xdc\xcdx0)\xc2\x8dD-\xa9m\xe3\xd7\x91w\x10\x8aD\xd37+\x8b\xf7\xa7\xa2\x8d{\x0c\xd8\x80\xe1<)lg\xb9\xbfr\x95^)^\x0e\xe5*\xbfGk!5/$01z\xf7\xcf\x86\x1aF\xf2V\x12\xa8w\xad\x070\xf3\x10\x86\xd6\x19\r\xdd\x88\xbe\xc4\xef\xbb\xd2\t,\xa2\xcd9\xbd\x11\x03\xed\xc9X\x98_\x00\xf5\xfa\t<\x9d\xfco/\x84\xca:\x1e\xc6A\xb0\x1f\x8d\x07\x18\x11\\WC\r\x7f\\\xa0\xea\'\xcc\x96\xc7\xd8\x9a\xb4-\r\x88\xc8\x12\x1f\x8b`\n#\x9a\x92\xa9\x86\x85z\x0ctB\xff:\xaf\xbc\xd4F\xcf$R\x8a\x81\xbd\x84\xe03F\x95\xa0\xbb\xdc\xd9\x7f\xc9\x91/\xc3\x9c~m\x9d\xbb\xfd\x8a\x80\xa8\x81\xb1VC\xf5y\x13N\xa6\x1dq\x1bn\xa0\x83\xeaQ\xe4-\xe3m\x99\xcf\xe6\xb2n\xe7\x0e\xea*\x01\xb5\xdb\xf5P\x03\x96\x82\x91\xe9\xa7\x9bm\x9c\x98\xe3j\x85UG\xd9\x0f\xb5\xb47\xd18d\x9f~VL\xa6\x98\xf2.\xf3\x821\xc8\x03\\fP\'\xee\x85\xbf\xdbd\xc1\x023\xf9\xb5D\xda\xe6Y\x0b[\x86\x9b\xbd\x96z\xe67\x05\xba\x1f\xfd\x1f\xb2F\xf2P\xbd<\xd7\xbdUj\xb1@O\xa2}\x02C\xc4\x01eu\x7f%b\xb4\xfc\xe1D\x02\x8f\xbfj\xd7~E\xd5^h\xc8\xc3\xf9\xb3\x1e\xf0\xbb\x02\xfb\x8c\xc4\xc2\xa8&xn)\x08^\xc0H\xbc\x19\xb7-a?N=?\x93\x97\xb2Q\xe0\x04`T\x1bS2\xd8\xbc3d\xef?\x1e\xab\xc2\x82\xcc\xa4\xe7\xd9\xe6\xe2\xd3\xe9Q\x83\x11\xf4\xfb\x82\xa4y\x176\xaf\xf4_\xbf\xa196\xb4\x05B\xc7\xb3\xd2\x0c\x8c\x18\x95\xe1\xba\x97=Y|\x19k\x0c\xf2\xb3\x0fAV\xd1\x04\xeffX\xcd*?\x03S\x92\x0b\x85\x00\x99x+sh\x07\xd2zl\xbbUS\xf0A\x1aS\xa1\x1fFRf\xc6\x9b\x8dV\x85\x14kE\xae\xef\x05\x18Nx\t\xc8K\xd2\xfd1\xc2\xb9H\xde:L\xd5h%c\xa5,$b\xf9\xa2\xce\xa6\xe5X\x11\xb9\x12\xe7\xd6\x1d\x1f\x03\x8e\xba\xc8>=\x8f\xca\xdf\x80U\xce\x16\xb50w\xaes\xa9)\xdd\x863f\xad2\xc6t`\xc1>\x9d;7o\xa6\xef\x08}1S\xb3\xf7\xdf\xa6\xa0@\xae=\xa3\xb8H\x89\x0f\xdd\x7f\xed\xa4\x19\xf5\x94\xc91\xb9B\xca"\x93\xc1\x05&\xbd\x8c\x82\xdf;C\xcb\xd4R\xc8>\xde\xd8j@\x81\xb6\xa7r\xe9\xb5\xb2\xe0\r:\x8d+\x89\xe1\xee\xf5Aj\x8d\xfb\xa0\xd8?\x06\x10D\xcc\xa6?@\'\xc06^\xfa^s\xe6\r\x8d\x1e\x9cv\xd6\xce\xda)Q\x7f\x83\xba\xe0\xc7R\x82\xe9\xbf\xb8\x88\x12\xe7\x13\xc4\xc4/\x8f\x1d\xde\x197\xe8\x9aFe:\xc33\x02\xbc\x85q7\xbc\xde#\x1e\xdb\x7f\xf2#\xda\x80IT,\xc5\xe7\xe7)\x1a\xb0\x0e-\xbe\xf8\x14\xee\xa1\x82\x1c\x99j\xe4}\x84\xb4\xcc\x10\x84\xean\xc8\x9f\xe7=a2\xa7\x84\xa1\x87\x00n\xd7\x9b\xd2\xe8c\xc7\x9f\xca\xbd=\xdch*\x1b\x0f\xceH\x81\xf7\xdc\x1a\x93A\xdbJ\xe3\x936\xe3\xff\xfb!\'\xe3\x1b"\xff\xc6\x1b4\x98\xde\xc1%A3\x16\x7f&\xafM\xdfX\xfb\\\x1d\x91Vp\x19\xcd\xd8\xe3$\x13J\x9c\x89\xbc~\x07O\xac?\x0c\xa6\x80yZ\xef0\xef}\x89BA\xe9k\xfa\xf9P\x97\xe5\x14\xd4+/_\xa6\xba\xf9\x04Ph\xe1\x1a\xb5=\xd6nq\xd8\x13L\x03\xd5\x19V\xd9e&\xdfJ\x99\x90\xca\xc7\x84\xfb\x08H\xa6Y\xc0T[\x87\xbeok\xb4\xeb\xca\xdb\x9d\xcf|\xbdn\x9f\xde\xb10\xecnWc\x80\x18\x07\xfb\x1eYb{Q\x0e\x0f\xfc\xcbE\xcct\xfe\xd7\x8a\xb6\x1a\x17\xba\xeb\xfdG\xdbz\xa8\xe89\xb5[\x0e\x83kO\xdc|\x14\x92\xdc3\nc\x05~e1') -assert isinstance(pkt.innerContextToken.token.mechToken.value.root, KRB_InitialContextToken) -assert pkt.innerContextToken.token.mechToken.value.root.innerContextToken.TOK_ID == b'\x01\x00' -krb = pkt.innerContextToken.token.mechToken.value.root.innerContextToken.payload -assert isinstance(krb, Kerberos) -assert krb.root.ticket.sname.nameString == [b"cifs", b"localdc"] +assert isinstance(pkt.innerToken.token.mechToken.value.root, GSSAPI_BLOB) +assert pkt.innerToken.token.mechToken.value.root.innerToken.TOK_ID == b'\x01\x00' +krb = pkt.innerToken.token.mechToken.value.root.innerToken.root +assert isinstance(krb, KRB_AP_REQ) +assert krb.ticket.sname.nameString == [b"cifs", b"localdc"] + += MSPAC - Parse WIN2K-PAC (real life) -= Parse WIN2K-PAC (real life) +from scapy.layers.msrpce.mspac import * # PAC in the example from https://scapy.readthedocs.io/en/latest/layers/kerberos.html#decrypt-fast @@ -101,7 +267,7 @@ assert [type(x) for x in pkt.Payloads] == [ UPN_DNS_INFO, NDRSerialization1Header, PAC_ATTRIBUTES_INFO, - PAC_REQUESTOR, + PAC_REQUESTOR_SID, PAC_SIGNATURE_DATA, PAC_SIGNATURE_DATA, ] @@ -110,16 +276,16 @@ assert [type(x) for x in pkt.Payloads] == [ assert pkt.Payloads[2].Upn == 'SRV$@dom1.local' assert pkt.Payloads[2].DnsDomainName == 'DOM1.LOCAL' assert pkt.Payloads[2].SamName == 'SRV$' -assert pkt.Payloads[2].Sid == b'\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xfa*@1\xb2f\xa6\x1c\x11dp\\P\x04\x00\x00' +assert pkt.Payloads[2].Sid.summary() == 'S-1-5-21-826288890-480667314-1550869521-1104' -assert pkt.Payloads[3].value.Claims.ClaimsSet.value.value[0].value.data.ClaimsArrays.value.value[0].usClaimsSourceType == 1 -claimentry = pkt.Payloads[3].value.Claims.ClaimsSet.value.value[0].value.data.ClaimsArrays.value.value[0].ClaimEntries.value.value[0] -assert claimentry.Id.value.value[0].value == b'ad://ext/AuthenticationSilo' -assert claimentry.Values.value.StringValues.value.value[0].value.value[0].value == b'T0-silo' +assert pkt.Payloads[3].valueof("Claims.ClaimsSet.ClaimsArrays")[0].usClaimsSourceType == 1 +claimentry = pkt.Payloads[3].valueof("Claims.ClaimsSet.ClaimsArrays")[0].valueof("ClaimEntries")[0] +assert claimentry.valueof("Id") == b'ad://ext/AuthenticationSilo' +assert claimentry.valueof("Values.StringValues")[0] == b"T0-silo" assert pkt.Payloads[4].Flags[0].PAC_WAS_REQUESTED -assert pkt.Payloads[5].Sid == b'\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xfa*@1\xb2f\xa6\x1c\x11dp\\P\x04\x00\x00' +assert pkt.Payloads[5].Sid.summary() == 'S-1-5-21-826288890-480667314-1550869521-1104' assert pkt.Payloads[6].SignatureType == 16 assert pkt.Payloads[6].Signature == b'd\xb0qv\xf8\xd3X\x0b\x7f4\xfe\xda' @@ -127,7 +293,7 @@ assert pkt.Payloads[6].Signature == b'd\xb0qv\xf8\xd3X\x0b\x7f4\xfe\xda' assert pkt.Payloads[7].SignatureType == 16 assert pkt.Payloads[7].Signature == b'\x835J\xa7\x80\xb1S\xcez\x8b\xd2\xc2' -= Parse WIN2K-PAC (MS-PAC sect 3) += MSPAC - Parse WIN2K-PAC (MS-PAC sect 3) # Example data from [MS-PAC] sect 3 - Structural example @@ -136,11 +302,11 @@ data = b'0\x82\x05R0\x82\x05N\xa0\x04\x02\x02\x00\x80\xa1\x82\x05D\x04\x82\x05@\ pkt = AuthorizationData(data) assert isinstance(pkt.seq[0].adData.Payloads[0], NDRSerialization1Header) -k = pkt.seq[0].adData.Payloads[0].value.data +k = pkt.seq[0].adData.Payloads[0].value assert isinstance(k, KERB_VALIDATION_INFO) -assert k.EffectiveName.Buffer.value.value[0].value == b'lzhu' -assert k.LogonDomainName.Buffer.value.value[0].value == b"NTDEV" -assert "S%s" % "-".join(str(x) for x in k.LogonDomainId.value.SubAuthority) == 'S21-397955417-626881126-188441444' +assert k.valueof("EffectiveName.Buffer") == b'lzhu' +assert k.valueof("LogonDomainName.Buffer") == b"NTDEV" +assert "S-1-5-%s" % "-".join(str(x) for x in k.LogonDomainId.value.SubAuthority) == 'S-1-5-21-397955417-626881126-188441444' assert len(k.ExtraSids.value.value) == 13 assert [x.RelativeId for x in k.GroupIds.value.value] == [3392609, 2999049, 3322974, 513, 2931095, 3338539, 3354830, 3026599, 3338538, 2931096, 3392610, 3342740, 3392630, 3014318, 2937394, 3278870, 3038018, 3322975, 3513546, 2966661, 3338434, 3271401, 3051245, 3271606, 3026603, 3018354] @@ -154,7 +320,7 @@ assert len(pkt.seq[0].adData.Payloads[2].Signature) == 16 assert isinstance(pkt.seq[0].adData.Payloads[3], PAC_SIGNATURE_DATA) assert pkt.seq[0].adData.Payloads[3].Signature == b'\xf7\xa54\xda\xb2\xc0)\x86\xef\xe0\xfb\xe5\x11\nO2' -= Build WIN2K-PAC (MS-PAC sect 3) += MSPAC - Build WIN2K-PAC (MS-PAC sect 3) pkt = PACTYPE( Buffers=[ @@ -165,509 +331,508 @@ pkt = PACTYPE( ], Payloads=[ NDRSerialization1Header( - Version=1, Endianness=16, CommonHeaderLength=8, Filler=3435973836 + Version=1, + Endianness=16, + CommonHeaderLength=8, + Filler=3435973836, ) / NDRSerialization1PrivateHeader(ObjectBufferLength=1184, Filler=0) / NDRPointer( - ndr64=False, referent_id=131072, - value=KERB_VALIDATION_INFO_WRAP( - ndr64=False, - data=KERB_VALIDATION_INFO( - LogonTime=FILETIME( - dwLowDateTime=258377425, dwHighDateTime=29780581 - ), - LogoffTime=FILETIME( - dwLowDateTime=4294967295, dwHighDateTime=2147483647 - ), - KickOffTime=FILETIME( - dwLowDateTime=4294967295, dwHighDateTime=2147483647 - ), - PasswordLastSet=FILETIME( - dwLowDateTime=4265202711, dwHighDateTime=29772408 - ), - PasswordCanChange=FILETIME( - dwLowDateTime=681808919, dwHighDateTime=29772610 - ), - PasswordMustChange=FILETIME( - dwLowDateTime=2535740439, dwHighDateTime=29786490 - ), - EffectiveName=RPC_UNICODE_STRING( - Length=8, - MaximumLength=8, - Buffer=NDRPointer( - referent_id=131076, - value=NDRConformantArray( - max_count=4, - value=[ - NDRVaryingArray( - offset=0, actual_count=4, value=b"lzhu" - ) - ], - ), - ), - ), - FullName=RPC_UNICODE_STRING( - Length=36, - MaximumLength=36, - Buffer=NDRPointer( - referent_id=131080, - value=NDRConformantArray( - max_count=18, - value=[ - NDRVaryingArray( - offset=0, - actual_count=18, - value=b"Liqiang(Larry) Zhu", - ) - ], - ), + value=KERB_VALIDATION_INFO( + LogonTime=FILETIME(dwLowDateTime=258377425, dwHighDateTime=29780581), + LogoffTime=FILETIME( + dwLowDateTime=4294967295, dwHighDateTime=2147483647 + ), + KickOffTime=FILETIME( + dwLowDateTime=4294967295, dwHighDateTime=2147483647 + ), + PasswordLastSet=FILETIME( + dwLowDateTime=4265202711, dwHighDateTime=29772408 + ), + PasswordCanChange=FILETIME( + dwLowDateTime=681808919, dwHighDateTime=29772610 + ), + PasswordMustChange=FILETIME( + dwLowDateTime=2535740439, dwHighDateTime=29786490 + ), + EffectiveName=RPC_UNICODE_STRING( + Length=8, + MaximumLength=8, + Buffer=NDRPointer( + referent_id=131076, + value=NDRConformantArray( + max_count=4, + value=[ + NDRVaryingArray(offset=0, actual_count=4, value=b"lzhu") + ], ), ), - LogonScript=RPC_UNICODE_STRING( - Length=18, - MaximumLength=18, - Buffer=NDRPointer( - referent_id=131084, - value=NDRConformantArray( - max_count=9, - value=[ - NDRVaryingArray( - offset=0, actual_count=9, value=b"ntds2.bat" - ) - ], - ), + ), + FullName=RPC_UNICODE_STRING( + Length=36, + MaximumLength=36, + Buffer=NDRPointer( + referent_id=131080, + value=NDRConformantArray( + max_count=18, + value=[ + NDRVaryingArray( + offset=0, + actual_count=18, + value=b"Liqiang(Larry) Zhu", + ) + ], ), ), - ProfilePath=RPC_UNICODE_STRING( - Length=0, - MaximumLength=0, - Buffer=NDRPointer( - referent_id=131088, - value=NDRConformantArray( - max_count=0, - value=[ - NDRVaryingArray(offset=0, actual_count=0, value=b"") - ], - ), + ), + LogonScript=RPC_UNICODE_STRING( + Length=18, + MaximumLength=18, + Buffer=NDRPointer( + referent_id=131084, + value=NDRConformantArray( + max_count=9, + value=[ + NDRVaryingArray( + offset=0, + actual_count=9, + value=b"ntds2.bat", + ) + ], ), ), - HomeDirectory=RPC_UNICODE_STRING( - Length=0, - MaximumLength=0, - Buffer=NDRPointer( - referent_id=131092, - value=NDRConformantArray( - max_count=0, - value=[ - NDRVaryingArray(offset=0, actual_count=0, value=b"") - ], - ), + ), + ProfilePath=RPC_UNICODE_STRING( + Length=0, + MaximumLength=0, + Buffer=NDRPointer( + referent_id=131088, + value=NDRConformantArray( + max_count=0, + value=[ + NDRVaryingArray(offset=0, actual_count=0, value=b"") + ], ), ), - HomeDirectoryDrive=RPC_UNICODE_STRING( - Length=0, - MaximumLength=0, - Buffer=NDRPointer( - referent_id=131096, - value=NDRConformantArray( - max_count=0, - value=[ - NDRVaryingArray(offset=0, actual_count=0, value=b"") - ], - ), + ), + HomeDirectory=RPC_UNICODE_STRING( + Length=0, + MaximumLength=0, + Buffer=NDRPointer( + referent_id=131092, + value=NDRConformantArray( + max_count=0, + value=[ + NDRVaryingArray(offset=0, actual_count=0, value=b"") + ], ), ), - UserSessionKey=USER_SESSION_KEY( - data=[ - CYPHER_BLOCK(data=b"\x00\x00\x00\x00\x00\x00\x00\x00"), - CYPHER_BLOCK(data=b"\x00\x00\x00\x00\x00\x00\x00\x00"), - ] - ), - LogonServer=RPC_UNICODE_STRING( - Length=22, - MaximumLength=24, - Buffer=NDRPointer( - referent_id=131104, - value=NDRConformantArray( - max_count=12, - value=[ - NDRVaryingArray( - offset=0, actual_count=11, value=b"NTDEV-DC-05" - ) - ], - ), + ), + HomeDirectoryDrive=RPC_UNICODE_STRING( + Length=0, + MaximumLength=0, + Buffer=NDRPointer( + referent_id=131096, + value=NDRConformantArray( + max_count=0, + value=[ + NDRVaryingArray(offset=0, actual_count=0, value=b"") + ], ), ), - LogonDomainName=RPC_UNICODE_STRING( - Length=10, - MaximumLength=12, - Buffer=NDRPointer( - referent_id=131108, - value=NDRConformantArray( - max_count=6, - value=[ - NDRVaryingArray( - offset=0, actual_count=5, value=b"NTDEV" - ) - ], - ), + ), + UserSessionKey=USER_SESSION_KEY( + data=[ + CYPHER_BLOCK(data=b"\x00\x00\x00\x00\x00\x00\x00\x00"), + CYPHER_BLOCK(data=b"\x00\x00\x00\x00\x00\x00\x00\x00"), + ] + ), + LogonServer=RPC_UNICODE_STRING( + Length=22, + MaximumLength=24, + Buffer=NDRPointer( + referent_id=131104, + value=NDRConformantArray( + max_count=12, + value=[ + NDRVaryingArray( + offset=0, + actual_count=11, + value=b"NTDEV-DC-05", + ) + ], ), ), - Reserved1=[0, 0], - Reserved3=[0, 0, 0, 0, 0, 0, 0], - LogonCount=4180, - BadPasswordCount=0, - UserId=2914711, - PrimaryGroupId=513, - GroupCount=26, - GroupIds=NDRPointer( - referent_id=131100, + ), + LogonDomainName=RPC_UNICODE_STRING( + Length=10, + MaximumLength=12, + Buffer=NDRPointer( + referent_id=131108, value=NDRConformantArray( - max_count=26, + max_count=6, value=[ - PGROUP_MEMBERSHIP(RelativeId=3392609, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=2999049, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3322974, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=513, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=2931095, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3338539, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3354830, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3026599, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3338538, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=2931096, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3392610, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3342740, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3392630, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3014318, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=2937394, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3278870, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3038018, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3322975, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3513546, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=2966661, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3338434, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3271401, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3051245, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3271606, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3026603, Attributes=7), - PGROUP_MEMBERSHIP(RelativeId=3018354, Attributes=7), + NDRVaryingArray( + offset=0, actual_count=5, value=b"NTDEV" + ) ], ), ), - UserFlags=32, - LogonDomainId=NDRPointer( - referent_id=131112, - value=PSID( - max_count=4, - Revision=1, - SubAuthorityCount=4, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[21, 397955417, 626881126, 188441444], + ), + Reserved1=[0, 0], + Reserved3=[0, 0, 0, 0, 0, 0, 0], + LogonCount=4180, + BadPasswordCount=0, + UserId=2914711, + PrimaryGroupId=513, + GroupCount=26, + GroupIds=NDRPointer( + referent_id=131100, + value=NDRConformantArray( + max_count=26, + value=[ + GROUP_MEMBERSHIP(RelativeId=3392609, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=2999049, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3322974, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=513, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=2931095, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3338539, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3354830, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3026599, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3338538, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=2931096, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3392610, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3342740, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3392630, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3014318, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=2937394, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3278870, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3038018, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3322975, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3513546, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=2966661, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3338434, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3271401, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3051245, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3271606, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3026603, Attributes=7), + GROUP_MEMBERSHIP(RelativeId=3018354, Attributes=7), + ], + ), + ), + UserFlags=32, + LogonDomainId=NDRPointer( + referent_id=131112, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[21, 397955417, 626881126, 188441444], + max_count=4, + Revision=1, + SubAuthorityCount=4, ), - UserAccountControl=16, - SidCount=13, - ExtraSids=NDRPointer( - referent_id=131116, - value=NDRConformantArray( - max_count=13, - value=[ - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131120, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 773533881, - 1816936887, - 355810188, - 513, - ], + ), + UserAccountControl=16, + SidCount=13, + ExtraSids=NDRPointer( + referent_id=131116, + value=NDRConformantArray( + max_count=13, + value=[ + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131120, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 773533881, + 1816936887, + 355810188, + 513, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=7, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131124, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3101812, - ], + Attributes=7, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131124, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3101812, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131128, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3291368, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131128, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3291368, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131132, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3291341, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131132, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3291341, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131136, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3322973, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131136, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3322973, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131140, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3479105, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131140, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3479105, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131144, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3271400, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131144, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3271400, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131148, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3283393, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131148, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3283393, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131152, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3338537, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131152, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3338537, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131156, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3038991, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131156, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3038991, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131160, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3037999, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131160, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3037999, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131164, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3248111, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131164, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3248111, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - PKERB_SID_AND_ATTRIBUTES( - Sid=NDRPointer( - referent_id=131168, - value=PSID( - max_count=5, - Revision=1, - SubAuthorityCount=5, - IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( - Value=b"\x00\x00\x00\x00\x00\x05" - ), - SubAuthority=[ - 21, - 397955417, - 626881126, - 188441444, - 3038983, - ], + Attributes=536870919, + ), + KERB_SID_AND_ATTRIBUTES( + Sid=NDRPointer( + referent_id=131168, + value=SID( + IdentifierAuthority=RPC_SID_IDENTIFIER_AUTHORITY( + Value=b"\x00\x00\x00\x00\x00\x05" ), + SubAuthority=[ + 21, + 397955417, + 626881126, + 188441444, + 3038983, + ], + max_count=5, + Revision=1, + SubAuthorityCount=5, ), - Attributes=536870919, ), - ], - ), + Attributes=536870919, + ), + ], ), - ResourceGroupDomainSid=None, - ResourceGroupCount=0, - ResourceGroupIds=None, - ) - ) - / Padding(load=b"\x00\x00\x00\x00"), - ), - PAC_CLIENT_INFO( - ClientId=127906621700000000, NameLength=8, Name=b"lzhu" - ), + ), + ResourceGroupDomainSid=None, + ResourceGroupCount=0, + ResourceGroupIds=None, + ), + ) + / Padding(), + PAC_CLIENT_INFO(ClientId=127906621700000000, NameLength=8, Name="lzhu"), PAC_SIGNATURE_DATA( SignatureType=4294967158, Signature=b"A\xed\xce\x9a4\x81]:\xef{\xc9\x88t\x80]%", + RODCIdentifier=b"", ), PAC_SIGNATURE_DATA( SignatureType=4294967158, Signature=b"\xf7\xa54\xda\xb2\xc0)\x86\xef\xe0\xfb\xe5\x11\nO2", + RODCIdentifier=b"", ), ], cBuffers=4, @@ -676,12 +841,234 @@ pkt = PACTYPE( assert raw(pkt) == data[22:] -+ Crypto tests -~ disabled += MSPAC - Dissect and rebuild UPN_DNS_INFO + +from scapy.layers.msrpce.mspac import UPN_DNS_INFO + +data = b'4\x00\x18\x00\x18\x00P\x00\x03\x00\x00\x00\x1a\x00h\x00\x1c\x00\x88\x00\x00\x00\x00\x00A\x00d\x00m\x00i\x00n\x00i\x00s\x00t\x00r\x00a\x00t\x00o\x00r\x00@\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x00\x00\x00\x00D\x00O\x00M\x00A\x00I\x00N\x00.\x00L\x00O\x00C\x00A\x00L\x00A\x00d\x00m\x00i\x00n\x00i\x00s\x00t\x00r\x00a\x00t\x00o\x00r\x00\x00\x00\x00\x00\x00\x00\x01\x05\x00\x00\x00\x00\x00\x05\x15\x00\x00\x00\xfe\x00\xb0r\x02\n\xa6\xdd\xa9\xa4e\x02\xf4\x01\x00\x00\x00\x00\x00\x00' + +# This is extended +pkt = UPN_DNS_INFO(data) + +assert pkt.Upn == 'Administrator@domain.local' +assert pkt.DnsDomainName == 'DOMAIN.LOCAL' +assert pkt.SamName == 'Administrator' +assert pkt.Sid.summary() == 'S-1-5-21-1924137214-3718646274-40215721-500' +assert isinstance(pkt.payload, Raw) and pkt.load == b"\x00\x00\x00\x00" + +# Re-build +pkt.clear_cache() +assert bytes(pkt) == data + + ++ Build a CLAIMS_SET to test size_of + += MSPAC - Construct a CLAIMS_SET object + +% the goal of this test is to see if: +% - all intermediate types are properly inferred +% - sizes are properly computed + +from scapy.layers.msrpce.mspac import * + +claimSet = CLAIMS_SET( + ClaimsArrays=[ + CLAIMS_ARRAY( + usClaimsSourceType=1, + ClaimEntries=[ + CLAIM_ENTRY( + Id="ad://ext/AuthenticationSilo", + Type=3, + Values=NDRUnion( + tag=3, + value=CLAIM_ENTRY_sub2( + StringValues=["T0-silo"], + ), + ), + ) + ], + ) + ], + usReservedType=0, + ulReservedFieldSize=0, + ReservedField=None, + ndr64=False, +) + += MSPAC - Check that Pointers, Arrays, etc. were inferred + +assert isinstance(claimSet.ClaimsArrays, NDRPointer) +assert isinstance(claimSet.ClaimsArrays.value, NDRConformantArray) +assert isinstance(claimSet.ClaimsArrays.value.value[0].ClaimEntries, NDRPointer) +assert isinstance(claimSet.ClaimsArrays.value.value[0].ClaimEntries.value, NDRConformantArray) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values, NDRUnion) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values.value.StringValues, NDRPointer) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values.value.StringValues.value, NDRConformantArray) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values.value.StringValues.value.value[0], NDRPointer) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values.value.StringValues.value.value[0].value, NDRConformantArray) +assert isinstance(claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].Values.value.StringValues.value.value[0].value.value[0], NDRVaryingArray) +assert claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].valueof("Values").valueof("StringValues")[0] == b'T0-silo' + += MSPAC - Build the packet + +assert bytes(claimSet) == b'\x01\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x01\x00\x00\x00\x00\x00\x02\x00\x03\x00\x03\x00\x01\x00\x00\x00\x00\x00\x02\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x1c\x00\x00\x00a\x00d\x00:\x00/\x00/\x00e\x00x\x00t\x00/\x00A\x00u\x00t\x00h\x00e\x00n\x00t\x00i\x00c\x00a\x00t\x00i\x00o\x00n\x00S\x00i\x00l\x00o\x00\x00\x00\x01\x00\x00\x00\x00\x00\x02\x00\x08\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00T\x000\x00-\x00s\x00i\x00l\x00o\x00\x00\x00' + += MSPAC - Dissect the packet + +claimSet = CLAIMS_SET(bytes(claimSet), ndr64=False) + +assert claimSet.ClaimsArrays.value.value[0].ClaimEntries.value.value[0].Id.value.value[0].value == b'ad://ext/AuthenticationSilo' +assert claimSet.ClaimsArrays.value.value[0].ClaimEntries.value.value[0].Type == 3 +assert claimSet.ClaimsArrays.value.value[0].ClaimEntries.value.value[0].Values.value.ValueCount == 1 +assert claimSet.valueof("ClaimsArrays")[0].valueof("ClaimEntries")[0].valueof("Values").valueof("StringValues")[0] == b'T0-silo' + + ++ Ticketer++ tests +~ mock + +% Same test ccache as kerberos.rst + += Ticketer++ - Load ticketer module + +from scapy.modules.ticketer import * + += Ticketer++ - Write ccache to disk + +from scapy.utils import get_temp_file + +CCACHE_DATA = bytes.fromhex("0504000c00010008ffffffff0000000000000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000002000000020000000c444f4d41494e2e4c4f43414c000000066b72627467740000000c444f4d41494e2e4c4f43414c0012000000208b4226a190866cbe345ae5e668823edd5359cb00bd479a6428bc8feb1ba55752633332fa633332fa6333bf9a633484770050e100000000000000000000000004486182044430820440a003020105a10e1b0c444f4d41494e2e4c4f43414ca221301fa003020102a11830161b066b72627467741b0c444f4d41494e2e4c4f43414ca382040430820400a003020112a103020102a28203f2048203ee662c2aefcca3f8c78de38e1af1d63b18de011d864d9bec12f3c11e20b0bbdc46e6f5c8311b331b1cc27b23193e90fa47ba7aa6a67fba5826a1f4754ea5050eeab2e07d07a3ec1029b2a11e058ce31e48f4de2bce017e9c2915ee40ffa0f7109597088286fa290fe6ca777465162c5757a67cc53a8e3204846a4ca9cff30c8073d1e9e735b5eb22717f9777c2f38fb13d204952db15e4f160e26535f596f3ce64f9a8d96011718d0405650d7f7c728f87dd2d0e220e4610347faa8a45099b63a351f5adcfccf669d9b6112e31881af869561294a21eb6e2b164b8ce6c6c7b0327ec6c71c23784b06c19030a3f81119f377cb6f0395b5477bffbc5c1a2264ec4af76f4b39a4e2f7030d48c8ebbcaf212036ea0a5abdd5da91fcdc3fb9700d5379f03fbc9fe3a47078dae30b05a418f46ee9ea25f520eb7e67b53d96f7f486e5878b22ea8f4215137a7dcf7f4b6f50463715d9d3c544f294420ed0f7426955fa0a527efce86264f7c29bdfc2cee2c3eb227eb4b7651eb8008e0eb269446a45488296b0427f82b959ad070146cd8a9aed9ef236815bd2149f3f86d73227584f294dc86cf4a77e4eeabf98f4f342dbfc4beb46d834b0c3103d8c5964cad4852eed365ca8e50937e21976122d5cde18c5ab6dd5528c3a680c0a219711766dd5b6a3c103ae65ad5f573a31543a0ebcefde1749062951030f63907cde092010c22c90763248c9f6cd03a6f0a7cb9a7b7441bc7de4c40c1d749373afee597a52c9dbe7533d7ba24a3a26df29474b93643eed97f6b8ffd13976869844841bdd364f2454d6e3ce1ae677ec01c592c25b50e120303240ddaac82dfa9d63b1c42c239b78a6c4ebba2b6458b924931c52b223b9c9cfd6cf0f083e6239e30747f1302de8bde94fe8756b5e0118f5ed61dccc3862ddbc93f103c3160ac15858cbe330420d6e07e2c9f242c2caf8f04d83f3cd71f404c1d56814c9e2aa787763abc295334299487f454e4b4eb5f0e7c3cf5e377374acf827c9fe255e1c7cdb13129ef07c731164ee4eed503f735829a8b7cc2e3718db23d85838fbf7a43861a1c8f890e4c33437b65749946b46f6cff1767158f5684b035f2ea086f7b564f6a57050714b4cad5165b72be6f7a6820b2e9f8936506147e64a77a2f9cf9c13fe4fd59b83191898101068a003e6f7f918006616204ff4b18a9bf495497ba0df0dfcbb89a5e643c60637667357fcf1d97b424240ea75fcf0d26bb159055107f80d1bc682c9057f22a3ef5fb0f50adb30ba975b25069d393bf7eb2522f230912ac1e64bba93c91aa760abb1209bb1313e38dddebcac325d27bef99d66045c09799b71020a44f64bbb59c405449304fd95b8d6bdc6d17e476cba188f30ad04bb6c91d91b028b0953986929a9fb42b21f73028c8ba1f416c70630000000000000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000000000000030000000c582d4341434845434f4e463a000000156b7262355f6363616368655f636f6e665f646174610000000770615f74797065000000206b72627467742f444f4d41494e2e4c4f43414c40444f4d41494e2e4c4f43414c0000000000000000000000000000000000000000000000000000000000000000000000000000013200000000") +KRBTGT = bytes.fromhex("6df5a9a90cb076f4d232a123d9c24f46ae11590a5430710bc1881dca337989ce") + +TICKETER_TEMPFILE = get_temp_file() + +with open(TICKETER_TEMPFILE, "wb") as fd: + fd.write(CCACHE_DATA) + += Ticketer++ - Create and load Ticketer object + +t = Ticketer() +t.open_ccache(TICKETER_TEMPFILE) + += Ticketer++ - Get ticket 0, change it, resign it and set it back + +# mock the random to get consistency +from unittest import mock + +def fake_random(x): + # wow, impressive entropy + return b"0" * x + +with mock.patch('scapy.libs.rfc3961.os.urandom', side_effect=fake_random): + tkt = t.dec_ticket(0, hash=KRBTGT) + assert tkt.renewTill.val == '20220928172927Z' + tkt.renewTill.val = '20220930172927Z' + t.update_ticket(0, tkt, resign=True, hash=KRBTGT, kdc_hash=KRBTGT) + += Ticketer++ - Call show() with ccache + +with ContextManagerCaptureOutput() as cmco: + t.show(utc=True) + outp = cmco.get_output().strip() + +print(outp) + +assert outp == """ +CCache tickets: +0. Administrator@DOMAIN.LOCAL -> krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL + canonicalize+pre-authent+initial+renewable+proxiable+forwardable +Start time End time Renew until Auth time +27/09/22 17:29:30 28/09/22 03:29:30 30/09/22 17:29:27 27/09/22 17:29:30 +""".strip() + += Ticketer++ - Save to disk -# disabled because pycryptodome but they work ! +t.save_ccache() -= Test vectors for KRB-FX-CF2 += Ticketer++ - Read and check written ccache + +EXPECTED_CCACHE_DATA = bytes.fromhex("0504000c00010008ffffffff0000000000000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000002000000020000000c444f4d41494e2e4c4f43414c000000066b72627467740000000c444f4d41494e2e4c4f43414c0012000000208b4226a190866cbe345ae5e668823edd5359cb00bd479a6428bc8feb1ba55752633332fa633332fa6333bf9a633727770050e100000000000000000000000004486182044430820440a003020105a10e1b0c444f4d41494e2e4c4f43414ca221301fa003020102a11830161b066b72627467741b0c444f4d41494e2e4c4f43414ca382040430820400a003020112a103020102a28203f2048203eed3d1adb3a09042173463eb0ef195beb666adbaa83193905697db7340daa9fc6cd3450280651effddc129b3761d49569f3c384e450db9ef094b4619d2036126a0b1b44c983e46664ee28cdb8fc33b52d14d2a8357f6c37b31bec5074ee6ee5ab74a896460c767411d0532c6cb69e0da698054ef8f8bf87fb9e8d2d289ec1b22d1ec602ce71c80b98a14aff448374054d4987c0bd13127914a0191d93c3440b5209c4f2190c80d21e064e6f71ab269ab9c0dbf6533e8e29068a3c686b6377d3c79c902818f12a400eabd8f8bb35bce837e9cb0a4413db223bf22e13bee81eb6a4170ae863fd7082db8dac81b70f96c7880c6d5f8350209aa090b75f6343635ba01e9fafdc7700ee84bd9ae0497517ce69b89e44b3933ea3b1a6c36bd38699eba195bb22f0e694b9e952fc187cf7ee5e02b05ec2397e76c217da3c328eeccf5d4ffbe77a765127fc2828e5c8edc1987cb7fbfcfecbb308f4858f711c52ada9c3622dd43d47c29b30630ecf51b9e88cefcf06cb7862922c36a81ae09ec9f62f406f6d4a269cec849a2fe872a16026dce242c775870d827450700c9defdd204342ea1e7d72c5b1c8d92b0318f298898b19a2c705722837c2ff569fc796d55b779950be0db9955d57d349c7d7688b81b9219e376098a2902e23cd01d7bf7734089ab08bc30a7fd2d138aea4454084e3e14d76119e2ef4da6fff3b5758c58efe2904491f6dd57a7eb777aa847783b6ef905c8c796889e6d7e89952a2cef7f99d09405a07b6897291d13eb3a0c4280601b4f4d5cbd00a0125fb87eeb522cd90a8b046163c076a61115e1affe3e362700d984747f1372c92beeb3e1ce4b97ceac032ac8988c536a9594f9032463750f78ca30161e4910d8ff3810d7d4da60d90fded2fcda92a4d6a7b776ba82370130807a30ab0b648f50537453de6c575cc6c98847ae1aa342c3b324005c3988e6cfb161b5b39153cdbd7a305c4cc0949e47197673cd72c29f41f383a7c2b241bd0e70d736f6e342b88128cc38f964588aa32b860dd788a43fb91d4d934401434d6d9e6c622e58a9d99e02331ca642cd9c435305ddbf949751b8c2617489a4cefe376920b7803d493e61d4fdc41f2f6fe50bf5919ede1295eaab25db71aa6e98bbc80a32d7acc24f9cc9b651cb72d22b17031a1d03fd9166c5f488924689aa4859094b42b72c4bf467a1fdb826289bde90035aff2322c68a34b350b0b3b2818c656701b359cbfdb7eb5665439a4deb2cc95bacc358a693f2d0e31975653665fdc468d627c6eee589bbc46bd019a70e394c90529abe646105623c43956c86bf366e4be1f3560b2e4ca01f1e25432618573a9f257890a435e899724eebd9fd271abefeae2f0a55f3abb4619b9ded206bf70ac3b77622d114309e49bb42d01e8c8678765ab4b80000000000000001000000010000000c444f4d41494e2e4c4f43414c0000000d41646d696e6973747261746f7200000000000000030000000c582d4341434845434f4e463a000000156b7262355f6363616368655f636f6e665f646174610000000770615f74797065000000206b72627467742f444f4d41494e2e4c4f43414c40444f4d41494e2e4c4f43414c0000000000000000000000000000000000000000000000000000000000000000000000000000013200000000") + +with open(TICKETER_TEMPFILE, "rb") as fd: + RESULT = fd.read() + +print(RESULT.hex()) + +assert RESULT == EXPECTED_CCACHE_DATA + += Ticketer++ - Import ticket + +TKT = KRB_Ticket(bytes.fromhex("618204b3308204afa003020105a10e1b0c444f4d41494e2e4c4f43414ca221301fa003020102a11830161b066b72627467741b0c444f4d41494e2e4c4f43414ca38204733082046fa003020112a103020103a28204610482045dbd10c11e1def682dc3607c98db0806acf2809a1f8c73fda44f86c14bd039c4c95a41ed400ac4e558970c51316ffdf34bd695a636bcb1e5074419d083e918085ec56ff77af9f6a410faff3b9859a635184486c83521b5390ec724185057e3e62843a92d9ba500dd24d9ebeff0654fe459cf35d9607b11f7c35bf6ba4dd378fd5c99554650296abcc374c3ff2fcf807038848f351e9134f69726b5e92aec99e4aa99613c35609b0094b533811513e9ba48b9113f0f2b4dbcf9e05a6668c998c09f65ae48c8ea1b7fbc62b5cbbec7decc0a4832df93aec08c138a63621f8c584a8530a380b54b37fdb8dda6924e4260710cf8b66c71479dcb6916790c5c582b9953cab7085178e280d182a74f93fcd3bc83a0dc26284551a4d230a50a8b341de132fdf0f97bb7abdec48021e04c3deda89897c684d5603636bd66842ed4b2586f8e09fbb5e0228bcce3e5ffc82e5674f16a65a4f1b7b17b3854a5465734a5fec573c54526f27b9ea8a64646f01268b040d09f2acda82a37fb195cb24f8c1092919574999fd61d859aed2af5a9457a20a72e6188c0d813cb12713779f84f7bed298e2cd793b06e639d859b4fb3a5f746e2023bcf0627a8a87425899aa3a9b63f558965eccabc35330562b055426e2fc6808c456ee8f047d09a7021b6a4f2547cde6552224b294750efd492ea0745035f76a394d5b6e26442e5542b4d557722ee21b70c05567241ed97dffb31502d950c50462f478fccd8454ec38424688e87c4428c3763b369f1b51509ef36548dcf7a5c842475aa65bec10d6f86cecd90e4694f36d68052b55a2715c00e269c215071311482118ed0168fabb3053ad59dcdf42a42502685cdfcc679d2272dd12ab658ff8588b34cb48b3aef4a1961694ab2b31a812a683015ed343a8c21498997b0ded3767f73e069c9633845b582d6f1a987d6b09d31b330a3cbf2c430fb6f5d6fa27f83d9624b7bb8cebc248933b68dbe1b6b2822b96621159d9249ded893cbedcf1fc5ee77cb69695852170b24ea2f36aa898a24212b2edf84459a4381bd243797b9a3281d7e1b280f6add79dbb1cc5d887178d0813549a168a38be441bb387764098c4e7bed81f7973ee19e733767a4dd05212a18b12c838c674c18b0d6304a28be3de7928ffdd1449d297884c6a6a574b13a0d289425c1ebf37c5af56d04753fcc0c02fdcc98427fb9aa33510905ba2b6746a8b59742e4243f6fba814585b122794a54aecba3ea956a0c85fded2582cb4809ee7be471253f0256503636e81f35df38b177c3c071677e1dd9efa6b10c6a122ab0522f2b10e8b625355f5c1e7996c7055237182691ede31a5e602966f90c2a66bdf997872dbdc97155d723bc1fb187bd0f42cbcdedbe2c5717d13e27e2134ac6cd9d3a53cd215344a8278065da4eea7544860eda5fdb41f849ff7c1db775f7a0a62d2875b43b55bc091e8056666507dfcaded40a83211db7a5856d4c9b5e2ef862830cef8a4c36ce034e9a9e11f558f008cdbe4152081c30dae53b6de44e1703236490cfc87be9e96fa0679f87255069994a262d61d57be0382fe9e570")) + +t = Ticketer() +t.import_krb(TKT, hash=bytes.fromhex("dd4e16dbcfe19d82cb6fc9b593bb7449c1d8a46687dc20c295ed0e51cc4c3d0d")) + +tkt, _, upn, spn = t.export_krb(0) +hexdiff(tkt, TKT) +assert bytes(tkt) == bytes(TKT) +assert upn == 'DC1$@DOMAIN.LOCAL' +assert spn == 'krbtgt/DOMAIN.LOCAL@DOMAIN.LOCAL' + += Ticketer++ - Create keytab + +t = Ticketer() +t.add_cred("host/dc.domain.local", password="Scapy1", salt=b"salt") + +assert t.get_cred("host/dc.domain.local").key == bytes.fromhex("811f44006ad73972ffec42cc89ce6e79749e6effd8db4db5fb0f38c0f3fa6f4f") + += Ticketer++ - Get SPN ssp + +ssp = t.ssp("host/dc.domain.local") + += Ticketer++ - Load keytab + +from scapy.utils import get_temp_file +TICKETER_TEMPFILE = get_temp_file() + +KEYTAB_DATA = bytes.fromhex("0502000000440001000c646f6d61696e2e6c6f63616c000d41646d696e6973747261746f720000000067fd666f0100110010de93a48926de94c2feff6abd8e0e763b00000000000000540001000c646f6d61696e2e6c6f63616c000d41646d696e6973747261746f720000000067fd666f0200120020dcd8ce2bb77dfb6cab0e1afb69a9a5713a8818ed502c3625edc7772e6b4c442a00000000000000440001000c646f6d61696e2e6c6f63616c000d41646d696e6973747261746f720000000067fd666f03001700102b576acbe6bcfda7294d6bd18041b8fe00000000") + +with open(TICKETER_TEMPFILE, "wb") as fd: + fd.write(KEYTAB_DATA) + +t = Ticketer() +t.open_keytab(TICKETER_TEMPFILE) + +assert t.get_cred("Administrator@domain.local", EncryptionType.RC4_HMAC).key == b'+Wj\xcb\xe6\xbc\xfd\xa7)Mk\xd1\x80A\xb8\xfe' +assert t.get_cred("Administrator@domain.local", EncryptionType.AES128_CTS_HMAC_SHA1_96).key == b'\xde\x93\xa4\x89&\xde\x94\xc2\xfe\xffj\xbd\x8e\x0ev;' + += Ticketer++ - Call show() with keytab + +with ContextManagerCaptureOutput() as cmco: + t.show(utc=True) + outp = cmco.get_output().strip() + +# crop first line +outp = outp.split("\n", 1)[1] + +print(repr(outp)) + +assert outp == """ +Principal Timestamp KVNO Keytype +Administrator@domain.local 14/04/25 19:47:59 0 AES128-CTS-HMAC-SHA1-96 +Administrator@domain.local 14/04/25 19:47:59 0 AES256-CTS-HMAC-SHA1-96 +Administrator@domain.local 14/04/25 19:47:59 0 RC4-HMAC + +No tickets in CCache. +""".strip() + += Ticketer++ - Get UPN ssp + +ssp = t.ssp("Administrator@domain.local") + += Ticketer++ - Save keytab + +from scapy.utils import get_temp_file +TICKETER_TEMPFILE = get_temp_file() + +t.save_keytab(TICKETER_TEMPFILE) + ++ Crypto tests + += RFC3691 - Test vectors for KRB-FX-CF2 # https://datatracker.ietf.org/doc/html/rfc6113.html#appendix-A @@ -690,60 +1077,60 @@ from scapy.libs.rfc3961 import Key, EncryptionType, KRB_FX_CF2 def test_krb_fx_cf2(etype): k1 = Key.string_to_key(etype, b"key1", b"key1") k2 = Key.string_to_key(etype, b"key2", b"key2") - return bytes_hex(KRB_FX_CF2(k1, k2, b"a", b"b").key) + return KRB_FX_CF2(k1, k2, b"a", b"b").key.hex() -assert test_krb_fx_cf2(EncryptionType.AES128) == b"97df97e4b798b29eb31ed7280287a92a" -assert test_krb_fx_cf2(EncryptionType.AES256) == b"4d6ca4e629785c1f01baf55e2e548566b9617ae3a96868c337cb93b5e72b1c7b" -assert test_krb_fx_cf2(EncryptionType.RC4) == b'24d7f6b6bae4e5c00d2082c5ebab3672' +assert test_krb_fx_cf2(EncryptionType.AES128_CTS_HMAC_SHA1_96) == "97df97e4b798b29eb31ed7280287a92a" +assert test_krb_fx_cf2(EncryptionType.AES256_CTS_HMAC_SHA1_96) == "4d6ca4e629785c1f01baf55e2e548566b9617ae3a96868c337cb93b5e72b1c7b" +assert test_krb_fx_cf2(EncryptionType.RC4_HMAC) == '24d7f6b6bae4e5c00d2082c5ebab3672' -= Test vectors for _n_fold += RFC3691 - Test vectors for _n_fold from scapy.libs.rfc3961 import _n_fold # https://datatracker.ietf.org/doc/html/rfc3961.html#appendix-A.1 -assert bytes_hex(_n_fold(b"012345", 8)) == b"be072631276b1955" -assert bytes_hex(_n_fold(b"password", 7)) == b"78a07b6caf85fa" -assert bytes_hex(_n_fold(b"Rough Consensus, and Running Code", 8)) == b"bb6ed30870b7f0e0" -assert bytes_hex(_n_fold(b"password", 21)) == b"59e4a8ca7c0385c3c37b3f6d2000247cb6e6bd5b3e" -assert bytes_hex(_n_fold(b"MASSACHVSETTS INSTITVTE OF TECHNOLOGY", 24)) == b"db3b0d8f0b061e603282b308a50841229ad798fab9540c1b" -assert bytes_hex(_n_fold(b"Q", 21)) == b"518a54a215a8452a518a54a215a8452a518a54a215" -assert bytes_hex(_n_fold(b"ba", 21)) ==b"fb25d531ae8974499f52fd92ea9857c4ba24cf297e" +assert _n_fold(b"012345", 8).hex() == "be072631276b1955" +assert _n_fold(b"password", 7).hex() == "78a07b6caf85fa" +assert _n_fold(b"Rough Consensus, and Running Code", 8).hex() == "bb6ed30870b7f0e0" +assert _n_fold(b"password", 21).hex() == "59e4a8ca7c0385c3c37b3f6d2000247cb6e6bd5b3e" +assert _n_fold(b"MASSACHVSETTS INSTITVTE OF TECHNOLOGY", 24).hex() == "db3b0d8f0b061e603282b308a50841229ad798fab9540c1b" +assert _n_fold(b"Q", 21).hex() == "518a54a215a8452a518a54a215a8452a518a54a215" +assert _n_fold(b"ba", 21).hex() == "fb25d531ae8974499f52fd92ea9857c4ba24cf297e" -= Test vectors for mit_des_string_to_key += RFC3691 - Test vectors for mit_des_string_to_key # https://datatracker.ietf.org/doc/html/rfc3961.html#appendix-A.2 from scapy.libs.rfc3961 import Key, EncryptionType def _mit_des_string_to_key(text, salt): - k = Key.string_to_key(EncryptionType.DES_MD5, text, salt) - return bytes_hex(k.key) + k = Key.string_to_key(EncryptionType.DES_CBC_MD5, text, salt) + return k.key.hex() -assert _mit_des_string_to_key(b"password", b"ATHENA.MIT.EDUraeburn") == b"cbc22fae235298e3" -assert _mit_des_string_to_key(b"potatoe", b"WHITEHOUSE.GOVdanny") == b"df3d32a74fd92a01" -assert _mit_des_string_to_key((u"\U0001d11e").encode(), b"EXAMPLE.COMpianist") == b"4ffb26bab0cd9413" -assert _mit_des_string_to_key((u"\xdf").encode(), (u"ATHENA.MIT.EDUJuri\u0161i\u0107").encode()) == b"62c81a5232b5e69d" -assert _mit_des_string_to_key(b"11119999", b"AAAAAAAA") == b"984054d0f1a73e31" -assert _mit_des_string_to_key(b"NNNN6666", b"FFFFAAAA") == b"c4bf6b25adf7a4f8" +assert _mit_des_string_to_key(b"password", b"ATHENA.MIT.EDUraeburn") == "cbc22fae235298e3" +assert _mit_des_string_to_key(b"potatoe", b"WHITEHOUSE.GOVdanny") == "df3d32a74fd92a01" +assert _mit_des_string_to_key((u"\U0001d11e").encode(), b"EXAMPLE.COMpianist") == "4ffb26bab0cd9413" +assert _mit_des_string_to_key((u"\xdf").encode(), (u"ATHENA.MIT.EDUJuri\u0161i\u0107").encode()) == "62c81a5232b5e69d" +assert _mit_des_string_to_key(b"11119999", b"AAAAAAAA") == "984054d0f1a73e31" +assert _mit_des_string_to_key(b"NNNN6666", b"FFFFAAAA") == "c4bf6b25adf7a4f8" -= Test vectors for DES3 += RFC3691 - Test vectors for DES3 # https://datatracker.ietf.org/doc/html/rfc3961.html#appendix-A.4 def _des3_string_to_key(text, salt): - k = Key.string_to_key(EncryptionType.DES3, text, salt) - return bytes_hex(k.key) + k = Key.string_to_key(EncryptionType.DES3_CBC_SHA1_KD, text, salt) + return k.key.hex() -assert _des3_string_to_key(b"password", b"ATHENA.MIT.EDUraeburn") == b"850bb51358548cd05e86768c313e3bfef7511937dcf72c3e" -assert _des3_string_to_key(b"potatoe", b"WHITEHOUSE.GOVdanny") == b"dfcd233dd0a43204ea6dc437fb15e061b02979c1f74f377a" -assert _des3_string_to_key(b"penny", b"EXAMPLE.COMbuckaroo") == b"6d2fcdf2d6fbbc3ddcadb5da5710a23489b0d3b69d5d9d4a" -assert _des3_string_to_key((u"\xdf").encode(), (u"ATHENA.MIT.EDUJuri\u0161i\u0107").encode()) == b"16d5a40e1ce3bacb61b9dce00470324c831973a7b952feb0" -assert _des3_string_to_key((u"\U0001d11e").encode(), b"EXAMPLE.COMpianist") == b"85763726585dbc1cce6ec43e1f751f07f1c4cbb098f40b19" +assert _des3_string_to_key(b"password", b"ATHENA.MIT.EDUraeburn") == "850bb51358548cd05e86768c313e3bfef7511937dcf72c3e" +assert _des3_string_to_key(b"potatoe", b"WHITEHOUSE.GOVdanny") == "dfcd233dd0a43204ea6dc437fb15e061b02979c1f74f377a" +assert _des3_string_to_key(b"penny", b"EXAMPLE.COMbuckaroo") == "6d2fcdf2d6fbbc3ddcadb5da5710a23489b0d3b69d5d9d4a" +assert _des3_string_to_key((u"\xdf").encode(), (u"ATHENA.MIT.EDUJuri\u0161i\u0107").encode()) == "16d5a40e1ce3bacb61b9dce00470324c831973a7b952feb0" +assert _des3_string_to_key((u"\U0001d11e").encode(), b"EXAMPLE.COMpianist") == "85763726585dbc1cce6ec43e1f751f07f1c4cbb098f40b19" -= Test vectors for AES += RFC3692 - Test vectors for AES from scapy.libs.rfc3961 import Key, EncryptionType @@ -753,16 +1140,170 @@ from scapy.libs.rfc3961 import Key, EncryptionType # Pass phrase = "password" # Salt = "ATHENA.MIT.EDUraeburn" -k = Key.string_to_key(EncryptionType.AES128, b"password", b"ATHENA.MIT.EDUraeburn", struct.pack(">L", 1200)) -assert bytes_hex(k.key) == b"4c01cd46d632d01e6dbe230a01ed642a" +k = Key.string_to_key(EncryptionType.AES128_CTS_HMAC_SHA1_96, b"password", b"ATHENA.MIT.EDUraeburn", struct.pack(">L", 1200)) +assert k.key.hex() == "4c01cd46d632d01e6dbe230a01ed642a" # Iteration count = 1200 # Pass phrase = (65 characters) # "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # Salt = "pass phrase exceeds block size" -k = Key.string_to_key(EncryptionType.AES256, b"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", b"pass phrase exceeds block size", struct.pack(">L", 1200)) -assert bytes_hex(k.key) == b"d78c5c9cb872a8c9dad4697f0bb5b2d21496c82beb2caeda2112fceea057401b" +k = Key.string_to_key(EncryptionType.AES256_CTS_HMAC_SHA1_96, b"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", b"pass phrase exceeds block size", struct.pack(">L", 1200)) +assert k.key.hex() == "d78c5c9cb872a8c9dad4697f0bb5b2d21496c82beb2caeda2112fceea057401b" + += RFC8009 - Test vectors for AES-CTS HMAC-SHA2 - Sample results for string-to-key conversion + +from scapy.libs.rfc3961 import Key, EncryptionType + +# https://datatracker.ietf.org/doc/html/rfc8009#appendix-A + +# Iteration count = 32768 +# Pass phrase = "password" +# Salt = 10df9dd783e5bc8acea1730e74355f61 + "ATHENA.MIT.EDUraeburn" + +k = Key.string_to_key(EncryptionType.AES128_CTS_HMAC_SHA256_128, b"password", b"\x10\xdf\x9d\xd7\x83\xe5\xbc\x8a\xce\xa1s\x0et5_aATHENA.MIT.EDUraeburn") +assert k.key.hex() == '089bca48b105ea6ea77ca5d2f39dc5e7' + +# Iteration count = 32768 +# Pass phrase = "password" +# Salt = 10df9dd783e5bc8acea1730e74355f61 + "ATHENA.MIT.EDUraeburn" + +k = Key.string_to_key(EncryptionType.AES256_CTS_HMAC_SHA384_192, b"password", b"\x10\xdf\x9d\xd7\x83\xe5\xbc\x8a\xce\xa1s\x0et5_aATHENA.MIT.EDUraeburn") +assert k.key.hex() == '45bd806dbf6a833a9cffc1c94589a222367a79bc21c413718906e9f578a78467' + += RFC8009 - Test vectors for AES-CTS HMAC-SHA2 - Sample results for key derivation + +# enctype aes128-cts-hmac-sha256-128: +# 128-bit base-key: 3705D96080C17728A0E800EAB6E0D23C + +from scapy.libs.rfc3961 import _AES128CTS_SHA256_128 + +k = Key(EncryptionType.AES128_CTS_HMAC_SHA256_128, key=bytes.fromhex("3705D96080C17728A0E800EAB6E0D23C")) + +# Kc value for key usage 2 (label = 0x0000000299): +kc = _AES128CTS_SHA256_128.derive(k, struct.pack(">IB", 2, 0x99), 128) +assert kc.hex() == 'b31a018a48f54776f403e9a396325dc3' + +# Ke value for key usage 2 (label = 0x00000002AA): +ke = _AES128CTS_SHA256_128.derive(k, struct.pack(">IB", 2, 0xAA), 128) +assert ke.hex() == '9b197dd1e8c5609d6e67c3e37c62c72e' + +# Ki value for key usage 2 (label = 0x0000000255): +ki = _AES128CTS_SHA256_128.derive(k, struct.pack(">IB", 2, 0x55), 128) +assert ki.hex() == '9fda0e56ab2d85e1569a688696c26a6c' + +# enctype aes256-cts-hmac-sha384-192: +# 256-bit base-key: 6D404D37FAF79F9DF0D33568D320669800EB4836472EA8A026D16B7182460C52 + +from scapy.libs.rfc3961 import _AES256CTS_SHA384_192 + +k = Key(EncryptionType.AES256_CTS_HMAC_SHA384_192, key=bytes.fromhex("6D404D37FAF79F9DF0D33568D320669800EB4836472EA8A026D16B7182460C52")) + +# Kc value for key usage 2 (label = 0x0000000299): +kc = _AES256CTS_SHA384_192.derive(k, struct.pack(">IB", 2, 0x99), 192) +assert kc.hex() == 'ef5718be86cc84963d8bbb5031e9f5c4ba41f28faf69e73d' + +# Ke value for key usage 2 (label = 0x00000002AA): +ke = _AES256CTS_SHA384_192.derive(k, struct.pack(">IB", 2, 0xAA), 256) +assert ke.hex() == '56ab22bee63d82d7bc5227f6773f8ea7a5eb1c825160c38312980c442e5c7e49' + +# Ki value for key usage 2 (label = 0x0000000255): +ki = _AES256CTS_SHA384_192.derive(k, struct.pack(">IB", 2, 0x55), 192) +assert ki.hex() == '69b16514e3cd8e56b82010d5c73012b622c4d00ffc23ed1f' + += RFC8009 - Test vectors for AES-CTS HMAC-SHA2 - Sample encryptions and decryptions + +# enctype aes128-cts-hmac-sha256-128: + +k = Key(EncryptionType.AES128_CTS_HMAC_SHA256_128, key=bytes.fromhex("3705D96080C17728A0E800EAB6E0D23C")) + +# Plaintext: (empty) +# Confounder: 7E5895EAF2672435BAD817F545A37148 + +c = k.encrypt(2, b"", confounder=bytes.fromhex("7E5895EAF2672435BAD817F545A37148")) +assert c.hex() == "ef85fb890bb8472f4dab20394dca781dad877eda39d50c870c0d5a0a8e48c718" +assert k.decrypt(2, c) == b"" + +# Plaintext: 000102030405 +# Confounder: 7BCA285E2FD4130FB55B1A5C83BC5B24 + +c = k.encrypt(2, bytes.fromhex("000102030405"), confounder=bytes.fromhex("7BCA285E2FD4130FB55B1A5C83BC5B24")) +assert c.hex() == "84d7f30754ed987bab0bf3506beb09cfb55402cef7e6877ce99e247e52d16ed4421dfdf8976c" +assert k.decrypt(2, c).hex() == "000102030405".lower() + +# Plaintext: 000102030405060708090A0B0C0D0E0F +# Confounder: 56AB21713FF62C0A1457200F6FA9948F + +c = k.encrypt(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F"), confounder=bytes.fromhex("56AB21713FF62C0A1457200F6FA9948F")) +assert c.hex() == "3517d640f50ddc8ad3628722b3569d2ae07493fa8263254080ea65c1008e8fc295fb4852e7d83e1e7c48c37eebe6b0d3" +assert k.decrypt(2, c).hex() == "000102030405060708090A0B0C0D0E0F".lower() + +# Plaintext: 000102030405060708090A0B0C0D0E0F1011121314 +# Confounder: A7A4E29A4728CE10664FB64E49AD3FAC + +c = k.encrypt(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F1011121314"), confounder=bytes.fromhex("A7A4E29A4728CE10664FB64E49AD3FAC")) +assert c.hex() == "720f73b18d9859cd6ccb4346115cd336c70f58edc0c4437c5573544c31c813bce1e6d072c186b39a413c2f92ca9b8334a287ffcbfc" +assert k.decrypt(2, c).hex() == "000102030405060708090A0B0C0D0E0F1011121314".lower() + +# aes256-cts-hmac-sha384-192: + +k = Key(EncryptionType.AES256_CTS_HMAC_SHA384_192, key=bytes.fromhex("6D404D37FAF79F9DF0D33568D320669800EB4836472EA8A026D16B7182460C52")) + +# Plaintext: (empty) +# Confounder: F764E9FA15C276478B2C7D0C4E5F58E4 + +c = k.encrypt(2, b"", confounder=bytes.fromhex("F764E9FA15C276478B2C7D0C4E5F58E4")) +assert c.hex() == "41f53fa5bfe7026d91faf9be959195a058707273a96a40f0a01960621ac612748b9bbfbe7eb4ce3c" +assert k.decrypt(2, c) == b"" + +# Plaintext: 000102030405 +# Confounder: B80D3251C1F6471494256FFE712D0B9A + +c = k.encrypt(2, bytes.fromhex("000102030405"), confounder=bytes.fromhex("B80D3251C1F6471494256FFE712D0B9A")) +assert c.hex() == "4ed7b37c2bcac8f74f23c1cf07e62bc7b75fb3f637b9f559c7f664f69eab7b6092237526ea0d1f61cb20d69d10f2" +assert k.decrypt(2, c).hex() == "000102030405".lower() + +# Plaintext: 000102030405060708090A0B0C0D0E0F +# Confounder: 53BF8A0D105265D4E276428624CE5E63 + +c = k.encrypt(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F"), confounder=bytes.fromhex("53BF8A0D105265D4E276428624CE5E63")) +assert c.hex() == "bc47ffec7998eb91e8115cf8d19dac4bbbe2e163e87dd37f49beca92027764f68cf51f14d798c2273f35df574d1f932e40c4ff255b36a266" +assert k.decrypt(2, c).hex() == "000102030405060708090A0B0C0D0E0F".lower() + +# Plaintext: 000102030405060708090A0B0C0D0E0F1011121314 +# Confounder: 763E65367E864F02F55153C7E3B58AF1 + +c = k.encrypt(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F1011121314"), confounder=bytes.fromhex("763E65367E864F02F55153C7E3B58AF1")) +assert c.hex() == "40013e2df58e8751957d2878bcd2d6fe101ccfd556cb1eae79db3c3ee86429f2b2a602ac86fef6ecb647d6295fae077a1feb517508d2c16b4192e01f62" +assert k.decrypt(2, c).hex() == "000102030405060708090A0B0C0D0E0F1011121314".lower() + += RFC8009 - Test vectors for AES-CTS HMAC-SHA2 - Sample checksums + +# Checksum type: hmac-sha256-128-aes128 + +k = Key(EncryptionType.AES128_CTS_HMAC_SHA256_128, key=bytes.fromhex("3705D96080C17728A0E800EAB6E0D23C")) +cksum = k.make_checksum(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F1011121314")) +assert cksum.hex() == "d78367186643d67b411cba9139fc1dee" + +# Checksum type: hmac-sha384-192-aes256 + +k = Key(EncryptionType.AES256_CTS_HMAC_SHA384_192, key=bytes.fromhex("6D404D37FAF79F9DF0D33568D320669800EB4836472EA8A026D16B7182460C52")) +cksum = k.make_checksum(2, bytes.fromhex("000102030405060708090A0B0C0D0E0F1011121314")) +assert cksum.hex() == "45ee791567eefca37f4ac1e0222de80d43c3bfa06699672a" + += RFC8009 - Test vectors for AES-CTS HMAC-SHA2 - Sample pseudorandom function (PRF) invocations + +# enctype aes128-cts-hmac-sha256-128: + +k = Key(EncryptionType.AES128_CTS_HMAC_SHA256_128, key=bytes.fromhex("3705D96080C17728A0E800EAB6E0D23C")) +out = k.prf(b"test") +assert out.hex() == "9d188616f63852fe86915bb840b4a886ff3e6bb0f819b49b893393d393854295" + +# enctype aes256-cts-hmac-sha384-192: + +k = Key(EncryptionType.AES256_CTS_HMAC_SHA384_192, key=bytes.fromhex("6D404D37FAF79F9DF0D33568D320669800EB4836472EA8A026D16B7182460C52")) +out = k.prf(b"test") +assert out.hex() == "9801f69a368c2bf675e59521e177d9a07f67efe1cfde8d3c8d6f6a0256e3b17db3c1b62ad1b8553360d17367eb1514d2" = Decrypt PA-ENC-TIMESTAMP @@ -770,8 +1311,479 @@ from scapy.libs.rfc3961 import Key, EncryptionType 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 ") enc = pkt[Kerberos].root.padata[0].padataValue -k = Key(enc.etype.val, key=hex_bytes("7fada4e566ae4fb270e2800a23ae87127a819d42e69b5e22de0ddc63da80096d")) +k = Key(enc.etype.val, key=bytes.fromhex("7fada4e566ae4fb270e2800a23ae87127a819d42e69b5e22de0ddc63da80096d")) ts = enc.decrypt(k) assert ts.patimestamp == "20220715171847Z" ts.pausec == 0x9a4db + ++ [MS-KILE] test vectors +~ mock + += [MS-KILE] RC4 GSS_WrapEx (RFC4757) test vectors (sect 4.5) + +from unittest import mock +from scapy.libs.rfc3961 import Key, EncryptionType + +ssp = KerberosSSP() +ctx = KerberosSSP.CONTEXT(IsAcceptor=False, req_flags=GSS_C_FLAGS.GSS_C_CONF_FLAG) + +ctx.KrbSessionKey = Key(EncryptionType.RC4_HMAC, key=bytes.fromhex("81a2cb90af7fc2d19554a150d8185359")) +ctx.SendSeqNum = 0x60cbacd3 +Confounder = bytes.fromhex("5256f3fb630cf12a") + +with mock.patch('scapy.layers.kerberos.os.urandom', side_effect=lambda x: Confounder): + _msgs, sig = ssp.GSS_WrapEx( + ctx, + [ + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=bytes.fromhex("112233445566778899aabbccddeeff")), + ], + ) + +assert isinstance(sig, KRB_GSSAPI_Token) +assert sig.innerToken.TOK_ID == b"\x02\x01" +assert sig.innerToken.root.SGN_ALG == 0x11 +assert sig.innerToken.root.SEAL_ALG == 0x10 + +assert bytes(sig) == b'`+\x06\t*\x86H\x86\xf7\x12\x01\x02\x02\x02\x01\x11\x00\x10\x00\xff\xff\xe2\x9e\x8b\xbccH\xe7@\xeb\xaaa\x92D\xa1V\xa1;\\\xf6^\x03\x03\xae\x18n\x1aQ\r\x7fP\xdb\xfe\xe9\xeb\xab2\x9dws9\xf5\xcb\x94\xab\xc1\x9e\xbd\x08\x0f\xfcx\x18\x1b\xf8\x1f\xf2\'\x18-\xe4"\x93vuTf3\xbdj\xb6\x88%\x8a\x94\xd12\xfbY\x0f\x81R\xd3\xf1\x9b\xd5Z\x1f3o\xb7\xc3\x82\x14\t\x87\xac#\x89\x13M\x803\x88/\x92==S$\xa3\xe9\xf5C{\xd7\x0f\t^k\xb0\x0e\xe6\x8d\x8f!\x91+\x19\xb2y$\xc6\x1bN;\xfehA\x1f\x9f"\r\xe8\xda\xce\x00\xe7g\xb6b17\x06s\rM\xc8S\x9b0\x9f\xc7^l\xa4\xca\xe4p\xcd\xf1,\xc3\xcf\xb1\x91Hn>^\xb8\xc8\x07#\xb2\xb0G;\x07\xe4\xeaM8T\x87\xdd0=\xf2\xdb\x8d1\xf8\xc9\rS\xc4\xad\xcf9\xadx\xcfl\x85\xfb\xb8{LN\xe51\xa4,!3\xdf+\x03b\x13#t\xdf\x99T \xe4\xb2\xa6\xd1\xe1\x9dxy\xd5\x18e-Q\x01\xa3\x16\x96+\'\xb3\x88L\xb6}\x07W/\x96\xb9f\x8c\xa4,\xcas\x11\xa7\x15*\xc7\xc6\xd4\x92\x00\x91\x92\xfaJpy\x89\xe4;*\x10\xf1\x9eS^|\xf8\xaf\xda\xf6<\xe9\xa2\xa8\\\xe1\xbd\x17\xd8\x1c\xfev\xd2\xceWY\xa7\xfd\xbe\xffo\xb2y\xb8b\x0b\xc2\xc5\x18;$\xbe\x83\x1c~\xe1W\x11O\'\x00\xda!\x0b6\xed\xb7\xbd\xa7\xd9\x1a2\xf7\x94\x0b\xefC\x1cvW\x1c\xd4D\x99\xf7y\xccN\xbe\x82\x9f\xb3N\xea\xa1\xe4B$\rYb\xbd\xbc\xbc\x16\xc9b\x97KTn\x9c\xee8\r\xdaI\xf6Q\xac\xc5\xc5\x8a\xca\xe4\xad\x06\xd5~K\x91\xd8\xc5Use\xe8\xdd\xda~\xe9U\tc\xd7\rOV\xb4O\xc5\xa2n)\xb3l\xb2\x1d\x11"\x18%\xb5\xa2!|\xb1\xf1EM4\xd9J\x85\\\xb8`\xf2\xfeCh\x1e=0.~\x12Bs\xdd\x18\xb0O\xdd\xf6`\xb8\x85\x8e\x1ex\xd0"\xcc\x03\xf4g\xf3\xcf\x1an]\xf5;\xb81yEB\xb1\xd0\x8e8\xd3\xbf\xb0\xbf.[\xa6\xf7Z\x0fw\xd5k\xf2\x92K\x14O\xff<\x87\xeczW\xbf\xf3E\xee\x8aD\x96gm8\xc9E<8\xe6E!\xdb-\xe6\xd6E*\xa8\xf3\xda\x16u\x13N\x8d\x90\xcb\xb0\xd2t\xcea\x89V?\xd9\xa5nV\xa8\x00f\x1ex{\x089Pb05\xdd\xee\xb2\xfb\x84\xf6\xfb%\x07\xf2\xc1W\xe7N\x81\xa8\x19p\xe1\x14u\xce\x92n9:U\xb0kw\xc4D\xdb\xd26\x88\xe8\xa7|\x7f03xt\xfe\xf7\x87\xa1\x87\xfc\xaf\xd7:ZH7\xc8\xe3\xe6\x07\x120\x85\x97\xffr\xea.\xda\xe6\x9c\x94\x02\xadz\xe8\x1a\xbb>\x91\x00\xf0\xc8{\x99\xb2VBF\xbdV\xaf\x8em\x0e\xcf)(\xe5\x15\x12\x18\xf7\xe6\'\xc5e\xe1U@foO|\x0e\x93|-\x0e\x84x/\xcb\x1bS^YolN\n\xed|\x1d5\x0e\x16\x9d\x04_.\xaa\xa4\xbb/\x94\xcd\x14\x95v\xf85\xe5\xee\xcbD\x18g}\x04D\xe5\x1f\xaf\xcb\xed*\xfa\xc5\x0b\x1d2\x0b\xc2#\xd2b6\x01\xae\xe6\xdfj6:$)K\xfb;\x00\xf2f\x8d\xfc@N\x9f\xa1\x7f\xe96\xe6b\x07V\xa6\x91\x8f}\xe2\xde4?8\x0f\xab\x83\xfd\xe9\x11\x12K\xe5\x08\xa4\x82\x01\\0\x82\x01X\xa0\x03\x02\x01\x12\xa2\x82\x01O\x04\x82\x01K\\>\t\xe4\x1d8,a(\x7f\x1e\xd2\x8dHH\x9c\xa3\x03?&\xb9\xf4\xba\xef\xcf\xcf\xb6(8\x91\x0f\xa3lq\xc6 f&Ou\xd8Bk\xe84s\xf1\xec\xf6\x97wY\xc6Un;\xf5\xdeh\xb9J\xd6\xaf\xf4r\x00\x80\x17\x8d\xc4p\x81\xac\x89\xf1\xf6\x98\xef\x1f\xb3\xe5\x91}\xf5m\x1a\xbd\x08\x1d\x0217W0\x81\xddZ\xec,J%\xe2o\x86\xef{"a\xe0\xe2hBc\xeb^\x8b\xa3\x8c\xf7W\xf9F\xc6&\x1a\x041\x0c\xdf\xc3S\xaa>\x04\x90\xd7\x8a\xdd\xf3j\x80#4_\x95u\xaby3\x0f\x878\xe3\',t\xa7\xe9\xba7&\xd6\x82y\x1d9\x06\xf1\xff\xaf\xb33O\xdb\x00\xc5\x19\xd0\xb7\t\xe9\xeb\xe0iv\x08\xaa\xf4\x00\xcaG\xbb7\xb9P\xcd\xcf\xcbC\x9b\xec\xfdH\x1b\xbf\x89\x11\x96L\xa8\xb4\\6\xcf\x9a\xa6\x16\xf0\xfb,\xaf\x06.qj\xf0\x03\xfd\xc0 \x80\xb6\xb84\xcf\xec\tW~5\xad,\x14-\xf05\x04\xb2\xd4[o\xce\xa3\xf9\x06\x08\x0e\xeb\x1e\xbf2\xd7\xe4\xc2\x14\xabn_\x0c8j;#\r\xee\xce\xa6\x1f\xc3+\xed\x0c\xb7\xabdb\xb4\x8b\xb2\xd0\xe97\xa5P\xcd\xf1\x96\x8aT:=\xfc\xd9\x1e\xb6q\xcdM\x16\xead\x81\x84/\xab\xdd\xc8\xe1\xed\x17\xa3\xf5\x1c\xf1\x98\xf1\xf7\xbd\xbc\xc8\xdf' += GSS_Accept_sec_context (SPNEGO_negTokenResp: KRB_AP_REQ->KRB_AP_REP) + +with KrbRandomPatcher(): + srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + +assert negResult == 0 +assert isinstance(tok, SPNEGO_negToken) +tok = SPNEGO_negToken(bytes(tok)) +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert tok.token.negResult == 0 +assert tok.token.supportedMech.oid == '1.2.840.48018.1.2.2' +assert isinstance(tok.token.responseToken, SPNEGO_Token) +assert tok.token.mechListMIC is not None + +ap_rep = tok.token.responseToken.value.root +assert isinstance(ap_rep, KRB_AP_REP) + +apreppart = ap_rep.encPart.decrypt(clicontext.ssp.KEY) +assert apreppart.ctime == "20240305165255Z" +assert apreppart.subkey.keyvalue == b"0000000000000000" +assert apreppart.subkey.keytype == 17 + +# Hardcode (yes this will probably require updating this test) +bytes(tok) +assert bytes(tok) == b'\xa1\x81\xa90\x81\xa6\xa0\x03\n\x01\x00\xa1\x0b\x06\t*\x86H\x82\xf7\x12\x01\x02\x02\xa2r\x04pon0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM\xa3\x1e\x04\x1c\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00\x17F\x8al\x01c\x00\xcf4\x12oI' + += GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->OK) + +with KrbRandomPatcher(): + clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + +assert tok is None +assert negResult == 0 +assert clicontext.KrbSessionKey.key == srvcontext.KrbSessionKey.key +assert srvcontext.KrbSessionKey.key == b'0000000000000000' + += GSS_GetMICEx/GSS_VerifyMICEx: client sends a signed payload + +data_header = b"header" # signed but not encrypted +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" # encrypted + +sig = client.GSS_GetMICEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +assert isinstance(sig, KRB_InnerToken) and sig.TOK_ID == b"\x04\x04" +assert sig.root.SND_SEQ == 0x7FFFFFFF//2 + 1 +assert bytes(sig) == b'\x04\x04\x04\xff\xff\xff\xff\xff\x00\x00\x00\x00@\x00\x00\x00\xfc\xc6\x86\xab\x85e\x18\xe8\x7f\xa81t' +server.GSS_VerifyMICEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data), + ], + sig +) + += GSS_GetMICEx/GSS_VerifyMICEx: server answers back + +sig = server.GSS_GetMICEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +assert isinstance(sig, KRB_InnerToken) and sig.TOK_ID == b"\x04\x04" +assert sig.root.SND_SEQ == 1 +assert bytes(sig) == b'\x04\x04\x05\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00\x00\x01G\x81\x93\xb9\x92\xd0NvHH\xf6\x9c' +client.GSS_VerifyMICEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data), + ], + sig +) + += GSS_GetMICEx/GSS_VerifyMICEx: inject fault + +sig = client.GSS_GetMICEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +bad_data_header = data_header[:-3] + b"hey" +try: + server.GSS_VerifyMICEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=bad_data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data), + ], + sig + ) + assert False, "No error was reported, but there should have been one" +except ValueError: + pass + += Create client and server KerberosSSP (raw) + +client = KerberosSSP( + UPN="User1@DOMAIN.LOCAL", + SPN="cifs/dc1", + ST=KRB_Ticket(bytes.fromhex("618204a13082049da003020105a10e1b0c444f4d41494e2e4c4f43414ca2163014a003020103a10d300b1b04636966731b03646331a382046c30820468a003020112a10302010da282045a04820456671f6131b38ee6e682d62cb937b8b79c589753182f8dbcb14a91b031052a3c20f7b4c89bf9a41fe9960d112acc73f6bd6527dfe70700a3d3c2e72b4ba6705dfc040fd56f9d7cd60b580ebecec2bfb240baac619690dbd9301ed98cac037cfdff8ff96ac98358969f3532f9c6adc076d136a0ef96ebddef293df879bb42adfbf7670434f340ad673e0303ae186e1a510d7f50dbfee9ebab323c715d6b27a67ffec60dba9f7475e5dbf88eee1fcc95b7d467ab2b4ecef893a92a25c80b8480ac8c12bc10741523a2738a3d7c3d2c438235111188968486cab2934b32cad1b6b4b2cbf343b25d41ad463c0513cf21cf9f77f072f4a49d8042947064e3375a1ae76c355fd48d5fc163cf7f865af91bcb788cffe2e9e1a30a7e3f91be8fb55b0a8b8c0b600ef3e0e88feaad4fbf4fffe76c9302ee2acfa3b64ca28cd006fd4af9c27d2eb45e47e582b87e632aa23475caeb0e3e9d777339f5cb94abc19ebd080ffc78181bf81ff227182de422937675546633bd6ab688258a94d132fb590f8152d3f19bd55a1f336fb7c382140987ac2389134d8033882f923d3d5324a3e9f5437bd70f095e6bb00ee68d8f21912b19b27924c61b4e3bfe68411f9f220de8dace00e767b662313706730d4dc8539b309fc75e6ca4cae470cdf12cc3cfb191486e3e5eb8c80723b2b0473b07e4ea4d385487dd303df2db8d31f8c90d53c4adcf39ad78cf6c85fbb87b4c4ee531a42c2133df2b0362132374df995420e4b2a6d1e19d7879d518652d5101a316962b27b3884cb67d07572f96b9668ca42cca7311a7152ac7c6d492009192fa4a707989e43b2a10f19e535e7cf8afdaf63ce9a2a85ce1bd17d81cfe76d2ce5759a7fdbeff6fb279b8620bc2c5183b24be831c7ee157114f2700da210b36edb7bda7d91a32f7940bef431c76571cd44499f779cc4ebe829fb34eeaa1e442240d5962bdbcbc16c962974b546e9cee380dda49f651acc5c58acae4ad06d57e4b91d8c5557365e8ddda7ee9550963d70d4f56b44fc5a26e29b36cb21d11221825b5a2217cb1f1454d34d94a855cb860f2fe43681e3d302e7e124273dd18b04fddf660b8858e1e78d022cc03f467f3cf1a6e5df53bb831794542b1d08e38d3bfb0bf2e5ba6f75a0f77d56bf2924b144fff3c87ec7a57bff345ee8a4496676d38c9453c38e64521db2de6d6452aa8f3da1675134e8d90cbb0d274ce6189563fd9a56e56a800661e787b083950623035ddeeb2fb84f6fb2507f2c157e74e81a81970e11475ce926e393a55b06b77c444dbd23688e8a77c7f30337874fef787a187fcafd73a5a4837c8e3e60712308597ff72ea2edae69c9402ad7ae81abb3e9100f0c87b99b2564246bd56af8e6d0ecf2928e5151218f7e627c565e15540666f4f7c0e937c2d0e84782fcb1b535e596f6c4e0aed7c1d350e169d045f2eaaa4bb2f94cd149576f835e5eecb4418677d0444e51fafcbed2afac50b1d320bc223d2623601aee6df6a363a24294bfb3b00f2668dfc404e9fa17fe936e6620756a6918f7de2de343f380fab83fde911124be508")), + KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("4aad1c4c7b5bf02bfd061cfaebf0188d6c4f4642d569ca4ab536cb68adcb0e68")), +) +server = KerberosSSP( + SPN="cifs/dc1", + PASSWORD="Password1", + KEY=Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("133614b285c1d76d4ec78d642e9c6f7451d7652cf6c5fe635af6e89050d42517")), +) + += GSS_Init_sec_context (KRB_AP_REQ) - DCE_STYLE + +with KrbRandomPatcher(): + clicontext, tok, negResult = client.GSS_Init_sec_context( + None, + req_flags=( + 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 + ) + ) + +assert negResult == 1 +assert isinstance(tok, KRB_AP_REQ) +ap_req = KRB_AP_REQ(bytes(tok)) +assert isinstance(ap_req, KRB_AP_REQ) +assert ap_req.apOptions == "001" +assert ap_req.ticket == client.ST + +auth = ap_req.authenticator.decrypt(client.KEY) +assert auth.cksum.cksumtype == 0x8003 +assert auth.cksum.checksum.Flags == ( + 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 +) +assert auth.cksum.checksum.Exts[0].sprintf("%type%") == 'GSS_EXTS_CHANNEL_BINDING' + +# Hardcode (yes this will probably require updating this test) +bytes(tok) +assert bytes(tok) == b'n\x82\x06\x1d0\x82\x06\x19\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0e\xa2\x04\x03\x02\x05 \xa3\x82\x04\xa5a\x82\x04\xa10\x82\x04\x9d\xa0\x03\x02\x01\x05\xa1\x0e\x1b\x0cDOMAIN.LOCAL\xa2\x160\x14\xa0\x03\x02\x01\x03\xa1\r0\x0b\x1b\x04cifs\x1b\x03dc1\xa3\x82\x04l0\x82\x04h\xa0\x03\x02\x01\x12\xa1\x03\x02\x01\r\xa2\x82\x04Z\x04\x82\x04Vg\x1fa1\xb3\x8e\xe6\xe6\x82\xd6,\xb97\xb8\xb7\x9cX\x97S\x18/\x8d\xbc\xb1J\x91\xb01\x05*< \xf7\xb4\xc8\x9b\xf9\xa4\x1f\xe9\x96\r\x11*\xccs\xf6\xbde\'\xdf\xe7\x07\x00\xa3\xd3\xc2\xe7+K\xa6p]\xfc\x04\x0f\xd5o\x9d|\xd6\x0bX\x0e\xbe\xce\xc2\xbf\xb2@\xba\xaca\x96\x90\xdb\xd90\x1e\xd9\x8c\xac\x03|\xfd\xff\x8f\xf9j\xc9\x83X\x96\x9f52\xf9\xc6\xad\xc0v\xd16\xa0\xef\x96\xeb\xdd\xef)=\xf8y\xbbB\xad\xfb\xf7g\x044\xf3@\xadg>\x03\x03\xae\x18n\x1aQ\r\x7fP\xdb\xfe\xe9\xeb\xab2\x9dws9\xf5\xcb\x94\xab\xc1\x9e\xbd\x08\x0f\xfcx\x18\x1b\xf8\x1f\xf2\'\x18-\xe4"\x93vuTf3\xbdj\xb6\x88%\x8a\x94\xd12\xfbY\x0f\x81R\xd3\xf1\x9b\xd5Z\x1f3o\xb7\xc3\x82\x14\t\x87\xac#\x89\x13M\x803\x88/\x92==S$\xa3\xe9\xf5C{\xd7\x0f\t^k\xb0\x0e\xe6\x8d\x8f!\x91+\x19\xb2y$\xc6\x1bN;\xfehA\x1f\x9f"\r\xe8\xda\xce\x00\xe7g\xb6b17\x06s\rM\xc8S\x9b0\x9f\xc7^l\xa4\xca\xe4p\xcd\xf1,\xc3\xcf\xb1\x91Hn>^\xb8\xc8\x07#\xb2\xb0G;\x07\xe4\xeaM8T\x87\xdd0=\xf2\xdb\x8d1\xf8\xc9\rS\xc4\xad\xcf9\xadx\xcfl\x85\xfb\xb8{LN\xe51\xa4,!3\xdf+\x03b\x13#t\xdf\x99T \xe4\xb2\xa6\xd1\xe1\x9dxy\xd5\x18e-Q\x01\xa3\x16\x96+\'\xb3\x88L\xb6}\x07W/\x96\xb9f\x8c\xa4,\xcas\x11\xa7\x15*\xc7\xc6\xd4\x92\x00\x91\x92\xfaJpy\x89\xe4;*\x10\xf1\x9eS^|\xf8\xaf\xda\xf6<\xe9\xa2\xa8\\\xe1\xbd\x17\xd8\x1c\xfev\xd2\xceWY\xa7\xfd\xbe\xffo\xb2y\xb8b\x0b\xc2\xc5\x18;$\xbe\x83\x1c~\xe1W\x11O\'\x00\xda!\x0b6\xed\xb7\xbd\xa7\xd9\x1a2\xf7\x94\x0b\xefC\x1cvW\x1c\xd4D\x99\xf7y\xccN\xbe\x82\x9f\xb3N\xea\xa1\xe4B$\rYb\xbd\xbc\xbc\x16\xc9b\x97KTn\x9c\xee8\r\xdaI\xf6Q\xac\xc5\xc5\x8a\xca\xe4\xad\x06\xd5~K\x91\xd8\xc5Use\xe8\xdd\xda~\xe9U\tc\xd7\rOV\xb4O\xc5\xa2n)\xb3l\xb2\x1d\x11"\x18%\xb5\xa2!|\xb1\xf1EM4\xd9J\x85\\\xb8`\xf2\xfeCh\x1e=0.~\x12Bs\xdd\x18\xb0O\xdd\xf6`\xb8\x85\x8e\x1ex\xd0"\xcc\x03\xf4g\xf3\xcf\x1an]\xf5;\xb81yEB\xb1\xd0\x8e8\xd3\xbf\xb0\xbf.[\xa6\xf7Z\x0fw\xd5k\xf2\x92K\x14O\xff<\x87\xeczW\xbf\xf3E\xee\x8aD\x96gm8\xc9E<8\xe6E!\xdb-\xe6\xd6E*\xa8\xf3\xda\x16u\x13N\x8d\x90\xcb\xb0\xd2t\xcea\x89V?\xd9\xa5nV\xa8\x00f\x1ex{\x089Pb05\xdd\xee\xb2\xfb\x84\xf6\xfb%\x07\xf2\xc1W\xe7N\x81\xa8\x19p\xe1\x14u\xce\x92n9:U\xb0kw\xc4D\xdb\xd26\x88\xe8\xa7|\x7f03xt\xfe\xf7\x87\xa1\x87\xfc\xaf\xd7:ZH7\xc8\xe3\xe6\x07\x120\x85\x97\xffr\xea.\xda\xe6\x9c\x94\x02\xadz\xe8\x1a\xbb>\x91\x00\xf0\xc8{\x99\xb2VBF\xbdV\xaf\x8em\x0e\xcf)(\xe5\x15\x12\x18\xf7\xe6\'\xc5e\xe1U@foO|\x0e\x93|-\x0e\x84x/\xcb\x1bS^YolN\n\xed|\x1d5\x0e\x16\x9d\x04_.\xaa\xa4\xbb/\x94\xcd\x14\x95v\xf85\xe5\xee\xcbD\x18g}\x04D\xe5\x1f\xaf\xcb\xed*\xfa\xc5\x0b\x1d2\x0b\xc2#\xd2b6\x01\xae\xe6\xdfj6:$)K\xfb;\x00\xf2f\x8d\xfc@N\x9f\xa1\x7f\xe96\xe6b\x07V\xa6\x91\x8f}\xe2\xde4?8\x0f\xab\x83\xfd\xe9\x11\x12K\xe5\x08\xa4\x82\x01\\0\x82\x01X\xa0\x03\x02\x01\x12\xa2\x82\x01O\x04\x82\x01K\\>\t\xe4\x1d8,a(\x7f\x1e\xd2\x8dHH\x9c\xa3\x03?&\xb9\xf4\xba\xef\xcf\xcf\xb6(8\x91\x0f\xa3lq\xc6 f&Ou\xd8Bk\xe84s\xf1\xec\xf6\x97wY\xc6Un;\xf5\xdeh\xb9J\xd6\xaf\xf4r\x00\x80\x17\x8d\xc4p\x81\xac\x89\xf1\xf6\x98\xef\x1f\xb3\xe5\x91}\xf5m\x1a\xbd\x08\x1d\x0217W0\x81\xdd\x10O\xda\x97\xf1qo\xa9\xdcT\xe4_\xfaxt\xdf\xcb*\x95L\xd3\x85\xdf\xf04\x14\xb3\x14\x9c1cU\xe5\x18H\xf3^\x86\xd4\xd2\xe39-Y\x0b\x80\x92\xf0\x08\x03\xc5\x99{;z\xc0\xdd\x08\x1d\x94\xd4\xa4\xda,9\x00\xa7\x87I\x01\x9b\xb7\xf0\x01ITC\xcdJr\xd7+\x95\xadI\xf0\x14\xfc7t\xa2\x9a\xa7\xe0mA\x8c\'\xf0\x9c\xbc\x97\xaa\xd6\xec\x82.\xfa^\x08\xa7\x1b\xef\xa8\x979\x93\x8f\x80.i\x05\xf3jj\xef2\xf4B`Q\xed_\xde\x00\x14\xee\xae \xd1\xbc6\x8b;\xf19\x1fikM\xadf\x15\xc9\xb7G\xf6\xa9,\x9cJ\xe9e\xa8\xcc\x8e.b\x86\x88\xb3!p\x04\xe6\x03/\x17\xae\x03\x13:\xe4\xedG%\x98$\x9d\x13<\x92\x16\x80:\x94\x8f\x87jb\xa6.\xc2\n\xbe\xdb\x9d3\x8d\xf5\xb2\\\x8b\xd6\xcb\xc0\xa6%\xc7\xb1\xe3m\x86\x1fsXj\x19\xad\xe7\x06\xfc\x0b\xf1\xcf' + += GSS_Accept_sec_context (KRB_AP_REQ->KRB_AP_REP) - DCE_STYLE + +with KrbRandomPatcher(): + srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + +assert negResult == 1 +assert isinstance(tok, KRB_AP_REP) +ap_rep = KRB_AP_REP(bytes(tok)) + +apreppart = ap_rep.encPart.decrypt(client.KEY) +assert apreppart.ctime == "20240305165255Z" +assert apreppart.subkey.keyvalue == b"0000000000000000" +assert apreppart.subkey.keytype == 17 + +# Hardcode (yes this will probably require updating this test) +bytes(tok) +assert bytes(tok) == b'on0l\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2`0^\xa0\x03\x02\x01\x12\xa2W\x04UaS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd\xd3\xc3\xd9\xadN`\xd2;\xd7{\xb7\xf4p.\xa9\x9a\xb1}D\xc6|_t\n\r"M\xcd\xe2\t\xf0Ri\xc7\xcf\xb5\xefr9\xf0`iS7N\x06qKP\x06\xde\xc4\x18\xd5_\xcb\x0ct\x03k\xbc\xb9\x1adT\x03\xc1\x8bM' + += GSS_Init_sec_context (SPNEGO_negToken: KRB_AP_REP->KRB_AP_REP) - DCE_STYLE + +with KrbRandomPatcher(): + clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + +assert negResult == 0 +assert isinstance(tok, KRB_AP_REP) +ap_rep = KRB_AP_REP(bytes(tok)) + +apreppart = ap_rep.encPart.decrypt(client.KEY) +assert apreppart.ctime == "20240305165255Z" + +# Hardcode (yes this will probably require updating this test) +bytes(tok) +assert bytes(tok) == b'oQ0O\xa0\x03\x02\x01\x05\xa1\x03\x02\x01\x0f\xa2C0A\xa0\x03\x02\x01\x12\xa2:\x048aS\xeck\xcc\xad~\xfa^\x8d\xca\xbb\xc5\xd2/\xfd.e\xec\xef\xce\x91\x1d\x99\xd8\xcd2\x01\x0fA\xe4\xde\x12\xf4\xbc>\xe1\x98T\xc4\x82\xb5w\x1arZb\xdb\x9b-+\xf3\xfa\x0b\xdeD' + += GSS_Accept_sec_context (KRB_AP_REP->OK) - DCE_STYLE + +with KrbRandomPatcher(): + srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) + +assert negResult == 0 +assert tok is None + + += GSS_Wrap/GSS_Unwrap: client sends wrapped payload without confidentiality + +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" + +sig = client.GSS_Wrap( + clicontext, + data, + conf_req_flag=False, +) +assert sig.TOK_ID == b"\x05\x04" +assert sig.root.Flags == 4 +assert sig.root.EC == 12 +assert sig.root.RRC == 12 +assert bytes(sig) == b'\x05\x04\x04\xff\x00\x0c\x00\x0c\x00\x00\x00\x00@\x00\x00\x00\x8f\x0c\xab\x90h\xc8\xdf1\x078\x03\x0ctestAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE' + +ddata = server.GSS_Unwrap( + srvcontext, + sig, +) +assert ddata == data + += GSS_Wrap/GSS_Unwrap: server answers back without confidentiality + +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" + +sig = server.GSS_Wrap( + srvcontext, + data, + conf_req_flag=False, +) +assert sig.TOK_ID == b"\x05\x04" +assert sig.root.Flags == 5 +assert sig.root.EC == 12 +assert sig.root.RRC == 12 +bytes(sig) +assert bytes(sig) == b"\x05\x04\x05\xff\x00\x0c\x00\x0c\x00\x00\x00\x00\x00\x00\x00\x00~\xd8\x08\x89K'\xa0\x01\xda\x7f\xff\xd3testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" + +ddata = client.GSS_Unwrap( + clicontext, + sig, +) +assert ddata == data + += GSS_WrapEx/GSS_UnwrapEx: client sends wrapped payload with confidentiality + +from unittest import mock +from scapy.libs.rfc3961 import Key, EncryptionType + +# Data + +dcerpc_hdr = bytes.fromhex("0500000310000000fc004c00030000008c00000001000c00") +dcerpc_data = bytes.fromhex("000000001bc104a40f046e43bd2a4b2722092807010000000100000000000000e40400000904000000000000010000000600000001000000000002000000000001000000000000000000020000000000120000000000000000000000000000001200000000000000440043003d0063006f006e0074006f0073006f002c00440043003d0063006f006d00000000000000") +dcerpc_sectrailer = bytes.fromhex("0906040000000000") +Confounder = bytes.fromhex("aeb63f1db8e2cb61548867a0e4074e85") +k = Key(EncryptionType.AES256_CTS_HMAC_SHA1_96, key=bytes.fromhex("613f2dfabd35d17d86b00cf1001ce9458bf379c1d3921bbfdcd2de8782bec540")) +SeqNum = 0x60298ed4 + +# Prepare context + +clicontext = KerberosSSP.CONTEXT(IsAcceptor=False) +srvcontext = KerberosSSP.CONTEXT(IsAcceptor=True) + +clicontext.KrbSessionKey = srvcontext.KrbSessionKey = k +clicontext.SendSeqNum = srvcontext.RecvSeqNum = SeqNum +clicontext.flags = srvcontext.flags = ( + 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 | + GSS_C_FLAGS.GSS_C_CONF_FLAG +) + +client = server = KerberosSSP() + +# Test + +with mock.patch('scapy.layers.kerberos.os.urandom', side_effect=lambda x: Confounder): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=dcerpc_hdr), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=dcerpc_data), + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=dcerpc_sectrailer), + ], + ) + +assert _msgs[0].data == dcerpc_hdr +assert _msgs[1].data == b'|\xdf\xf8\xe5lS#\xe9\x9c\x15\xb4\xad\x06\xa8\xb9\x01\xd2\x13\xe6qLL\xd1\x82:v\xf2\xb1B\xc9u \xc5\xc88\xce\x91\xed*\x9c+v,W\x97\xde\xaan\xb8\x80\x9bd\xedW\x1aot\xa1\xb8\xbdp\xc0\xee\xe5\xb0\xa4\xce\x15{OA\x08\xee#;w\tV\x0e3\x9el\x00\x8f\xbaM\x07[\x1f,&\x99\x92\x91tvh\xbf\xcf\xb6\xd1\xbaB\xe3\xc9\x943\xed\xf04\x92!\xbd`\x00\x05;\xfce18H\xcb\xd8\x1eTT\x18\xbe\xb4\xbc\x08X\x1b$\x96\x04\xc9\xc6\xf1$\xfc,\xc0' +assert _msgs[2].data == dcerpc_sectrailer + +assert sig.TOK_ID == b"\x05\x04" +assert sig.root.Flags == 6 +assert sig.root.Filler == 0xFF +assert sig.root.EC == 16 +assert sig.root.RRC == 28 +assert sig.root.SND_SEQ == SeqNum +assert bytes(sig) == b'\x05\x04\x06\xff\x00\x10\x00\x1c\x00\x00\x00\x00`)\x8e\xd4\xf8\xb9\x99JO\xdeA\x9c+t\xbb\xe9>\xf0G\xd5\x9d\x9b\xca:\x10\xee\x1f\xe93\xc1*/`H\x89\xf4\xab\xd7E!\xd5<*ou\x94\xa3\t\xf1\x7f\xaa\xe9\x95}\xaa\xb7\x9f\xd4F\xfe\x9bt\xa1\x00' + +decrypted = server.GSS_UnwrapEx( + srvcontext, + _msgs, + sig, +)[1].data +assert decrypted == dcerpc_data + += GSS_WrapEx/GSS_UnwrapEx: server answers back confidentiality + +with KrbRandomPatcher(): + _msgs, sig = server.GSS_WrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=dcerpc_hdr), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=dcerpc_data), + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=dcerpc_sectrailer), + ], + ) + +assert _msgs[0].data == dcerpc_hdr +assert _msgs[1].data == b"\x9av\xf9 :e\x0f\xd8!\x1c\xc7\x076'a.NN\xcf\x0c\xec\x8c\x83\xb4\x9c'<%i\x17\xbe\xcc\x01 \x1d\x031\\Y\x92H\xe4\xd50W\x8e\xe0\xe85\xd8\xf5c[\x97Bl\x16\x12P\x03l\xdb\x99$\xef\x9a\x06\x85\x18\xcf\xc5\x91~\x88\xca\xb2D\xf8\xe5(+\xb30\r\xbf\xe8\xc7\x11\x18\xfa,&(\xc3l)c\x08%\xaf\x80\xe5u\xadw\x06\x15\xe8\xed\xfa\xb3\xe0\x1d\xb2\xdan\xcfb<\x01\x9d\xa6\xb4=W:Z\xb6\xbf\xe9\x1a\xc8g\x9d\x01\x87\x03DC1\xc0>d\x00\xa8\xc0}\xf3\x03\x00\x03\x00\x00\x00\xff\xff\xff\xff') + +assert pkt.NtVersion == 3 +assert pkt.NullGuid == uuid.UUID('00000000-0000-0000-0000-000000000000') +assert pkt.DnsForestName == b"domain.local." +assert pkt.DnsDomainName == b"domain.local." +assert pkt.DnsHostName == b"DC1.domain.local." +assert pkt.Flags == 0x3f37d + += Dissect NETLOGON_SAM_LOGON_RESPONSE_NT40 - V1 + +pkt = NETLOGON(b'\x13\x00\\\x00\\\x00D\x00C\x001\x00\x00\x00\x00\x00D\x00O\x00M\x00A\x00I\x00N\x00\x00\x00\x01\x00\x00\x00\xff\xff\xff\xff') + +assert pkt.NtVersion == 1 +assert pkt.UnicodeLogonServer == r"\\DC1" +assert pkt.UnicodeDomainName == "DOMAIN" diff --git a/test/scapy/layers/ldapopenldap.uts b/test/scapy/layers/ldapopenldap.uts new file mode 100644 index 00000000000..a25b692d944 --- /dev/null +++ b/test/scapy/layers/ldapopenldap.uts @@ -0,0 +1,42 @@ +% Tests that need a local instance of OpenLDAP to run + ++ Functional test against OpenLDAP +~ linux ci_only + += (OpenLDAP) connect to server, bind + +cli = LDAP_Client() +cli.connect("127.0.0.1") +cli.bind(LDAP_BIND_MECHS.SIMPLE, simple_username="cn=admin,dc=scapy,dc=net", simple_password="Bonjour1") +cli.close() + += (OpenLDAP) connect to server, bind, search + +cli = LDAP_Client() +cli.connect("127.0.0.1") +cli.bind(LDAP_BIND_MECHS.SIMPLE, simple_username="cn=admin,dc=scapy,dc=net", simple_password="Bonjour1") +res = cli.search("dc=scapy,dc=net", "(&(givenName=Another)(sn=Test))", scope=2) +cli.close() + +assert res == { + 'uid=another,ou=People,dc=scapy,dc=net': { + 'objectClass': ['top', + 'person', + 'inetOrgPerson'], + 'cn': ['Another Test'], + 'uid': ['another'], + 'sn': ['Test'], + 'givenName': ['Another'], + 'userPassword': ['testing'] + } +} + += (OpenLDAP) connect to server using SSL +~ disabled + +# We need a version of OpenLDAP that is more recent. Let's wait. + +cli = LDAP_Client() +cli.connect("127.0.0.1", use_ssl=True, no_check_certificate=True) +cli.bind(LDAP_BIND_MECHS.SIMPLE, simple_username="cn=admin,dc=scapy,dc=net", simple_password="Bonjour1") +cli.close() diff --git a/test/scapy/layers/llmnr.uts b/test/scapy/layers/llmnr.uts index fe95e259b8f..ef953c1a8df 100644 --- a/test/scapy/layers/llmnr.uts +++ b/test/scapy/layers/llmnr.uts @@ -10,12 +10,17 @@ assert pkt.sport == 5355 assert pkt.dport == 5355 assert pkt[LLMNRQuery].opcode == 0 += Dissection with the "T"entative bit set and the "TrunCation" bit unset +r = LLMNRResponse(b'\x87\xdf\x81\x00\x00\x01\x00\x01\x00\x00\x00\x00\x01C\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x1e\x00\x04\xc0\xa8-\x15') +assert r.tc == 0 and r.t == 1 + = 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.t == 0 assert pkt.z == 0 assert pkt.rcode == 0 assert pkt.qdcount == 0 @@ -36,3 +41,23 @@ b = Ether(b'\x14\x0cv\x8f\xfe(\xd0P\x99V\xdd\xf9\x08\x00E\x00\x00(\x00\x01\x00\x assert b.answers(a) assert not a.answers(b) += Summary +q = LLMNRQuery(b'\xd5\xd5\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07example\x00\x00\x01\x00\x01') +assert q.mysummary()[0] == r"LLMNRQuery who has 'example.'" + +q = LLMNRQuery(b'Yy\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x01\xff\x00\x00\x01\x00\x01') +assert q.mysummary()[0] == r"LLMNRQuery who has '\xff.'" + +with no_debug_dissector(): + q = LLMNRQuery(b'@@\x00\x1b\xed7\x96J\x00\x00\x00\x01\x00\x00') + assert q.mysummary()[0] == r"LLMNRQuery [malformed]" + +r = LLMNRResponse(b'e\xcc\x80\x00\x00\x01\x00\x01\x00\x00\x00\x00\x07example\x00\x00\x01\x00\x01\x07example\x00\x00\x01\x00\x01\x00\x00\x00\x1e\x00\x04\xc0\x00\x02\x01') +assert r.mysummary()[0] == r"LLMNRResponse 'example.' is at '192.0.2.1'" + +r = LLMNRResponse(b'\n\xe6\x80\x00\x00\x01\x00\x01\x00\x00\x00\x00\x01\xff\x00\x00\x1c\x00\x01\xc0\x0c\x00\x1c\x00\x01\x00\x00\x00\x1e\x00\x10\xfe\x80\x00\x00\x00\x00\x00\x00xu\x17\xff\xfe\xbc\xac\xcb') +assert r.mysummary()[0] == r"LLMNRResponse '\xff.' is at 'fe80::7875:17ff:febc:accb'" + +with no_debug_dissector(): + r = LLMNRResponse(b'\xd3<\x80\x00\x00\x01\x00\x01\x00\x00\x00\x00\x04H\x00\x00\x01\x00\x01\xc0\x0c\x00\x01\x00\x01\x00\x00\x00\x1e\x00\x04\xc0\xa88\x04') + assert r.mysummary()[0] == r"LLMNRResponse [malformed]" diff --git a/test/scapy/layers/lltd.uts b/test/scapy/layers/lltd.uts index 08cd53393ab..3b5dab77487 100644 --- a/test/scapy/layers/lltd.uts +++ b/test/scapy/layers/lltd.uts @@ -20,13 +20,13 @@ assert pkt.dst == pkt.real_dst assert pkt.src == pkt.real_src assert pkt.tos == 0 assert pkt.function == 0 -assert pkt.hashret() == b'\xd9\x88\x00\x00' +assert pkt.hashret()[2:] == b'\x00\x00' = 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)) +assert all(isinstance(LLTDAttribute(type=i), LLTDAttribute) for i in range(256)) +assert all(isinstance(LLTDAttribute(raw(LLTDAttribute(type=i))), LLTDAttribute) for i in range(256)) = Large TLV m1, m2, seq = RandMAC()._fix(), RandMAC()._fix(), 123 @@ -59,4 +59,5 @@ key, value = data.popitem() assert key.endswith(' [Detailed Icon Image]') assert value == 'abcdefg' - += Summary +assert LLTDAttributeMachineName(b'\x0f\x04{\x00\n\x00').mysummary()[0] == r"Hostname: '{\n'" diff --git a/test/scapy/layers/msdrsr.uts b/test/scapy/layers/msdrsr.uts new file mode 100644 index 00000000000..87861bc9d1f --- /dev/null +++ b/test/scapy/layers/msdrsr.uts @@ -0,0 +1,87 @@ +% MS-DRSR tests + ++ [MS-DRSR] test vectors + ++ Dissect DRSR Crack_Names exchange + += [EXCH] - Load MSDRSR exchange and decrypt (SPNEGOSSP/NTLMSSP) + +load_layer("msrpce") +bind_layers(TCP, DceRpc5, sport=49685) # the DCE/RPC port +bind_layers(TCP, DceRpc5, dport=49685) + +conf.dcerpc_session_enable = True +conf.winssps_passive = [ + SPNEGOSSP( + [ + NTLMSSP( + IDENTITIES={ + "Administrator": MD4le("Password123!"), + }, + ) + ] + ) +] +pkts = sniff(offline=scapy_path('test/pcaps/dcerpc_msdrsr_cracknames.pcapng.gz'), session=TCPSession) +conf.dcerpc_session_enable = False + += [EXCH] - Check IDL_DRSBind_Request + +from scapy.layers.msrpce.msdrsr import DRS_EXTENSIONS_INT + +bindreq = pkts[7] +assert IDL_DRSBind_Request in bindreq +ext = DRS_EXTENSIONS_INT(bindreq[IDL_DRSBind_Request].valueof("pextClient").rgb) +assert ext.Pid == 1234 +assert ext.dwReplEpoch == 1729468809 + += [EXCH] - Check IDL_DRSBind_Response + +import uuid + +bindresp = pkts[8] +assert IDL_DRSBind_Response in bindresp +assert bindresp[IDL_DRSBind_Response].phDrs.uuid == b'\xf4$I\xf5\xde\x0c\xfcO\x8b\xfa\xb0Y\x87\xf4\x11i' +ext = DRS_EXTENSIONS_INT(bindresp[IDL_DRSBind_Response].valueof("ppextServer").rgb) +assert ext.dwFlags.GETCHGREQ_V10 +assert ext.dwFlags == 0x3fffff7f +assert ext.Pid == 696 +assert ext.ConfigObjGuid == uuid.UUID('14ea64e0-3470-48e6-9ace-77012d8d474f') + += [EXCH] - Check IDL_DRSCrackNames_Request + +cnreq = pkts[9] +assert IDL_DRSCrackNames_Request in cnreq + +crackreq = cnreq[IDL_DRSCrackNames_Request].valueof("pmsgIn") +assert crackreq.formatOffered == 11 +assert crackreq.formatDesired == 0xfffffff2 + +assert crackreq.valueof("rpNames") == [ + b'S-1-5-21-1924137214-3718646274-40215721-522', + b'S-1-5-21-1924137214-3718646274-40215721-498', + b'S-1-5-21-1924137214-3718646274-40215721-516', + b'S-1-5-21-1924137214-3718646274-40215721-526', + b'S-1-5-21-1924137214-3718646274-40215721-527', + b'S-1-5-21-1924137214-3718646274-40215721-512', + b'S-1-5-21-1924137214-3718646274-40215721-519', + b'S-1-5-21-1924137214-3718646274-40215721-513', +] + += [EXCH] - Check IDL_DRSCrackNames_Response + +cnresp = pkts[10] +assert IDL_DRSCrackNames_Response in cnresp + +crackresp = cnresp[IDL_DRSCrackNames_Response].valueof("pmsgOut") +assert [x.valueof("pName") for x in crackresp.valueof("pResult").valueof("rItems")] == [ + b'Cloneable Domain Controllers@DOMAIN', + b'Enterprise Read-only Domain Controllers@DOMAIN', + b'Domain Controllers@DOMAIN', + b'Key Admins@DOMAIN', + b'Enterprise Key Admins@DOMAIN', + b'Domain Admins@DOMAIN', + b'Enterprise Admins@DOMAIN', + b'Domain Users@DOMAIN', +] + diff --git a/test/scapy/layers/msnrpc.uts b/test/scapy/layers/msnrpc.uts new file mode 100644 index 00000000000..7b31bf85421 --- /dev/null +++ b/test/scapy/layers/msnrpc.uts @@ -0,0 +1,505 @@ +% MS-NRPC tests + ++ [MS-NRPC] test vectors + += [MS-NRPC] test vectors - sect 4.2 + +from scapy.layers.tls.crypto.hash import Hash_MD4 +from scapy.layers.msrpce.msnrpc import ComputeSessionKeyStrongKey + +# Clear-text SharedSecret: +ClearSharedSecret = bytes.fromhex("2e002f002c006e004c003e004f004c005a003600730074005e0058004b0065004d0025002e0049002d00740045006000570056006a0043005b00300036003f005d003a00510076005f0054006e0055006f003a003a00420077002c0067006000760023004a004d0036004d007100530050007500550028006e00710034003e0079006a005b0064005c002b005600700052005f00790078007500630021006700300054003600350076007a005700410042005f004200220069003c003c0053002b00340027005e003a0021002c003b002500470073002d00280022003a0020006d003e00210043004c0066006e004e00") + +# OWF of SharedSecret: +SharedSecret = Hash_MD4().digest(ClearSharedSecret) +assert SharedSecret.hex() == "31a590170a351fd51148b2a10af2c305" + +# Client Challenge: + +ClientChallenge = bytes.fromhex("3a0390a46d0c3d4f") + +# Server Challenge: +ServerChallenge = bytes.fromhex("0c4c13d16041c860") + +# Session Key: +assert ComputeSessionKeyStrongKey(SharedSecret, ClientChallenge, ServerChallenge).hex() == "eefe8f40007a2eeb6843d0d30a5be2e3" + += [MS-NRPC] test vectors - sect 4.3 + +from unittest import mock +from scapy.layers.msrpce.msnrpc import NetlogonSSP + +# Input +SessionKey = bytes.fromhex("0cb6948805f797bf2a82807973b89537") +Confounder = bytes.fromhex("717f5076c5902bcd") +ClearTextMessage = bytes.fromhex("3000000000000000000000000000000030000000000000005c005c00570049004e002d00450055003400550047003800370048003200490056002e00320033003000360066006500760032002e006e00740074006500730074002e006d006900630072006f0073006f00660074002e0063006f006d0000000000020000000000100000000000000000000000000000001000000000000000570049004e002d004400310049005400420046004d003400410038005500000085bb1511fd09786d3b61b06400000000000000000000000001000000000000000000000000000000") +# Expected +FullNetlogonSignatureHeader = bytes.fromhex("13001a00ffff0000b37c1f0ec86468f086761f2f86f4f4c1632d1f547d2cf6ff") +EncryptedMessage = bytes.fromhex("c930c9a079d95c78bea6a3150908c11f4b68e41219bcb91680ead287da211eec66bc27df2bc9a0f4ecf25c88624e493c59cdec6bc7b08bed84b97c33138ae3c8377cb327f3ea6076da91c5d23dbf1b2f4066a455332716b7b64f2ec9a944702d20a85035de3b231a5216b7a6c9102bd17c7d6ab1b379445eb5a5276e360d3bcef93b5359d36b0006b0c10bc2fec73777816a383a4614494b7b18bc34cd5447681eb48f8132a0a08a50d752826cff068c76959d49767557e503d509fa3c18b0860a22a7e2bae50e812c5d71c31f9f1dfd143333b3043f6bf906e5d91207f1d988") + +# Perform the same operation using NetlogonSSP: + +client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) +clicontext, tok, negResult = client.GSS_Init_sec_context(None) + +with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=ClearTextMessage), + ] + ) + +assert _msgs[0].data == EncryptedMessage +assert bytes(sig)[:len(FullNetlogonSignatureHeader)] == FullNetlogonSignatureHeader + += [MS-NRPC] test vectors - sect 4.3.1 + +from unittest import mock +from scapy.layers.msrpce.msnrpc import NetlogonSSP + +# Input +RpcPDUHeader = bytes.fromhex("0500000310000000380138000c000000d400000001001500") +RpcSecTrailer = bytes.fromhex("44060c0003000000") +# Expected +FullNetlogonSignatureHeader = bytes.fromhex("13001a00ffff00005d69950dfde45ae9f092ae5c3c55aacd632d1f547d2cf6ff") + +# Perform the same operation using NetlogonSSP: + +client = NetlogonSSP(SessionKey=SessionKey, computername="DC1", domainname="DOMAIN", AES=True) +clicontext, tok, negResult = client.GSS_Init_sec_context(None) + +with mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=lambda x: Confounder): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=RpcPDUHeader), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=ClearTextMessage), + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=RpcSecTrailer), + ] + ) + +assert _msgs[0].data == RpcPDUHeader +assert _msgs[1].data == EncryptedMessage +assert _msgs[2].data == RpcSecTrailer +assert bytes(sig)[:len(FullNetlogonSignatureHeader)] == FullNetlogonSignatureHeader + ++ Dissect and Build full NRPC exchange + +# XXX in the DCE/RPC spec + MS-RPCE, padding is only supposed to be zeros +# but for some reason it's weird 0xaaaa, 0xaabb... stuff in Windows. +# This is ignored by all implementations, and looks like leftovers from Microsoft debugging +# but it means parsing + rebuilding properly a packet is *slightly* different. +# In the tests you will find several instances where we manually replace the padding with 0xAA, or similar +# to make the output match, but it would be cool to reverse engineer the ndr lib in windows and copy +# exactly the same debug values + += [EXCH] - Load MSRPCE and bind + +load_layer("msrpce") +bind_layers(TCP, DceRpc, sport=40564) # the DCE/RPC port +bind_layers(TCP, DceRpc, dport=40564) + += [EXCH] - Parse NRPC exchange (pcap) + +pkts = sniff(offline=scapy_path('test/pcaps/dcerpc_msnrpc.pcapng.gz'), session=DceRpcSession) + += [EXCH] - Check ept_map_Request + +from scapy.layers.msrpce.ept import * + +epm_req = pkts[2][DceRpc5].payload.payload +assert isinstance(epm_req, ept_map_Request) +assert epm_req.max_towers == 4 +assert epm_req.map_tower.value.max_count == 75 +assert epm_req.map_tower.value.tower_length == 75 + +twr = protocol_tower_t(epm_req.map_tower.value.tower_octet_string) +assert twr.count == 5 +assert twr.floors[0].sprintf("%uuid%") == 'logon' + += [EXCH] - Re-build ept_map_Request from scratch + +pkt = ept_map_Request( + entry_handle=NDRContextHandle(attributes=0, uuid=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), + obj=NDRPointer( + referent_id=1, + value=UUID(Data1=0, Data2=0, Data3=0, Data4=b'\x00\x00\x00\x00\x00\x00\x00\x00') + ), + map_tower=NDRPointer( + referent_id=2, + value=twr_p_t(tower_octet_string=b'\x05\x00\x13\x00\rxV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0b\x02\x00\x00\x00\x01\x00\x07\x02\x00\x00\x87\x01\x00\t\x04\x00\x00\x00\x00\x00') + ), + max_towers=4, + ndr64=False, +) + +output = bytearray(bytes(pkt)) +assert bytes(output) == bytes(epm_req) + += [EXCH] - Check ept_map_Response + +epm_resp = pkts[3][DceRpc5].payload.payload + +assert epm_resp.entry_handle.attributes == 0 +assert epm_resp.entry_handle.uuid == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' +assert epm_resp.ITowers.max_count == 4 +assert epm_resp.ITowers.value[0].value[0].value.max_count == 75 +assert epm_resp.valueof("ITowers")[0].max_count == 75 +assert epm_resp.ITowers.value[0].value[0].value.tower_length == 75 +assert epm_resp.valueof("ITowers")[0].tower_length == 75 + +twr = protocol_tower_t(epm_resp.ITowers.value[0].value[0].value.tower_octet_string) +assert twr.floors[0].sprintf("%uuid%") == 'logon' +assert twr.floors[1].sprintf("%uuid%") == 'NDR 2.0' +assert twr.floors[1].rhs == 0 +assert twr.floors[2].protocol_identifier == 11 +assert twr.floors[3].sprintf("%protocol_identifier%") == "NCACN_IP_TCP" +assert twr.floors[3].rhs == 49676 +assert twr.floors[4].sprintf("%protocol_identifier%") == "IP" +assert twr.floors[4].rhs == "192.168.122.17" + += [EXCH] - Re-build ept_map_Response from scratch + +pkt = ept_map_Response( + entry_handle=NDRContextHandle(attributes=0), + ITowers=[ + twr_p_t(tower_octet_string=b'\x05\x00\x13\x00\rxV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0b\x02\x00\x00\x00\x01\x00\x07\x02\x00\xc2\x0c\x01\x00\t\x04\x00\xc0\xa8z\x11'), + twr_p_t(tower_octet_string=b'\x05\x00\x13\x00\rxV4\x124\x12\xcd\xab\xef\x00\x01#Eg\xcf\xfb\x01\x00\x02\x00\x00\x00\x13\x00\r\x04]\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00+\x10H`\x02\x00\x02\x00\x00\x00\x01\x00\x0b\x02\x00\x00\x00\x01\x00\x07\x02\x00\xc2\x03\x01\x00\t\x04\x00\xc0\xa8z\x11') + ], + ndr64=False, +) + +pkt.ITowers.value[0].value[0].referent_id = 0x3 +pkt.ITowers.value[0].value[1].referent_id = 0x4 +pkt.ITowers.max_count = 4 +assert bytes(pkt) == bytes(epm_resp) + += [EXCH] - Check NetrServerReqChallenge_Request + +chall_req = pkts[6][NetrServerReqChallenge_Request] +assert chall_req.valueof("ComputerName") == b"WIN1" +assert chall_req.PrimaryName is None +assert chall_req.ClientChallenge.data == b"12345678" + += [EXCH] - Re-build NetrServerReqChallenge_Request from scratch + +pkt = NetrServerReqChallenge_Request( + ComputerName=b'WIN1', + ClientChallenge=PNETLOGON_CREDENTIAL(data=b'12345678'), + PrimaryName=None, + ndr64=False, +) + +assert bytes(pkt) == bytes(chall_req) + += [EXCH] - Check NetrServerReqChallenge_Response + +chall_resp = pkts[7][NetrServerReqChallenge_Response] +assert chall_resp.ServerChallenge.data == b'Zq/\xc4D\xfeRI' +assert chall_resp.status == 0 + += [EXCH] - Re-build NetrServerReqChallenge_Response from scratch + +pkt = NetrServerReqChallenge_Response( + ServerChallenge=PNETLOGON_CREDENTIAL(data=b'Zq/\xc4D\xfeRI'), + ndr64=False, +) + +assert bytes(pkt) == bytes(chall_resp) + += [EXCH] - Check NetrServerAuthenticate3_Request + +auth_req = pkts[8][NetrServerAuthenticate3_Request] +assert auth_req.PrimaryName is None +assert auth_req.valueof("AccountName") == b"WIN1$" +assert auth_req.sprintf("%SecureChannelType%") == "WorkstationSecureChannel" +assert auth_req.valueof("ComputerName") == b"WIN1" +assert auth_req.ClientCredential.data == b'd:\xb3p\xc6\x9e\xf40' +assert auth_req.NegotiateFlags == 1611661311 + += [EXCH] - Re-build NetrServerAuthenticate3_Request from scratch + +pkt = NetrServerAuthenticate3_Request( + AccountName=b'WIN1$', + ComputerName=b'WIN1', + ClientCredential=PNETLOGON_CREDENTIAL(data=b'd:\xb3p\xc6\x9e\xf40'), + PrimaryName=None, + SecureChannelType="WorkstationSecureChannel", + NegotiateFlags=1611661311, + ndr64=False, +) + +output = bytearray(bytes(pkt)) +assert bytes(output) == bytes(auth_req) + += [EXCH] - Check NetrServerAuthenticate3_Response + +auth_resp = pkts[9][NetrServerAuthenticate3_Response] +assert auth_resp.ServerCredential.data == b'1h\x8d\xb8\xf4zH\xaf' +assert auth_resp.NegotiateFlags == 1611661311 +assert auth_resp.AccountRid == 1105 +assert auth_resp.status == 0 + += [EXCH] - Re-build NetrServerAuthenticate3_Response from scratch + +pkt = NetrServerAuthenticate3_Response( + ServerCredential=PNETLOGON_CREDENTIAL(data=b'1h\x8d\xb8\xf4zH\xaf'), + NegotiateFlags=1611661311, + AccountRid=1105, + status=0, + ndr64=False, +) + +assert bytes(pkt) == bytes(auth_resp) + ++ GSS-API NetlogonSSP tests +~ mock + += [NetlogonSSP] - Create randomness-mock context manager + +# mock the random to get consistency +from unittest import mock + +def fake_urandom(x): + # wow, impressive entropy + return b"0" * x + +_patches = [ + # Patch all the random + mock.patch('scapy.layers.msrpce.msnrpc.os.urandom', side_effect=fake_urandom), +] + +class NetlogonRandomPatcher: + def __enter__(self): + for p in _patches: + p.start() + def __exit__(self, *args, **kwargs): + for p in _patches: + p.stop() + += [NetlogonSSP] - RC4 - Create client and server NetlogonSSP + +from scapy.layers.msrpce.msnrpc import NetlogonSSP, NL_AUTH_MESSAGE + +client = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00", computername="DC1", domainname="DOMAIN", AES=False) +server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00", computername="DC1", domainname="DOMAIN", AES=False) + += [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE) + +clicontext, tok, negResult = client.GSS_Init_sec_context(None) + +assert negResult == 1 +assert isinstance(tok, NL_AUTH_MESSAGE) +assert tok.MessageType == 0 +assert tok.Flags == 3 + +bytes(tok) +assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' + += [NetlogonSSP] - RC4 - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) + +srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + +assert negResult == 0 +assert tok.MessageType == 1 + +bytes(tok) +assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' + += [NetlogonSSP] - RC4 - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) + +clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + +assert negResult == 0 +assert tok is None + += [NetlogonSSP] - RC4 - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload + +data_header = b"header" # signed but not encrypted +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" # encrypted + +with NetlogonRandomPatcher(): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +encrypted = _msgs[1].data +assert bytes(encrypted) == b'~\x82\xda\x9e>t?QA\xe7\x06B\x87\x01\x03\x97\xea\xd2\xe9\xc4\xbfM$\x95VKxivff\x93\x9a\xe8\rbe#\xe6W\xb4\x82A\xd8\xa7\xf7]\xf3\xb0\x88' +assert bytes(sig) == b'w\x00z\x00\xff\xff\x00\x00\x9f\xcb\xb6s\x8c\x8c\x0c*\xa9E\xa4\xd1\x85\xee.\xa2:\xd7\x99\xdaO\x05N ' + +decrypted = server.GSS_UnwrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig +)[1].data +assert decrypted == data + += [NetlogonSSP] - RC4 - GSS_WrapEx/GSS_UnwrapEx: server answers back + +with NetlogonRandomPatcher(): + _msgs, sig = server.GSS_WrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +re_encrypted = _msgs[1].data +assert bytes(re_encrypted) == b'\x9b\xc7c\x81\xfbF(\x19\xb6>\x08i\x7f\x18~H\xd6m~\x11K\x83\xb6\x15\x9a\xceP\xa1K\x8d\x83\xbb\xa7\x0fR*J\x89-\xec!\xde\xffs)\xd8F\x9c@^' +assert bytes(sig) == b'w\x00z\x00\xff\xff\x00\x00\x9f\xcb\xb6r\x0c\x8c\x0c*\xa9E\xa4\xd1\x85\xee.\xa2\xdf\x92 \xc5\x8a7Yh' + +decrypted = client.GSS_UnwrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=re_encrypted), + ], + sig +)[1].data +assert decrypted == data + += [NetlogonSSP] - RC4 - GSS_WrapEx/GSS_UnwrapEx: inject fault + +_msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +encrypted = _msgs[1].data +assert encrypted != data +bad_data_header = data_header[:-3] + b"hey" +try: + server.GSS_UnwrapEx(srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=bad_data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig + ) + assert False, "No error was reported, but there should have been one" +except ValueError: + pass + += [NetlogonSSP] - AES - Create client and server NetlogonSSP + +from scapy.layers.msrpce.msnrpc import NetlogonSSP, NL_AUTH_MESSAGE + +client = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", computername="DC1", domainname="DOMAIN", AES=True) +server = NetlogonSSP(SessionKey=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", computername="DC1", domainname="DOMAIN", AES=True) + += [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE) + +clicontext, tok, negResult = client.GSS_Init_sec_context(None) + +assert negResult == 1 +assert isinstance(tok, NL_AUTH_MESSAGE) +assert tok.MessageType == 0 +assert tok.Flags == 3 + +bytes(tok) +assert bytes(tok) == b'\x00\x00\x00\x00\x03\x00\x00\x00DOMAIN\x00DC1\x00' + += [NetlogonSSP] - AES - GSS_Accept_sec_context (NL_AUTH_MESSAGE->NL_AUTH_MESSAGE) + +srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + +assert negResult == 0 +assert tok.MessageType == 1 + +bytes(tok) +assert bytes(tok) == b'\x01\x00\x00\x00\x00\x00\x00\x00' + += [NetlogonSSP] - AES - GSS_Init_sec_context (NL_AUTH_MESSAGE->OK) + +clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + +assert negResult == 0 +assert tok is None + += [NetlogonSSP] - AES - GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload + +data_header = b"header" # signed but not encrypted +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" # encrypted + +with NetlogonRandomPatcher(): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +encrypted = _msgs[1].data +assert bytes(encrypted) == b'\xbf\x1aP\xb4\xb54\xe4^\x1a\xfe\xf3\x1f(\xfa[\xc4\x06\xdb_\x1a9\x90P' +assert bytes(sig) == b'\x13\x00\x1a\x00\xff\xff\x00\x00.\n\x8e\xcf\xbek \x84\x978\xe2\xad\x8c\xdd\x8efS\x9b\xf3DG\xf4[\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +decrypted = client.GSS_UnwrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=re_encrypted), + ], + sig +)[1].data +assert decrypted == data + += [NetlogonSSP] - AES - GSS_WrapEx/GSS_UnwrapEx: inject fault + +_msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +encrypted = _msgs[1].data +assert encrypted != data +bad_data_header = data_header[:-3] + b"hey" +try: + server.GSS_UnwrapEx(srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=bad_data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig + ) + assert False, "No error was reported, but there should have been one" +except ValueError: + pass diff --git a/test/scapy/layers/msnrtp.uts b/test/scapy/layers/msnrtp.uts new file mode 100644 index 00000000000..0add8591ac3 --- /dev/null +++ b/test/scapy/layers/msnrtp.uts @@ -0,0 +1,155 @@ +% MS-NRTP tests + ++ [MS-NRTP] + += [MS-NRBF] parse .NET Binary Format + +from scapy.layers.ms_nrtp import * + +data = b'\x00\x01\x00\x00\x00\xff\xff\xff\xff\x01\x00\x00\x00\x00\x00\x00\x00\x0c\x02\x00\x00\x00NSystem.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089\x05\x01\x00\x00\x00\x13System.Data.DataSet\n\x00\x00\x00\x16DataSet.RemotingFormat\x13DataSet.DataSetName\x11DataSet.Namespace\x0eDataSet.Prefix\x15DataSet.CaseSensitive\x12DataSet.LocaleLCID\x1aDataSet.EnforceConstraints\x1aDataSet.ExtendedProperties\x14DataSet.Tables.Count\x10DataSet.Tables_0\x04\x01\x01\x01\x00\x00\x00\x02\x00\x07\x1fSystem.Data.SerializationFormat\x02\x00\x00\x00\x01\x08\x01\x08\x02\x02\x00\x00\x00\x05\xfd\xff\xff\xff\x1fSystem.Data.SerializationFormat\x01\x00\x00\x00\x07value__\x00\x08\x02\x00\x00\x00\x01\x00\x00\x00\x06\x04\x00\x00\x00\x00\t\x04\x00\x00\x00\t\x04\x00\x00\x00\x00\t\x04\x00\x00\x00\n\x01\x00\x00\x00\t\x05\x00\x00\x00\x0f\x05\x00\x00\x00\x07\x00\x00\x00\x02TRIMMED\x0b' + +pkt = NRBF(data) +assert len(pkt.records) == 5 + +assert isinstance(pkt.records[0], NRBFSerializationHeader) +assert pkt.records[0].RootID == 1 +assert pkt.records[0].HeaderId == -1 + +assert pkt.records[1].LibraryId == 2 +assert pkt.records[1].LibraryName.String == b'System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' + +assert pkt.records[2].ObjectId == 1 +assert pkt.records[2].MemberCount == 10 +assert len(pkt.records[2].MemberNames) == 10 +assert pkt.records[2].MemberNames[9].String == b"DataSet.Tables_0" +assert pkt.records[2].AdditionalInfos[0].Value.TypeName.String == b"System.Data.SerializationFormat" +assert pkt.records[2].AdditionalInfos[1].Value == PrimitiveTypeEnum.Boolean +assert pkt.records[2].AdditionalInfos[5].Value == PrimitiveTypeEnum.Byte +assert pkt.records[2].Members[0].Members[0].Value == 1 +assert isinstance(pkt.records[2].Members[1], NRBFBinaryObjectString) +assert isinstance(pkt.records[2].Members[2], NRBFMemberReference) +assert isinstance(pkt.records[2].Members[3], NRBFMemberReference) +assert isinstance(pkt.records[2].Members[4], NRBFMemberPrimitiveUnTyped) +assert isinstance(pkt.records[2].Members[7], NRBFObjectNull) +assert isinstance(pkt.records[2].Members[9], NRBFMemberReference) +assert pkt.records[2].Members[9].IdRef == 5 + +assert pkt.records[3].ObjectId == 5 +assert pkt.records[3].Values == b"TRIMMED" + +assert isinstance(pkt.records[4], NRBFMessageEnd) + += [MS-NRBF] build .NET Binary Format + +pkt = NRBF( + records=[ + NRBFSerializationHeader(HeaderId=-1), + NRBFBinaryLibrary( + LibraryId=2, + LibraryName=NRBFLengthPrefixedString( + String=b"System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + ), + ), + NRBFClassWithMembersAndTypes( + ObjectId=1, + Name=NRBFLengthPrefixedString(String=b"System.Data.DataSet"), + MemberCount=10, + MemberNames=[ + NRBFLengthPrefixedString(String=b"DataSet.RemotingFormat"), + NRBFLengthPrefixedString(String=b"DataSet.DataSetName"), + NRBFLengthPrefixedString(String=b"DataSet.Namespace"), + NRBFLengthPrefixedString(String=b"DataSet.Prefix"), + NRBFLengthPrefixedString(String=b"DataSet.CaseSensitive"), + NRBFLengthPrefixedString(String=b"DataSet.LocaleLCID"), + NRBFLengthPrefixedString(String=b"DataSet.EnforceConstraints"), + NRBFLengthPrefixedString(String=b"DataSet.ExtendedProperties"), + NRBFLengthPrefixedString(String=b"DataSet.Tables.Count"), + NRBFLengthPrefixedString(String=b"DataSet.Tables_0"), + ], + BinaryTypeEnums=[ + BinaryTypeEnum.Class, + BinaryTypeEnum.String, + BinaryTypeEnum.String, + BinaryTypeEnum.String, + BinaryTypeEnum.Primitive, + BinaryTypeEnum.Primitive, + BinaryTypeEnum.Primitive, + BinaryTypeEnum.Object, + BinaryTypeEnum.Primitive, + BinaryTypeEnum.PrimitiveArray, + ], + AdditionalInfos=[ + NRBFAdditionalInfo( + type=BinaryTypeEnum.SystemClass, + Value=NRBFClassTypeInfo( + TypeName=NRBFLengthPrefixedString( + String=b"System.Data.SerializationFormat" + ), + LibraryId=2, + ) + ), + NRBFAdditionalInfo( + type=BinaryTypeEnum.Primitive, + Value=PrimitiveTypeEnum.Boolean, + ), + NRBFAdditionalInfo( + type=BinaryTypeEnum.Primitive, + Value=PrimitiveTypeEnum.Int32, + ), + NRBFAdditionalInfo( + type=BinaryTypeEnum.Primitive, + Value=PrimitiveTypeEnum.Boolean, + ), + NRBFAdditionalInfo( + type=BinaryTypeEnum.Primitive, + Value=PrimitiveTypeEnum.Int32, + ), + NRBFAdditionalInfo( + type=BinaryTypeEnum.PrimitiveArray, + Value=PrimitiveTypeEnum.Byte, + ), + ], + LibraryId=2, + Members=[ + NRBFClassWithMembersAndTypes( + ObjectId=-3, + Name=NRBFLengthPrefixedString( + String=b"System.Data.SerializationFormat" + ), + MemberNames=[ + NRBFLengthPrefixedString(String=b"value__"), + ], + BinaryTypeEnums=[BinaryTypeEnum.Primitive], + AdditionalInfos=[ + NRBFAdditionalInfo(type=BinaryTypeEnum.Primitive, + Value=PrimitiveTypeEnum.Int32), + ], + LibraryId=2, + Members=[ + NRBFMemberPrimitiveUnTyped(type=PrimitiveTypeEnum.Int32, Value=1) + ], + ), + NRBFBinaryObjectString( + ObjectId=4, + Value=NRBFLengthPrefixedString(String=b""), + ), + NRBFMemberReference(IdRef=4), + NRBFMemberReference(IdRef=4), + NRBFMemberPrimitiveUnTyped(type=PrimitiveTypeEnum.Boolean, Value=0), + NRBFMemberPrimitiveUnTyped(type=PrimitiveTypeEnum.Int32, Value=1033), + NRBFMemberPrimitiveUnTyped(type=PrimitiveTypeEnum.Boolean, Value=0), + NRBFObjectNull(), + NRBFMemberPrimitiveUnTyped(type=PrimitiveTypeEnum.Int32, Value=1), + NRBFMemberReference(IdRef=5), + ], + ), + NRBFArraySinglePrimitive( + ObjectId=5, + PrimitiveTypeEnum=PrimitiveTypeEnum.Byte, + Values=b"TRIMMED", + ), + NRBFMessageEnd(), + ] +) + +assert bytes(pkt) == data diff --git a/test/scapy/layers/netbios.uts b/test/scapy/layers/netbios.uts index 9f4ff76c036..eaff95decfe 100644 --- a/test/scapy/layers/netbios.uts +++ b/test/scapy/layers/netbios.uts @@ -14,10 +14,14 @@ assert raw(z) == b'\x00\x00\x01\x10\x00\x01\x00\x00\x00\x00\x00\x00 FEEFFDFEDBCA pkt = IP(dst='192.168.0.255')/UDP(sport=137, dport='netbios_ns')/z pkt = IP(raw(pkt)) -assert pkt.QUESTION_NAME == b'TEST1 ' +assert pkt.QUESTION_NAME == b'TEST1' +assert pkt[NBNSQueryRequest].mysummary() == r"NBNSQueryRequest who has '\\TEST1'" assert NBNSQueryRequest in NBNSHeader(raw(z)) +z = NBNSQueryRequest(b' PPCACACACACACACACACACACACACACAAA\x00\x00 \x00\x01') +assert z.mysummary() == r"NBNSQueryRequest who has '\\\xff'" + = NBNSQueryResponse - build & dissect z = NBNSHeader()/NBNSQueryResponse(RR_NAME="FRED", ADDR_ENTRY=[NBNS_ADD_ENTRY(NB_ADDRESS="192.168.0.13")]) @@ -26,6 +30,41 @@ assert raw(z) == b'\x00\x00\x85\x00\x00\x00\x00\x01\x00\x00\x00\x00 EGFCEFEECACA pkt = NBNSHeader(raw(z)) assert NBNSQueryResponse in pkt assert pkt.ADDR_ENTRY[0].NB_ADDRESS == "192.168.0.13" +assert pkt[NBNSQueryResponse].mysummary() == r"NBNSQueryResponse '\\FRED' is at 192.168.0.13" + +z = NBNSQueryResponse(b' PPFCEFEECACACACACACACACACACACAAA\x00\x00 \x00\x01\x00\x04\x93\xe0\x00\x06\x00\x00\xc0\xa8\x00\r') +assert z.mysummary() == r"NBNSQueryResponse '\\\xffRED' is at 192.168.0.13" + +z = NBNSHeader(b'/S\x85\x80\x00\x00\x00\x01\x00\x00\x00\x00 FAEPFEEBFEEPCACACACACACACACACAAA\x00\x00 \x00\x01\x00\x03\xf4\x80\x00\x06\x00\x00\xc0\xa8\x01A') +assert z.RR_NAME == b'POTATO' +assert z.ADDR_ENTRY[0].G == 0 +assert z.ADDR_ENTRY[0].NB_ADDRESS == "192.168.1.65" + += NBNSQueryResponse answers NBNSQueryRequest + +req = IP(ihl=5, len=78, proto=17, chksum=8562, src='172.19.0.7', dst='172.19.0.255')/UDP(sport=137, dport=137, len=58, chksum=62101)/NBNSHeader(NM_FLAGS=17, QDCOUNT=1)/NBNSQueryRequest(QUESTION_NAME=b'Loremipsumdolor', SUFFIX=17217) +resp = IP(b'E\x00\x00Zn\xab@\x00@\x11s\xb5\xac\x13\x00\x05\xac\x13\x00\x07\x00\x89\x00\x89\x00FX\x8a\x00\x00\x85\x00\x00\x00\x00\x01\x00\x00\x00\x00 EMGPHCGFGNGJHAHDHFGNGEGPGMGPHCCA\x00\x00 \x00\x01\x00\x00\x00\xa5\x00\x06\x00\x00\xac\x13\x00\x05') + +try: + conf.checkIPaddr = True + assert not resp.answers(req) + conf.checkIPaddr = False + assert resp.answers(req) +finally: + conf.checkIPaddr = True + += NBNSQueryResponse answers long NBNSQueryRequest + +req = IP(ihl=5, len=78, proto=17, chksum=8562, src='172.19.0.7', dst='172.19.0.255')/UDP(sport=137, dport=137, len=58, chksum=62101)/NBNSHeader(NM_FLAGS=17, QDCOUNT=1)/NBNSQueryRequest(QUESTION_NAME=b'Loremipsumdolorsitamet', SUFFIX=17217) +resp = IP(b'E\x00\x00Zn\xab@\x00@\x11s\xb5\xac\x13\x00\x05\xac\x13\x00\x07\x00\x89\x00\x89\x00FX\x8a\x00\x00\x85\x00\x00\x00\x00\x01\x00\x00\x00\x00 EMGPHCGFGNGJHAHDHFGNGEGPGMGPHCCA\x00\x00 \x00\x01\x00\x00\x00\xa5\x00\x06\x00\x00\xac\x13\x00\x05') + +try: + conf.checkIPaddr = True + assert not resp.answers(req) + conf.checkIPaddr = False + assert resp.answers(req) +finally: + conf.checkIPaddr = True = NBNSNodeStatusResponse - build & dissect @@ -39,20 +78,32 @@ assert NBNSNodeStatusResponse in pkt pkt = UDP()/NBNSHeader()/NBNSNodeStatusRequest() assert raw(pkt.payload) == b'\x00\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01' +assert pkt[NBNSNodeStatusRequest].mysummary() == "NBNSNodeStatusRequest who has '\\\\*\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'" resp = UDP(b'\x00\x89\x00\x89\x00\xc9v>\x00\x00\x84\x00\x00\x00\x00\x01\x00\x00\x00\x00 CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x00!\x00\x01\x00\x00\x00\x00\x00\x89\x05DOMAIN \x00\x84\x00SRV1 \x00\x04\x00DOMAIN \x1c\x84\x00SRV1 \x04\x00DOMAIN \x1b\x04\x00RT\x00iX\x13\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') assert [x.NETBIOS_NAME.strip() for x in resp.NODE_NAME] == [b'DOMAIN', b'SRV1', b'DOMAIN', b'SRV1', b'DOMAIN'] assert resp.answers(pkt) +z = NBNSNodeStatusRequest(b' PPCACACACACACACACACACACACACACAAA\x00\x00!\x00\x01') +assert z.mysummary() == r"NBNSNodeStatusRequest who has '\\\xff'" + = NBNSWackResponse - build & dissect z = NBNSHeader()/NBNSWackResponse(RR_NAME="SARAH") assert raw(z) == b'\x00\x00\xbc\x00\x00\x00\x00\x01\x00\x00\x00\x00 FDEBFCEBEICACACACACACACACACACAAA\x00\x00 \x00\x01\x00\x00\x00\x02\x00\x02)\x10' pkt = NBNSHeader(raw(z)) -assert pkt[NBNSWackResponse].RR_NAME == b'SARAH ' +assert pkt[NBNSWackResponse].RR_NAME == b'SARAH' = NBTSession z = raw(TCP()/NBTSession()) assert z == b'\x00\x8b\x00\x8b\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\x00\x00\x00\x00\x00\x00\x00\x00' assert NBTSession in TCP(z) + += OSS-Fuzz Findings + +# Note: the packet is corrupted +with no_debug_dissector(): + raw_packet = b'E\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\x01\x00\x00\x05\x00\x00\x00' + packet = NBNSQueryResponse(raw_packet) + assert packet.summary() == "NBNSQueryResponse" diff --git a/test/scapy/layers/netflow.uts b/test/scapy/layers/netflow.uts index e15f0f0377e..5ad94a68feb 100644 --- a/test/scapy/layers/netflow.uts +++ b/test/scapy/layers/netflow.uts @@ -156,6 +156,198 @@ assert pkt[NetflowOptionsFlowsetV9].pad == b"\x00\x00" pkt[NetflowOptionsFlowsetV9].pad = None assert raw(pkt) == dat += NetflowV9 - Options Template build +~ netflow + +option_templateFlowSet_256 = NetflowOptionsFlowsetV9( + templateID = 256, + option_scope_length = 4*1, + option_field_length = 4*3, + scopes = [ + NetflowOptionsFlowsetScopeV9(scopeFieldType=1, scopeFieldlength= 4), + ], + options = [ + NetflowOptionsFlowsetOptionV9(optionFieldType= 10, optionFieldlength= 4), + NetflowOptionsFlowsetOptionV9(optionFieldType= 82, optionFieldlength= 32), + NetflowOptionsFlowsetOptionV9(optionFieldType= 83, optionFieldlength= 240) + ]) +assert raw(option_templateFlowSet_256) == b'\x00\x01\x00\x1c\x01\x00\x00\x04\x00\x0c\x00\x01\x00\x04\x00\n\x00\x04\x00R\x00 \x00S\x00\xf0\x00\x00' + += NetflowV9 - Advanced build, multiple flowsets and multiple records by flowset +~ netflow + +template_flowset = NetflowFlowsetV9( + templates=[ NetflowTemplateV9( + template_fields=[ + 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), + NetflowTemplateV9( + template_fields=[ + NetflowTemplateFieldV9(fieldType="IN_BYTES", fieldLength=1), + NetflowTemplateFieldV9(fieldType="IN_PKTS", fieldLength=4), + NetflowTemplateFieldV9(fieldType="PROTOCOL"), + NetflowTemplateFieldV9(fieldType="IPV6_SRC_ADDR"), + NetflowTemplateFieldV9(fieldType="IPV6_DST_ADDR"), + ], + templateID=257, + fieldCount=5) + ], + flowSetID=0 +) + +# Generate classes for data records +Record256 = GetNetflowRecordV9(template_flowset, templateID = 256) +Record257 = GetNetflowRecordV9(template_flowset, templateID = 257) + +# Now lets build a dataFlowSet with 5* #256 records +dataFlowset_1 = NetflowDataflowsetV9( + templateID=256, + records=[ + Record256( + IN_BYTES=b"\x12", + IN_PKTS=b"\0\0\0\0", + PROTOCOL=1, + IPV4_SRC_ADDR="192.168.0.10", + IPV4_DST_ADDR="192.168.0.11" + ), + Record256( + IN_BYTES=b"\x0c", + IN_PKTS=b"\1\1\1\1", + PROTOCOL=2, + IPV4_SRC_ADDR="172.0.0.10", + IPV4_DST_ADDR="172.0.0.11" + ), + Record256( + IN_BYTES=b"\x0c", + IN_PKTS=b"\1\1\1\1", + PROTOCOL=3, + IPV4_SRC_ADDR="172.0.0.10", + IPV4_DST_ADDR="172.0.0.11" + ), + Record256( + IN_BYTES=b"\x0c", + IN_PKTS=b"\1\1\1\1", + PROTOCOL=4, + IPV4_SRC_ADDR="172.0.0.10", + IPV4_DST_ADDR="172.0.0.11" + ), + Record256( + IN_BYTES=b"\x0c", + IN_PKTS=b"\1\1\1\1", + PROTOCOL=5, + IPV4_SRC_ADDR="172.0.0.10", + IPV4_DST_ADDR="172.0.0.11" + ) + ], +) + +dataFlowset_2 = NetflowDataflowsetV9( + templateID=257, + records=[ + Record257( + IN_BYTES=b"\x12", + IN_PKTS=b"\0\0\0\0", + PROTOCOL=1, + IPV6_SRC_ADDR="2001:db8:3333:4444:5555:6666:7777:8888", + IPV6_DST_ADDR="2001:db8::" + ), + Record257( + IN_BYTES=b"\x0c", + IN_PKTS=b"\1\1\1\1", + PROTOCOL=2, + IPV6_SRC_ADDR="2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF", + IPV6_DST_ADDR="2001:db8::" + ) + ], +) + +# An option template flowset, containing an unique template +opttmpl258_flowSet = NetflowOptionsFlowsetV9( + templateID = 258, + option_scope_length = 4*1, + option_field_length = 4*2, + scopes = [ + NetflowOptionsFlowsetScopeV9(scopeFieldType= 1, scopeFieldlength= 4), + ], + options = [ + NetflowOptionsFlowsetOptionV9(optionFieldType= 34, optionFieldlength= 4), + NetflowOptionsFlowsetOptionV9(optionFieldType= 35, optionFieldlength= 1) + ]) + +# And finally a Record class for #258 Options +class Record_258(NetflowRecordV9): + name = "Option interface-table" + fields_desc = [ + IntField("System", 0), + IntField("SAMPLING_INTERVAL", 4), + XByteField("SAMPLING_ALGORITHM", 1) + ] + match_subclass = True + + +# with a record Flowset +optiondataFlowset = NetflowDataflowsetV9( + templateID=258, + records=[ + Record_258( + System=424242, + SAMPLING_INTERVAL=100, + SAMPLING_ALGORITHM=0x01 + ), + Record_258( + System=242424, + SAMPLING_INTERVAL=1000, + SAMPLING_ALGORITHM=0x02 + ) + ], +) + +netflow_header = NetflowHeader()/NetflowHeaderV9(unixSecs=1547927349.328283) +pkt = netflow_header / template_flowset / opttmpl258_flowSet / dataFlowset_1 / dataFlowset_2 / optiondataFlowset +# Count: 12 = 2 + 1 + 5 + 2 + 2 + +assert raw(pkt) == b'\x00\t\x00\x0c\x00\x00\x00\x00\\C\x7f5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x01\x00\x00\x05\x00\x01\x00\x01\x00\x02\x00\x04\x00\x04\x00\x01\x00\x08\x00\x04\x00\x0c\x00\x04\x01\x01\x00\x05\x00\x01\x00\x01\x00\x02\x00\x04\x00\x04\x00\x01\x00\x1b\x00\x10\x00\x1c\x00\x10\x00\x01\x00\x18\x01\x02\x00\x04\x00\x08\x00\x01\x00\x04\x00"\x00\x04\x00#\x00\x01\x00\x00\x01\x00\x00L\x12\x00\x00\x00\x00\x01\xc0\xa8\x00\n\xc0\xa8\x00\x0b\x0c\x01\x01\x01\x01\x02\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x03\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x04\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x05\xac\x00\x00\n\xac\x00\x00\x0b\x00\x00\x01\x01\x00P\x12\x00\x00\x00\x00\x01 \x01\r\xb833DDUUffww\x88\x88 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x01\x01\x01\x01\x02 \x01\r\xb833DD\xcc\xcc\xdd\xdd\xee\xee\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x18\x00\x06y2\x00\x00\x00d\x01\x00\x03\xb2\xf8\x00\x00\x03\xe8\x02\x00\x00' + + += NetflowV9 - Advanced dissection, complete example +~ netflow + +pkt = NetflowHeader(b'\x00\t\x00\x0c\x00\x00\x00\x00\\C\x7f5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x004\x01\x00\x00\x05\x00\x01\x00\x01\x00\x02\x00\x04\x00\x04\x00\x01\x00\x08\x00\x04\x00\x0c\x00\x04\x01\x01\x00\x05\x00\x01\x00\x01\x00\x02\x00\x04\x00\x04\x00\x01\x00\x1b\x00\x10\x00\x1c\x00\x10\x00\x01\x00\x18\x01\x02\x00\x04\x00\x08\x00\x01\x00\x04\x00"\x00\x04\x00#\x00\x01\x00\x00\x01\x00\x00L\x12\x00\x00\x00\x00\x01\xc0\xa8\x00\n\xc0\xa8\x00\x0b\x0c\x01\x01\x01\x01\x02\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x03\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x04\xac\x00\x00\n\xac\x00\x00\x0b\x0c\x01\x01\x01\x01\x05\xac\x00\x00\n\xac\x00\x00\x0b\x00\x00\x01\x01\x00P\x12\x00\x00\x00\x00\x01 \x01\r\xb833DDUUffww\x88\x88 \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x01\x01\x01\x01\x02 \x01\r\xb833DD\xcc\xcc\xdd\xdd\xee\xee\xff\xff \x01\r\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x00\x18\x00\x06y2\x00\x00\x00d\x01\x00\x03\xb2\xf8\x00\x00\x03\xe8\x02\x00\x00') + +nf_header = pkt.getlayer(NetflowHeader) +assert nf_header.version == 9 +nfv9_header = pkt.getlayer(NetflowHeaderV9) +assert nf_header.count == 12 + +flowset_1 = pkt.getlayer(NetflowFlowsetV9, 1) +assert len(flowset_1.templates) == 2 +assert flowset_1.templates[0].templateID == 256 +assert flowset_1.templates[1].templateID == 257 +assert flowset_1.templates[1].fieldCount == 5 +assert flowset_1.templates[1].template_fields[1].fieldLength == 4 + +flowset_2 = pkt.getlayer(NetflowOptionsFlowsetV9, 1) +assert flowset_2.templateID == 258 +assert len(flowset_2.scopes) == 1 +assert len(flowset_2.options) == 2 +assert flowset_2.pad == b'\x00\x00' + +flowset_3 = pkt.getlayer(NetflowDataflowsetV9, 1) +assert flowset_3.templateID == 256 +assert flowset_3.length == 76 + +flowset_4 = pkt.getlayer(NetflowDataflowsetV9, 2) +assert flowset_4.templateID == 257 + +flowset_5 = pkt.getlayer(NetflowDataflowsetV9, 3) +assert flowset_5.templateID == 258 + ############ ############ @@ -264,3 +456,30 @@ 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' + +# test for netflow IP_DSCP (id=195) +dscp_flowset = NetflowFlowsetV9( + templates=[ + NetflowTemplateV9( + template_fields=[ + NetflowTemplateFieldV9(fieldType=195), + ], + templateID=273, + ) + ], + flowSetID=2, +) + +recordClass = GetNetflowRecordV9(dscp_flowset, templateID=273) + +dscp_dataset = NetflowDataflowsetV9( + templateID=273, + records=[ + recordClass( + IP_DSCP=42, + ), + ], +) + +# record is generated with 2 zero bytes of padding +assert(raw(dscp_dataset) == b'\x01\x11\x00\x08\x2a\x00\x00\x00') diff --git a/test/scapy/layers/ntlm.uts b/test/scapy/layers/ntlm.uts new file mode 100644 index 00000000000..83b66197297 --- /dev/null +++ b/test/scapy/layers/ntlm.uts @@ -0,0 +1,502 @@ +% NTLM tests + ++ [MS-NLMP] tests + += [MS-NLMP] 4.2.1 - Common Values + +User = "User" +UserDom = "Domain" +Passwd = "Password" +ServerName = "Server" +WorkstationName = "COMPUTER" +RandomSessionKey = b"UUUUUUUUUUUUUUUU" +Time = 0 +ClientChallenge = b'\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa' +ServerChallenge = b'\x01\x23\x45\x67\x89\xab\xcd\xef' + += [MS-NLMP] 4.2.4 + +NegotiateFlags = 0xe28a8233 +AVPairs1 = "Server" +AVPairs2 = "Domain" + += [MS-NLMP] 4.2.4.1.1 NTOWFv2() + +ResponseKeyNT = NTOWFv2(Passwd, User, UserDom) +assert ResponseKeyNT == b'\x0c\x86\x8a@;\xfdz\x93\xa3\x00\x1e\xf2.\xf0.?' + += Build NTLMv2_RESPONSE + +ntlm_response = NTLMv2_RESPONSE( + TimeStamp=Time, + ChallengeFromClient=ClientChallenge, + AvPairs=[ + AV_PAIR(AvId="MsvAvNbDomainName", Value=AVPairs2), + AV_PAIR(AvId="MsvAvNbComputerName", Value=AVPairs1), + AV_PAIR(AvId="MsvAvEOL"), # Windows does this (samba does not) + AV_PAIR(AvId="MsvAvEOL"), + ] +) + += [MS-NLMP] 4.2.4.2.2 NTLMv2 Response + +ntlm_response.NTProofStr = ntlm_response.computeNTProofStr( + ResponseKeyNT, + ServerChallenge, +) +assert ntlm_response.NTProofStr == b'h\xcd\n\xb8Q\xe5\x1c\x96\xaa\xbc\x92{\xeb\xefj\x1c' + += [MS-NLMP] 4.2.4.1.2 Session Base Key + +ExportedSessionKey = SessionBaseKey = NTLMv2_ComputeSessionBaseKey( + ResponseKeyNT, + ntlm_response.NTProofStr, +) +assert SessionBaseKey == b'\x8d\xe4\x0c\xca\xdb\xc1J\x82\xf1\\\xb0\xad\r\xe9\\\xa3' + += [MS-NLMP] 4.2.4.2.3 Encrypted Session Key + +EncryptedRandomSessionKey = RC4K(SessionBaseKey, RandomSessionKey) +assert EncryptedRandomSessionKey == b'\xc5\xda\xd2TO\xc9y\x90\x94\xce\x1c\xe9\x0b\xc9\xd0>' + += [MS-NLMP] 4.2.4.3 Messages + +ntlm_nego = NTLM_NEGOTIATE( + NegotiateFlags=NegotiateFlags, + ProductMajorVersion=5, + ProductMinorVersion=1, + ProductBuild=2600, +) +ntlm_nego.DomainName = UserDom +ntlm_nego.WorkstationName = WorkstationName + +# ntlm_chall = NTLM_Header(b'NTLMSSP\x00\x02\x00\x00\x00\x0c\x00\x0c\x008\x00\x00\x003\x82\x8a\xe2\x01#Eg\x89\xab\xcd\xef\x00\x00\x00\x00\x00\x00\x00\x00$\x00$\x00D\x00\x00\x00\x06\x00p\x17\x00\x00\x00\x0fS\x00e\x00r\x00v\x00e\x00r\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00') + +ntlm_auth = NTLM_Header(b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00l\x00\x00\x00T\x00T\x00\x84\x00\x00\x00\x0c\x00\x0c\x00H\x00\x00\x00\x08\x00\x08\x00T\x00\x00\x00\x10\x00\x10\x00\\\x00\x00\x00\x10\x00\x10\x00\xd8\x00\x00\x005\x82\x88\xe2\x05\x01(\n\x00\x00\x00\x0fD\x00o\x00m\x00a\x00i\x00n\x00U\x00s\x00e\x00r\x00C\x00O\x00M\x00P\x00U\x00T\x00E\x00R\x00\x86\xc3P\x97\xac\x9c\xec\x10%TvJW\xcc\xcc\x19\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaah\xcd\n\xb8Q\xe5\x1c\x96\xaa\xbc\x92{\xeb\xefj\x1c\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\x00\x00\x00\x00\x02\x00\x0c\x00D\x00o\x00m\x00a\x00i\x00n\x00\x01\x00\x0c\x00S\x00e\x00r\x00v\x00e\x00r\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc5\xda\xd2TO\xc9y\x90\x94\xce\x1c\xe9\x0b\xc9\xd0>') + +assert ntlm_auth.MIC is None + += [MS-NLMP] 4.2.4.4 GSS_WrapEx + +SeqNum = 0 +Plaintext = b'P\x00l\x00a\x00i\x00n\x00t\x00e\x00x\x00t\x00' + +SealKey = SEALKEY(ntlm_nego.NegotiateFlags, RandomSessionKey, "Client") +assert SealKey == b'Y\xf6\x00\x97<\xc4\x96\n%H\n|\x19nLX' + +SignKey = SIGNKEY(ntlm_nego.NegotiateFlags, RandomSessionKey, "Client") +assert SignKey == b'G\x88\xdc\x86\x1bG\x82\xf3]C\xfd\x98\xfe\x1a-9' + +# Build SSP and Context manually +ssp = NTLMSSP() +ctx = NTLMSSP.CONTEXT(IsAcceptor=False) +ctx.SendSeqNum = SeqNum +ctx.SendSignKey = SignKey +ctx.SendSealKey = SealKey +ctx.SendSealHandle = RC4Init(SealKey) + +_msgs, sig = ssp.GSS_WrapEx(ctx, [ + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=Plaintext), +]) +s = _msgs[0].data + +assert s == b'T\xe5\x01e\xbf\x196\xdc\x99` \xc1\x81\x1b\x0f\x06\xfb_' +assert sig.Checksum == b'\x7f\xb3\x8e\xc5\xc5]Iv' + +assert bytes(sig) == b'\x01\x00\x00\x00\x7f\xb3\x8e\xc5\xc5]Iv\x00\x00\x00\x00' + ++ GSS-API SPNEGO: SPNEGOSSP tests + += Create randomness-mock context manager + +# mock the random to get consistency +from unittest import mock + +def fake_urandom(x): + # wow, impressive entropy + return b"0" * x + +_patches = [ + # Patch all the random + mock.patch('scapy.layers.ntlm.os.urandom', side_effect=fake_urandom), +] + +class NTLMRandomPatcher: + def __enter__(self): + for p in _patches: + p.start() + def __exit__(self, *args, **kwargs): + for p in _patches: + p.stop() + + += Create client and server SPNEGOSSPs + +from scapy.layers.ntlm import NTLM_NEGOTIATE +from scapy.layers.spnego import SPNEGO_negTokenInit, SPNEGO_negTokenResp, SPNEGO_Token, SPNEGO_negToken, SPNEGO_MechListMIC, SPNEGOSSP + +client = SPNEGOSSP([ + NTLMSSP( + UPN="User1", + PASSWORD="Password1", + ), +]) +server = SPNEGOSSP([ + NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1"), + }, + NTLM_VALUES={ + "NetbiosDomainName": "DOMAIN", + "NetbiosComputerName": "WIN10", + "DnsDomainName": "domain.local", + "DnsComputerName": "WIN10.domain.local", + "DnsTreeName": "domain.local", + }, + ) +]) + += GSS_Init_sec_context (negTokenInit: NTLM_NEGOTIATE) + +clicontext, tok, negResult = client.GSS_Init_sec_context( + None, + req_flags=( + GSS_C_FLAGS.GSS_C_MUTUAL_FLAG | + GSS_C_FLAGS.GSS_C_INTEG_FLAG | + GSS_C_FLAGS.GSS_C_CONF_FLAG + ) +) +assert negResult == 1 +assert isinstance(tok, GSSAPI_BLOB) +tok = GSSAPI_BLOB(bytes(tok)) +assert tok.MechType.val == '1.3.6.1.5.5.2' +assert isinstance(tok.innerToken.token, SPNEGO_negTokenInit) +assert len(tok.innerToken.token.mechTypes) == 1 +assert tok.innerToken.token.mechTypes[0].oid == '1.3.6.1.4.1.311.2.2.10' +assert tok.innerToken.token.reqFlags is None +assert tok.innerToken.token.negHints is None +assert tok.innerToken.token.mechListMIC is None +assert tok.innerToken.token._mechListMIC is None + +ntlm_nego = tok.innerToken.token.mechToken.value +assert isinstance(ntlm_nego, NTLM_NEGOTIATE) +assert ntlm_nego.Payload == [] +assert ntlm_nego.MessageType == 1 +assert ntlm_nego.NegotiateFlags.NEGOTIATE_UNICODE and ntlm_nego.NegotiateFlags.NEGOTIATE_SIGN and ntlm_nego.NegotiateFlags.NEGOTIATE_KEY_EXCH +assert ntlm_nego.NegotiateFlags == 0xe2898235 +assert ntlm_nego.ProductMajorVersion == 10 +assert ntlm_nego.ProductMinorVersion == 0 +assert ntlm_nego.ProductBuild == 19041 +assert bytes(ntlm_nego) == b'NTLMSSP\x00\x01\x00\x00\x005\x82\x89\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00aJ\x00\x00\x00\x0f' + += GSS_Accept_sec_context (SPNEGO_negTokenResp: NTLM_NEGOTIATE->NTLM_CHALLENGE) + +with NTLMRandomPatcher(): + srvcontext, tok, negResult = server.GSS_Accept_sec_context(None, tok) + +assert negResult == 1 +assert isinstance(tok, SPNEGO_negToken) +tok = SPNEGO_negToken(bytes(tok)) +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert tok.token.negResult == 1 +assert tok.token.supportedMech.oid == '1.3.6.1.4.1.311.2.2.10' +assert isinstance(tok.token.responseToken, SPNEGO_Token) +assert tok.token.mechListMIC is None + +ntlm_chall = tok.token.responseToken.value +assert isinstance(ntlm_chall, NTLM_CHALLENGE) +assert ntlm_chall.NegotiateFlags == 0xe2898235 +assert ntlm_chall.getAv(2).Value == "DOMAIN" +assert ntlm_chall.getAv(1).Value == "WIN10" +assert ntlm_chall.getAv(4).Value == "domain.local" +assert ntlm_chall.getAv(3).Value == "WIN10.domain.local" +assert ntlm_chall.getAv(5).Value == "domain.local" +assert ntlm_chall.getAv(0) + += GSS_Init_sec_context (SPNEGO_negToken: NTLM_CHALLENGE->NTLM_AUTHENTICATE) + +with NTLMRandomPatcher(): + clicontext, tok, negResult = client.GSS_Init_sec_context(clicontext, tok) + +assert isinstance(tok, SPNEGO_negToken) +tok = SPNEGO_negToken(bytes(tok)) +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert tok.token.negResult is None +assert tok.token.supportedMech is None +assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) +sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +assert sig.Version == 1 +assert sig.SeqNum == 0 +assert isinstance(tok.token.responseToken, SPNEGO_Token) + +ntlm_auth = NTLM_Header(tok.token.responseToken.value.val) +assert isinstance(ntlm_auth, NTLM_AUTHENTICATE_V2) +assert ntlm_auth.NegotiateFlags == 0xe2898235 +assert ntlm_auth.UserName == "User1" +assert ntlm_auth.DomainName == "DOMAIN" +assert ntlm_auth.Workstation == "WIN10" +assert ntlm_chall.TargetInfo[:6] == ntlm_auth.NtChallengeResponse.AvPairs[:6] +assert ntlm_auth.NtChallengeResponse.TimeStamp == ntlm_chall.getAv(7).Value +assert ntlm_auth.NtChallengeResponse.getAv(6).Value == 2 +assert ntlm_auth.NtChallengeResponse.getAv(9).Value == "host/WIN10" + += GSS_Accept_sec_context (SPNEGO_negToken: NTLM_AUTHENTICATE->OK) + +srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok) +assert negResult == 0 # success :p +assert isinstance(tok, SPNEGO_negToken) +assert isinstance(tok.token, SPNEGO_negTokenResp) +assert tok.token.negResult == 0 +assert tok.token.supportedMech is None +assert tok.token.responseToken is None +assert isinstance(tok.token.mechListMIC, SPNEGO_MechListMIC) +sig = NTLMSSP_MESSAGE_SIGNATURE(tok.token.mechListMIC.value.val) +assert sig.Version == 1 +assert sig.SeqNum == 0 + +assert srvcontext.SessionKey == clicontext.SessionKey +assert clicontext.SessionKey == b"0000000000000000" + += GSS_WrapEx/GSS_UnwrapEx: client sends a encrypted payload + +data_header = b"header" # signed but not encrypted +data = b"testAAAAAAAAAABBBBBBBBBCCCCCCCCCDDDDDDDDDEEEEEEEEE" # encrypted + +with NTLMRandomPatcher(): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +encrypted = _msgs[1].data +assert bytes(encrypted) == b'\x9c_\xe9\xf2D\xc3\xe9^\xcd\x939\xff\xac\xa8\x16Y7\xcb \x80mS\xee.3\x85\x90\xfe\xb1_l\xcc\xcc\x7fl\x1ae,\x8b\xb3\x1cK\xd7zT\x1b\xd4W9Z' +assert sig.Checksum == b'\x91\xca\x9d\x0c\x15\x1e\xc5"' + +decrypted = server.GSS_UnwrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig +)[1].data +assert decrypted == data + += GSS_WrapEx/GSS_UnwrapEx: server answers back + +with NTLMRandomPatcher(): + _msgs, sig = server.GSS_WrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +re_encrypted = _msgs[1].data +assert bytes(re_encrypted) == b'\x8f@s\x9c\xa5[\xd4\xee\xb6\x9b,\x96\xe6\x94\x8e\x8d\x1565\x81\xd0E\xe9WI\xd0\\\x80\x9fD\x1f\xee\xfb\xe5\xc6s\x0c+\t\xba,\xf1\xa2Zj\xd6\x0e\xe4C\x02' +assert sig.Checksum == b'\x11l/\xeaO\xb8\x08z' + +decrypted = client.GSS_UnwrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=re_encrypted), + ], + sig +)[1].data +assert decrypted == data + += GSS_WrapEx/GSS_UnwrapEx: client continues with seqnum 2 + +with NTLMRandomPatcher(): + _msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] + ) + +encrypted = _msgs[1].data +assert bytes(encrypted) == b'\x96\xc2\xa8>\xa8\xc0\xb8\xc6\xb6\x8a\xe3\xc2\x84\x8a\xd4e\xeb?"s\xf9\x1drfC\xb9\xbe\xe8\x1e9\xfe\xa1\xa8^\xbe\x0e\x98\xb3]\xa0\x906\xf6`\xdfn\x88d_L' +assert sig.Checksum == b'\xc5t\xfa\xba\x1c\x9d-\xa1' + +assert sig.SeqNum == 2 +decrypted = server.GSS_UnwrapEx( + srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig +)[1].data +assert decrypted == data + += GSS_WrapEx/GSS_UnwrapEx: inject fault + +_msgs, sig = client.GSS_WrapEx( + clicontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=data) + ] +) +encrypted = _msgs[1].data +assert encrypted != data +bad_data_header = data_header[:-3] + b"hey" +try: + server.GSS_UnwrapEx(srvcontext, + [ + SSP.WRAP_MSG(conf_req_flag=False, sign=True, data=bad_data_header), + SSP.WRAP_MSG(conf_req_flag=True, sign=True, data=encrypted), + ], + sig + ) + assert False, "No error was reported, but there should have been one" +except ValueError: + pass + + ++ GSSAPI - Verify real exchange + += Real exchange - Parse token 0 from server + +from scapy.layers.gssapi import GSSAPI_BLOB + +tok0 = GSSAPI_BLOB( +b"\x60\x76\x06\x06\x2b\x06\x01\x05\x05\x02\xa0\x6c\x30\x6a\xa0\x3c" \ +b"\x30\x3a\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x1e\x06\x09" \ +b"\x2a\x86\x48\x82\xf7\x12\x01\x02\x02\x06\x09\x2a\x86\x48\x86\xf7" \ +b"\x12\x01\x02\x02\x06\x0a\x2a\x86\x48\x86\xf7\x12\x01\x02\x02\x03" \ +b"\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a\xa3\x2a\x30\x28" \ +b"\xa0\x26\x1b\x24\x6e\x6f\x74\x5f\x64\x65\x66\x69\x6e\x65\x64\x5f" \ +b"\x69\x6e\x5f\x52\x46\x43\x34\x31\x37\x38\x40\x70\x6c\x65\x61\x73" \ +b"\x65\x5f\x69\x67\x6e\x6f\x72\x65") + += Real exchange - Create server SPNEGOSSP + +from scapy.layers.ntlm import NTLM_NEGOTIATE, MD4le +from scapy.layers.spnego import SPNEGOSSP + +server = SPNEGOSSP( + [ + NTLMSSP( + IDENTITIES={ + "User1": MD4le("Password1!"), + }, + ), + ], + force_supported_mechtypes=tok0.innerToken.token.mechTypes +) + += Real exchange - Parse token 1 from client + +tok1 = GSSAPI_BLOB( +b"\x60\x48\x06\x06\x2b\x06\x01\x05\x05\x02\xa0\x3e\x30\x3c\xa0\x0e" \ +b"\x30\x0c\x06\x0a\x2b\x06\x01\x04\x01\x82\x37\x02\x02\x0a\xa2\x2a" \ +b"\x04\x28\x4e\x54\x4c\x4d\x53\x53\x50\x00\x01\x00\x00\x00\x97\x82" \ +b"\x08\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x00\x00\x0a\x00\x61\x4a\x00\x00\x00\x0f") + +srvcontext, _, negResult = server.GSS_Accept_sec_context(None, tok1) +assert negResult == 1 + += Real exchange - Inject token 2 from server + +tok2 = GSSAPI_BLOB( +b"\xa1\x81\xca\x30\x81\xc7\xa0\x03\x0a\x01\x01\xa1\x0c\x06\x0a\x2b" \ +b"\x06\x01\x04\x01\x82\x37\x02\x02\x0a\xa2\x81\xb1\x04\x81\xae\x4e" \ +b"\x54\x4c\x4d\x53\x53\x50\x00\x02\x00\x00\x00\x0c\x00\x0c\x00\x38" \ +b"\x00\x00\x00\x15\x82\x89\xe2\xdd\x92\xcd\x56\xcf\x74\xc6\x03\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x6a\x00\x6a\x00\x44\x00\x00\x00\x0a" \ +b"\x00\x63\x45\x00\x00\x00\x0f\x44\x00\x4f\x00\x4d\x00\x41\x00\x49" \ +b"\x00\x4e\x00\x02\x00\x0c\x00\x44\x00\x4f\x00\x4d\x00\x41\x00\x49" \ +b"\x00\x4e\x00\x01\x00\x06\x00\x44\x00\x43\x00\x31\x00\x04\x00\x18" \ +b"\x00\x64\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\x2e\x00\x6c" \ +b"\x00\x6f\x00\x63\x00\x61\x00\x6c\x00\x03\x00\x20\x00\x44\x00\x43" \ +b"\x00\x31\x00\x2e\x00\x64\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e" \ +b"\x00\x2e\x00\x6c\x00\x6f\x00\x63\x00\x61\x00\x6c\x00\x07\x00\x08" \ +b"\x00\x02\xea\x8e\xe8\xd2\x8d\xd9\x01\x00\x00\x00\x00") + +tok2.token.responseToken.value.show() + +# Inject challenge token +srvcontext.sub_context.chall_tok = tok2.token.responseToken.value + += Real exchange - Parse token 3 from client + +tok3 = GSSAPI_BLOB( +b"\xa1\x82\x01\xd7\x30\x82\x01\xd3\xa0\x03\x0a\x01\x01\xa2\x82\x01" \ +b"\xb6\x04\x82\x01\xb2\x4e\x54\x4c\x4d\x53\x53\x50\x00\x03\x00\x00" \ +b"\x00\x18\x00\x18\x00\x78\x00\x00\x00\x12\x01\x12\x01\x90\x00\x00" \ +b"\x00\x0c\x00\x0c\x00\x58\x00\x00\x00\x0a\x00\x0a\x00\x64\x00\x00" \ +b"\x00\x0a\x00\x0a\x00\x6e\x00\x00\x00\x10\x00\x10\x00\xa2\x01\x00" \ +b"\x00\x15\x82\x88\xe2\x0a\x00\x61\x4a\x00\x00\x00\x0f\x6c\xf5\x94" \ +b"\xd3\x4b\x59\x37\x72\x4a\x63\xe0\xb8\xf1\x2e\xf7\x39\x44\x00\x4f" \ +b"\x00\x4d\x00\x41\x00\x49\x00\x4e\x00\x55\x00\x73\x00\x65\x00\x72" \ +b"\x00\x31\x00\x57\x00\x49\x00\x4e\x00\x31\x00\x30\x00\x00\x00\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x00\x00\x00\x00\x00\xd7\x44\x98\xd1\xdf\xdf\xd0\x5f\xaf\x33\xbe" \ +b"\x69\x12\xdf\x7f\x6d\x01\x01\x00\x00\x00\x00\x00\x00\x02\xea\x8e" \ +b"\xe8\xd2\x8d\xd9\x01\x24\x0a\x3b\xc1\x49\x92\xcc\x1e\x00\x00\x00" \ +b"\x00\x02\x00\x0c\x00\x44\x00\x4f\x00\x4d\x00\x41\x00\x49\x00\x4e" \ +b"\x00\x01\x00\x06\x00\x44\x00\x43\x00\x31\x00\x04\x00\x18\x00\x64" \ +b"\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\x2e\x00\x6c\x00\x6f" \ +b"\x00\x63\x00\x61\x00\x6c\x00\x03\x00\x20\x00\x44\x00\x43\x00\x31" \ +b"\x00\x2e\x00\x64\x00\x6f\x00\x6d\x00\x61\x00\x69\x00\x6e\x00\x2e" \ +b"\x00\x6c\x00\x6f\x00\x63\x00\x61\x00\x6c\x00\x07\x00\x08\x00\x02" \ +b"\xea\x8e\xe8\xd2\x8d\xd9\x01\x06\x00\x04\x00\x02\x00\x00\x00\x08" \ +b"\x00\x30\x00\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x20\x00\x00\xc5\xb6\xc9\x62\xcc\x25\x74\x2d\xc9\x64\xc0\xcb\x01" \ +b"\xe8\xae\x03\x12\x56\xa9\xfa\x84\xcb\x37\xcd\xa6\xae\x6e\x5b\xe2" \ +b"\x16\x52\xbb\x0a\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x09\x00\x24\x00\x63\x00\x69\x00\x66" \ +b"\x00\x73\x00\x2f\x00\x31\x00\x39\x00\x32\x00\x2e\x00\x31\x00\x36" \ +b"\x00\x38\x00\x2e\x00\x30\x00\x2e\x00\x31\x00\x30\x00\x30\x00\x00" \ +b"\x00\x00\x00\x00\x00\x00\x00\x2a\xdf\x42\x60\xc7\x4b\xac\x30\xa0" \ +b"\x47\xdc\xcd\xb5\x5e\x13\x62\xa3\x12\x04\x10\x01\x00\x00\x00\x0f" \ +b"\x96\x54\xbb\x55\xd0\x6c\xcb\x00\x00\x00\x00") + +# Parse auth +srvcontext, tok, negResult = server.GSS_Accept_sec_context(srvcontext, tok3) +assert negResult == 0 + += Real exchange - Check mechListMIC against token 4 from server + +tok4 = GSSAPI_BLOB( +b"\xa1\x1b\x30\x19\xa0\x03\x0a\x01\x00\xa3\x12\x04\x10\x01\x00\x00" \ +b"\x00\xe3\x39\x61\x56\xbc\x42\x23\xdc\x00\x00\x00\x00") + +tok.show() +tok4.show() +assert tok.token.mechListMIC == tok4.token.mechListMIC + += MISC - Dissect legacy formed NTLM messages + +# NTLM Negotiate with missing everything + +data = b'NTLMSSP\x00\x01\x00\x00\x00\x05\x02\x88\xa0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + +pkt = NTLM_Header(data) +assert pkt.WorkstationNameLen == 0 +assert pkt.ProductMajorVersion is None + +pkt.clear_cache() +assert bytes(pkt) == data + + +# NTLM AUTH with missing version + +data = b'NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00d\x00\x00\x00\xb6\x00\xb6\x00|\x00\x00\x00\x08\x00\x08\x00@\x00\x00\x00\x10\x00\x10\x00H\x00\x00\x00\x0c\x00\x0c\x00X\x00\x00\x00\x00\x00\x00\x002\x01\x00\x005\x82\x89\x00C\x00O\x00U\x00S\x00B\x00A\x00N\x00A\x00N\x00A\x00N\x00A\x00G\x00O\x00U\x00R\x00D\x00E\x00\x91\xe9\xa2\xd8\xefE\xcd!2\xe8r\xae\x17*\xbfq\xbe8\x0b4\x90\x98\x12\x00s\x9e\x9e\xdc\nj(q\x1f\x84\xf8\xd3\x90e\xa7\xb3\x01\x01\x00\x00\x00\x00\x00\x00\x80\x8ax\xeeXc\xda\x01\xbe8\x0b4\x90\x98\x12W\x00\x00\x00\x00\x01\x00\x06\x00S\x00R\x00V\x00\x02\x00\x0c\x00D\x00O\x00M\x00A\x00I\x00N\x00\x03\x00 \x00s\x00r\x00v\x00.\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x04\x00\x18\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x05\x00\x18\x00d\x00o\x00m\x00a\x00i\x00n\x00.\x00l\x00o\x00c\x00a\x00l\x00\x07\x00\x08\x00\x90\xa8;}Qc\xda\x01\x00\x00\x00\x00\x00\x00\x00\x00' + +pkt = NTLM_Header(data) +assert pkt.Workstation == "GOURDE" +assert pkt.DomainName == "COUS" +assert pkt.UserName == "BANANANA" + +pkt.clear_cache() +assert bytes(pkt) == data diff --git a/test/scapy/layers/ntp.uts b/test/scapy/layers/ntp.uts index 255415ba695..120e841200d 100644 --- a/test/scapy/layers/ntp.uts +++ b/test/scapy/layers/ntp.uts @@ -24,16 +24,24 @@ assert NTPHeader in p assert not NTPControl in p assert not NTPPrivate in p assert NTP in p +assert p.mysummary() == "NTP v4, client" +ls(p) + p = NTPControl() assert not NTPHeader in p assert NTPControl in p assert not NTPPrivate in p assert NTP in p +assert p.mysummary() == "NTP v2, NTP control message" +ls(p) + p = NTPPrivate() assert not NTPHeader in p assert not NTPControl in p assert NTPPrivate in p assert NTP in p +assert p.mysummary() == "NTP v2, reserved for private use" +ls(p) = NTP - Layers (2) p = NTPHeader() @@ -121,7 +129,7 @@ assert p.status == 0 assert p.association_id == 0 assert p.offset == 0 assert p.count == 0 -assert p.data == b'' +assert p.data == b"" = NTP Control (mode 6) - CTL_OP_READSTAT (2) - response @@ -135,26 +143,41 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 1 assert p.sequence == 12 -assert isinstance(p.status_word, NTPSystemStatusPacket) -assert p.status_word.leap_indicator == 0 -assert p.status_word.clock_source == 6 -assert p.status_word.system_event_counter == 6 -assert p.status_word.system_event_code == 4 +assert isinstance(p.status, NTPSystemStatusPacket) +assert p.status.leap_indicator == 0 +assert p.status.clock_source == 6 +assert p.status.system_event_counter == 6 +assert p.status.system_event_code == 4 assert p.association_id == 0 assert p.offset == 0 assert p.count == 4 -assert isinstance(p.data, NTPPeerStatusDataPacket) -assert p.data.association_id == 58876 -assert isinstance(p.data.peer_status, NTPPeerStatusPacket) -assert p.data.peer_status.configured == 1 -assert p.data.peer_status.auth_enabled == 1 -assert p.data.peer_status.authentic == 1 -assert p.data.peer_status.reachability == 1 -assert p.data.peer_status.reserved == 0 -assert p.data.peer_status.peer_sel == 6 -assert p.data.peer_status.peer_event_counter == 2 -assert p.data.peer_status.peer_event_code == 4 - +assert isinstance(p.data[0], NTPPeerStatusDataPacket) +assert p.data[0].association_id == 58876 +assert isinstance(p.data[0].peer_status, NTPPeerStatusPacket) +assert p.data[0].peer_status.configured == 1 +assert p.data[0].peer_status.auth_enabled == 1 +assert p.data[0].peer_status.authentic == 1 +assert p.data[0].peer_status.reachability == 1 +assert p.data[0].peer_status.reserved == 0 +assert p.data[0].peer_status.peer_sel == 6 +assert p.data[0].peer_status.peer_event_counter == 2 +assert p.data[0].peer_status.peer_event_code == 4 + += NTP Control (mode 6) - CTL_OP_READSTAT (3) - multi +s = b'\x16\x81\x00\x0f\x00\x14\x00\x00\x00\x00\x008Et\x00\x11Es\x00\x11Er\x00\x11Eq\x00\x11Ep6\x1aEo4\x14En3\x14Em4\x14El4\x1aEk4\x14Ej\x88\x11Ei\x88\x11Eh\x88\x11Eg\x88\x11' +p = NTP(s) +assert isinstance(p, NTPControl) +assert p.version == 2 +assert p.response == 1 +assert isinstance(p.status, NTPSystemStatusPacket) +assert p.count == 56 +assert len(p.data) == 14 +assert all(isinstance(x, NTPPeerStatusDataPacket) for x in p.data) +assert p.data[0].association_id == 17780 +assert p.data[10].association_id == 17770 +assert p.data[13].association_id == 17767 +assert p.data[13].peer_status.peer_event_counter == 1 +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_READVAR (1) - request s = b'\x16\x02\x00\x12\x00\x00\xfc\x8f\x00\x00\x00\x00' @@ -167,7 +190,7 @@ assert p.op_code == 2 assert p.sequence == 18 assert p.status == 0 assert p.association_id == 64655 -assert p.data == b'' +assert p.data == b"" = NTP Control (mode 6) - CTL_OP_READVAR (2) - response (1st packet) @@ -181,22 +204,22 @@ assert p.err == 0 assert p.more == 1 assert p.op_code == 2 assert p.sequence == 18 -assert isinstance(p.status_word, NTPPeerStatusPacket) -assert p.status_word.configured == 1 -assert p.status_word.auth_enabled == 1 -assert p.status_word.authentic == 0 -assert p.status_word.reachability == 0 -assert p.status_word.peer_sel == 0 -assert p.status_word.peer_event_counter == 1 -assert p.status_word.peer_event_code == 1 +assert isinstance(p.status, NTPPeerStatusPacket) +assert p.status.configured == 1 +assert p.status.auth_enabled == 1 +assert p.status.authentic == 0 +assert p.status.reachability == 0 +assert p.status.peer_sel == 0 +assert p.status.peer_event_counter == 1 +assert p.status.peer_event_code == 1 assert p.association_id == 64655 assert p.offset == 0 assert p.count == 468 -assert p.data.load == b'srcadr=192.168.122.1, srcport=123, dstadr=192.168.122.100, dstport=123,\r\nleap=3, stratum=16, precision=-24, rootdelay=0.000, rootdisp=0.000,\r\nrefid=INIT, reftime=0x00000000.00000000, rec=0x00000000.00000000,\r\nreach=0x0, unreach=5, hmode=1, pmode=0, hpoll=6, ppoll=10, headway=62,\r\nflash=0x1200, keyid=1, offset=0.000, delay=0.000, dispersion=15937.500,\r\njitter=0.000, xleave=0.240,\r\nfiltdelay= 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00,\r\nfiltoffset= 0.00 0.00 0.00 0.00 ' +assert p.data == b'srcadr=192.168.122.1, srcport=123, dstadr=192.168.122.100, dstport=123,\r\nleap=3, stratum=16, precision=-24, rootdelay=0.000, rootdisp=0.000,\r\nrefid=INIT, reftime=0x00000000.00000000, rec=0x00000000.00000000,\r\nreach=0x0, unreach=5, hmode=1, pmode=0, hpoll=6, ppoll=10, headway=62,\r\nflash=0x1200, keyid=1, offset=0.000, delay=0.000, dispersion=15937.500,\r\njitter=0.000, xleave=0.240,\r\nfiltdelay= 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00,\r\nfiltoffset= 0.00 0.00 0.00 0.00 ' = NTP Control (mode 6) - CTL_OP_READVAR (3) - response (2nd packet) -s = b'\xd6\x82\x00\x12\xc0\x11\xfc\x8f\x01\xd4\x00i0.00 0.00 0.00 0.00,\r\nfiltdisp= 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00\r\n\x00\x00\x00' +s = b'\xd6\x82\x00\x12\xc0\x11\xfc\x8f\x01\xd4\x00i0.00 0.00 0.00 0.00,\r\nfiltdisp= 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00\r\n' p = NTP(s) assert isinstance(p, NTPControl) assert p.version == 2 @@ -206,11 +229,11 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 2 assert p.sequence == 18 -assert isinstance(p.status_word, NTPPeerStatusPacket) +assert isinstance(p.status, NTPPeerStatusPacket) assert p.association_id == 64655 assert p.offset == 468 assert p.count == 105 -assert p.data.load == b'0.00 0.00 0.00 0.00,\r\nfiltdisp= 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00\r\n\x00\x00\x00' +assert p.data == b'0.00 0.00 0.00 0.00,\r\nfiltdisp= 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00 16000.00\r\n' = NTP Control (mode 6) - CTL_OP_READVAR (4) - request @@ -223,7 +246,7 @@ assert p.response == 0 assert p.err == 0 assert p.more == 0 assert p.op_code == 2 -assert len(p.data.load) == 12 +assert p.data == b"test1,test2" assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'3dc23bc7edb9555339d68908c8afa612' @@ -238,7 +261,7 @@ assert p.response == 1 assert p.err == 1 assert p.more == 0 assert p.op_code == 2 -assert len(p.data.load) == 0 +assert not p.data assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'97280249dba07338ed722860db4a580a' @@ -253,7 +276,7 @@ assert p.response == 0 assert p.err == 0 assert p.more == 0 assert p.op_code == 3 -assert len(p.data.load) == 12 +assert p.data == b"test1,test2" assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'aff10cb4c9946dfc4d90094aa170944a' @@ -268,10 +291,10 @@ assert p.response == 1 assert p.err == 1 assert p.more == 0 assert p.op_code == 3 -assert hasattr(p, 'status_word') -assert isinstance(p.status_word, NTPErrorStatusPacket) -assert p.status_word.error_code == 5 -assert len(p.data.load) == 0 +assert hasattr(p, 'status') +assert isinstance(p.status, NTPErrorStatusPacket) +assert p.status.error_code == 5 +assert not p.data assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'807a80fbafc470679853a8e57865811c' @@ -287,7 +310,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 8 assert p.count == 12 -assert p.data.load == b'controlkey 1' +assert p.data == b'controlkey 1' assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'eaa7aca81b6a9cdb58e1530d36fbefa4' @@ -303,7 +326,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 8 assert p.count == 18 -assert p.data.load == b'Config Succeeded\r\n\x00\x00' +assert p.data == b'Config Succeeded\r\n' assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'bfa6d85ff96d1e326c293caceec2a539' @@ -319,7 +342,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 9 assert p.count == 15 -assert p.data.load == b'ntp.test.2.conf\x00' +assert p.data == b'ntp.test.2.conf' assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'c9fb8abe3c605ffa36d218c3b7648923' @@ -335,7 +358,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 9 assert p.count == 42 -assert p.data.load == b"Configuration saved to 'ntp.test.2.conf'\r\n\x00\x00" +assert p.data == b"Configuration saved to 'ntp.test.2.conf'\r\n" assert p.authenticator.key_id == 1 assert bytes_hex(p.authenticator.dgst) == b'32c2ba59c533fe28f550e5a0860295d9' @@ -351,7 +374,7 @@ assert p.err == 0 assert p.more == 0 assert p.op_code == 12 assert p.data == b'' -assert p.authenticator == b'' +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_REQ_NONCE (2) - response @@ -364,8 +387,8 @@ assert p.response == 1 assert p.err == 0 assert p.more == 0 assert p.op_code == 12 -assert p.data.load == b'nonce=db4186a2e1d9022472e24bc9\r\n' -assert p.authenticator == b'' +assert p.data == b'nonce=db4186a2e1d9022472e24bc9\r\n' +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_READ_MRU (1) - request @@ -378,8 +401,8 @@ assert p.response == 0 assert p.err == 0 assert p.op_code == 10 assert p.count == 40 -assert p.data.load == b'nonce=db4186a2e1d9022472e24bc9, frags=32' -assert p.authenticator == b'' +assert p.data == b'nonce=db4186a2e1d9022472e24bc9, frags=32' +assert not p.authenticator = NTP Control (mode 6) - CTL_OP_READ_MRU (2) - response s = b'\xd6\x8a\x00\x08\x00\x00\x00\x00\x00\x00\x00\xe9nonce=db4186a2e2073198b93c6419, addr.0=192.168.122.100:123,\r\nfirst.0=0xdb418673.323e1a89, last.0=0xdb418673.323e1a89, ct.0=1,\r\nmv.0=36, rs.0=0x0, WWQ.0=18446744073709509383, now=0xdb4186a2.e20ff8f4,\r\nlast.newest=0xdb418673.323e1a89\r\n\x00\x00\x00' @@ -391,9 +414,8 @@ assert p.response == 1 assert p.err == 0 assert p.op_code == 10 assert p.count == 233 -assert p.data.load == b'nonce=db4186a2e2073198b93c6419, addr.0=192.168.122.100:123,\r\nfirst.0=0xdb418673.323e1a89, last.0=0xdb418673.323e1a89, ct.0=1,\r\nmv.0=36, rs.0=0x0, WWQ.0=18446744073709509383, now=0xdb4186a2.e20ff8f4,\r\nlast.newest=0xdb418673.323e1a89\r\n\x00\x00\x00' -assert p.authenticator == b'' - +assert p.data == b'nonce=db4186a2e2073198b93c6419, addr.0=192.168.122.100:123,\r\nfirst.0=0xdb418673.323e1a89, last.0=0xdb418673.323e1a89, ct.0=1,\r\nmv.0=36, rs.0=0x0, WWQ.0=18446744073709509383, now=0xdb4186a2.e20ff8f4,\r\nlast.newest=0xdb418673.323e1a89\r\n' +assert not p.authenticator ############ ############ @@ -463,6 +485,7 @@ assert p.version == 2 assert p.mode == 7 assert p.request_code == 2 assert isinstance(p.data[0], NTPInfoPeer) +repr(p.data[0]) assert p.data[0].dstaddr == "192.168.122.102" assert p.data[0].srcaddr == "192.168.122.101" assert p.data[0].srcport == 123 @@ -573,7 +596,7 @@ assert p.data[0].peer == "127.127.1.0" assert p.data[0].peer_mode == 3 assert p.data[0].leap == 0 assert p.data[0].stratum == 11 -assert p.data[0].precision == 240 +assert p.data[0].precision == -16 assert p.data[0].refid == "127.127.1.0" @@ -1104,7 +1127,7 @@ assert p.data[0].ifname.startswith(b"lo") from decimal import Decimal -precision = b"\xec" # 236 +precision = b"\xec" # -20 dispersion = b"\x00\x00\xf2\xce" # 0.948455810546875 time_stamp = b"\xe6}gt\x00\x00\x00\x00" # Sat, 16 Jul 2022 16:36:04 +0000 @@ -1115,26 +1138,33 @@ pkt_1 = NTP( sent=RawVal(time_stamp), ) +# This field is intentionally set here, rather than in the constructor above, +# to cover a different code path: +pkt_1.recv = RawVal(time_stamp) + assert (isinstance(pkt_1.precision, RawVal)), type(pkt_1.precision) assert (isinstance(pkt_1.dispersion, RawVal)), type(pkt_1.dispersion) assert (isinstance(pkt_1.orig, RawVal)), type(pkt_1.orig) assert (isinstance(pkt_1.sent, RawVal)), type(pkt_1.sent) +assert (isinstance(pkt_1.recv, RawVal)), type(pkt_1.recv) -assert (pkt_1.precision.val == precision, pkt_1.precision.val) -assert (pkt_1.dispersion.val == dispersion, pkt_1.dispersion.val) -assert (pkt_1.orig.val == time_stamp, pkt_1.orig.val) -assert (pkt_1.sent.val == time_stamp, pkt_1.sent.val) +assert pkt_1.precision.val == precision, pkt_1.precision.val +assert pkt_1.dispersion.val == dispersion, pkt_1.dispersion.val +assert pkt_1.orig.val == time_stamp, pkt_1.orig.val +assert pkt_1.sent.val == time_stamp, pkt_1.sent.val +assert pkt_1.recv.val == time_stamp, pkt_1.recv.val time_stamp_hex = 0x00000000e67d6774 pkt_2 = NTP( - precision=236, + precision=-20, dispersion=Decimal('0.948455810546875'), orig=time_stamp_hex, - sent=time_stamp_hex + sent=time_stamp_hex, + recv=time_stamp_hex ) raw_pkt = (b"#\x02\n\xec\x00\x00\x00\x00\x00\x00\xf2\xce\x7f\x00\x00\x01\x00" - b"\x00\x00\x00\x00\x00\x00\x00\xe6}gt\x00\x00\x00\x00\x00\x00\x00" - b"\x00\x00\x00\x00\x00\xe6}gt\x00\x00\x00\x00") + b"\x00\x00\x00\x00\x00\x00\x00\xe6}gt\x00\x00\x00\x00\xe6}gt\x00" + b"\x00\x00\x00\xe6}gt\x00\x00\x00\x00") -assert raw(pkt_1) == raw(pkt_2) == raw_pkt \ No newline at end of file +assert raw(pkt_1) == raw(pkt_2) == raw_pkt diff --git a/test/scapy/layers/ppp.uts b/test/scapy/layers/ppp.uts index daa6a933a4d..1961580fa6e 100644 --- a/test/scapy/layers/ppp.uts +++ b/test/scapy/layers/ppp.uts @@ -17,11 +17,26 @@ assert PPPoED_Tags in p q=p[PPPoED_Tags] assert q.tag_list is not None r=q.tag_list +assert len(r) == 2 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 + +assert Padding in p and len(p[Padding]) == 4 + += PPPoE with tags (appended) +~ ppp ppoe +eth = Ether(dst="ff:ff:ff:ff:ff:ff", src="12:12:12:12:12:12", type=0x8863) +pppoed = PPPoED(version=1, type=1, code=0x9, sessionid=0, len=8) +server_name = PPPoETag(tag_type=0x0101, tag_len=0) +end_of_list = PPPoETag(tag_type=0, tag_len=0) + +original = eth / pppoed / server_name / end_of_list +dissected = Ether(original.build()) +assert PPPoED_Tags in dissected +assert dissected[PPPoED_Tags].tag_list[0].tag_type == 0x0101 +assert dissected[PPPoED_Tags].tag_list[1].tag_type == 0 = PPPoE with padding ~ ppp pppoe @@ -97,12 +112,24 @@ assert raw(p) == raw(q) assert q[PPP_ECP_Option].data == b"ABCDEFG" -= PPP with only one byte for protocol += PPP IP check that default protocol length is 2 bytes +~ ppp ip + +p = PPP()/IP() +p +r = raw(p) +r +assert r.startswith(b'\x00\x21') +assert len(r) == 22 + + += PPP check parsing with only one byte for protocol ~ ppp -assert len(raw(PPP() / IP())) == 21 +assert len(raw(PPP(proto=b'\x21') / 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 +assert PPP(b"\x00\x21" + raw(IP())) == PPP(b"\x21" + raw(IP())) diff --git a/test/scapy/layers/quic.uts b/test/scapy/layers/quic.uts new file mode 100644 index 00000000000..4e66957ee0a --- /dev/null +++ b/test/scapy/layers/quic.uts @@ -0,0 +1,117 @@ +% Scapy QUIC layer tests + ++ QUIC dissection / build + +% We use the examples from https://quic.xargs.org/. Big props & kudos to them ! +% FIXME TODO: THIS IS VERY INCOMPLETE. + += QUIC - Dissect Client Initial Packet + +from scapy.layers.quic import * + +pkt = QUIC(bytes.fromhex("c00000000108000102030405060705635f636964004103001c36a7ed78716be9711ba498b7ed868443bb2e0c514d4d848eadcc7a00d25ce9f9afa483978088de836be68c0b32a24595d7813ea5414a9199329a6d9f7f760dd8bb249bf3f53d9a77fbb7b395b8d66d7879a51fe59ef9601f79998eb3568e1fdc789f640acab3858a82ef2930fa5ce14b5b9ea0bdb29f4572da85aa3def39b7efafffa074b9267070d50b5d07842e49bba3bc787ff295d6ae3b514305f102afe5a047b3fb4c99eb92a274d244d60492c0e2e6e212cef0f9e3f62efd0955e71c768aa6bb3cd80bbb3755c8b7ebee32712f40f2245119487021b4b84e1565e3ca31967ac8604d4032170dec280aeefa095d08b3b7241ef6646a6c86e5c62ce08be099")) +assert QUIC_Initial in pkt +assert pkt.LongPacketType == 0 +assert pkt.DstConnID == b"\x00\x01\x02\x03\x04\x05\x06\x07" +assert pkt.SrcConnID == b"c_cid" +assert pkt.Length == 259 +assert len(pkt.load) + 1 == 259 +assert pkt.PacketNumber == 0 + += QUIC - Dissect Server Initial Packet + +from scapy.layers.quic import * + +pkt = QUIC(bytes.fromhex("c00000000105635f63696405735f63696400407500836855d5d9c823d07c616882ca770279249864b556e51632257e2d8ab1fd0dc04b18b9203fb919d8ef5a33f378a627db674d3c7fce6ca5bb3e8cf90109cbb955665fc1a4b93d05f6eb83252f6631bcadc7402c10f65c52ed15b4429c9f64d84d64fa406cf0b517a926d62a54a9294136b143b033")) +assert QUIC_Initial in pkt +assert pkt.LongPacketType == 0 +assert pkt.DstConnID == b"c_cid" +assert pkt.SrcConnID == b"s_cid" +assert pkt.Length == 117 +assert len(pkt.load) + 1 == 117 +assert pkt.PacketNumber == 0 + += QUIC - Dissect Server Handshake Packet + +from scapy.layers.quic import * + +pkt = QUIC(bytes.fromhex("e00000000105635f63696405735f63696440cf014420f919681c3f0f102a30f5e647a3399abf54bc8e80453134996ba33099056242f3b8e662bbfce42f3ef2b6ba87159147489f8479e849284e983fd905320a62fc7d67e9587797096ca60101d0b2685d8747811178133ad9172b7ff8ea83fd81a814bae27b953a97d57ebff4b4710dba8df82a6b49d7d7fa3d8179cbdb8683d4bfa832645401e5a56a76535f71c6fb3e616c241bb1f43bc147c296f591402997ed49aa0c55e31721d03e14114af2dc458ae03944de5126fe08d66a6ef3ba2ed1025f98fea6d6024998184687dc06")) +assert QUIC_Handshake in pkt +assert pkt.LongPacketType == 2 +assert pkt.DstConnID == b"c_cid" +assert pkt.SrcConnID == b"s_cid" +assert pkt.PacketNumber == 1 + += QUIC - QuicPacketNumberField / QuicPacketNumberBitFieldLenField - variable lengths + +from scapy.layers.quic import * + +pkt = QUIC_Initial(DstConnID=b'p\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR', SrcConnID=b'\xf7\x10Q', PacketNumber=0xFF) +assert bytes(pkt) == b'\xc0\x00\x00\x00\x01\x0fp\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR\x03\xf7\x10Q\x00\x00\xff' +pkt = QUIC_Initial(bytes(pkt)) +assert pkt.DstConnIDLen == 15 +assert pkt.SrcConnIDLen == 3 +assert pkt.PacketNumberLen == 0 +assert pkt.PacketNumber == 0xFF + +pkt = QUIC_Initial(DstConnID=b'p\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR', SrcConnID=b'\xf7\x10Q', PacketNumber=0xFFFF) +assert bytes(pkt) == b'\xc1\x00\x00\x00\x01\x0fp\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR\x03\xf7\x10Q\x00\x00\xff\xff' +pkt = QUIC_Initial(bytes(pkt)) +assert pkt.PacketNumberLen == 1 +assert pkt.PacketNumber == 0xFFFF + +pkt = QUIC_Initial(DstConnID=b'p\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR', SrcConnID=b'\xf7\x10Q', PacketNumber=0xFFFFFF) +assert bytes(pkt) == b'\xc2\x00\x00\x00\x01\x0fp\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR\x03\xf7\x10Q\x00\x00\xff\xff\xff' +pkt = QUIC_Initial(bytes(pkt)) +assert pkt.PacketNumberLen == 2 +assert pkt.PacketNumber == 0xFFFFFF + +pkt = QUIC_Initial(DstConnID=b'p\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR', SrcConnID=b'\xf7\x10Q', PacketNumber=0xFFFFFFFF) +assert bytes(pkt) == b'\xc3\x00\x00\x00\x01\x0fp\xa2\x8e@\x96\xc5}\xd0\xff\xb6\xc3\xd8\x1b\xcaR\x03\xf7\x10Q\x00\x00\xff\xff\xff\xff' +pkt = QUIC_Initial(bytes(pkt)) +assert pkt.PacketNumberLen == 3 +assert pkt.PacketNumber == 0xFFFFFFFF + += QUIC - QuicPacketNumberField / QuicPacketNumberBitFieldLenField - Out of range + +import struct +from scapy.layers.quic import * + +try: + pkt = QUIC_Initial(PacketNumber=0xFFFFFFFFFF) + bytes(pkt) + assert False, "QUIC Packet Number length should fail" +except struct.error: + pass + += QUIC - QuicVarIntField - variable lengths + +from scapy.layers.quic import * + +pkt = QUIC_Initial(Length=1) +assert bytes(pkt) == b'\xc0\x00\x00\x00\x01\x00\x00\x00\x01\x00' +assert QUIC_Initial(bytes(pkt)).Length == 1 + +pkt = QUIC_Initial(Length=1 << 9) +assert bytes(pkt) == b'\xc0\x00\x00\x00\x01\x00\x00\x00B\x00\x00' +assert QUIC_Initial(bytes(pkt)).Length == 1 << 9 + +pkt = QUIC_Initial(Length=1 << 17) +assert bytes(pkt) == b'\xc0\x00\x00\x00\x01\x00\x00\x00\x80\x02\x00\x00\x00' +assert QUIC_Initial(bytes(pkt)).Length == 1 << 17 + +pkt = QUIC_Initial(Length=4611686018427387903) +assert bytes(pkt) == b'\xc0\x00\x00\x00\x01\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\x00' +assert QUIC_Initial(bytes(pkt)).Length == 4611686018427387903 + += QUIC - QuicVarIntField - Out of range + +import struct +from scapy.layers.quic import * + +try: + pkt = QUIC_Initial(Length=0xFFFFFFFFFFFFFFFF) + bytes(pkt) + assert False, "QUIC Variable length should fail" +except struct.error: + pass diff --git a/test/scapy/layers/radius.uts b/test/scapy/layers/radius.uts index 7a428b3621c..7a39522af61 100644 --- a/test/scapy/layers/radius.uts +++ b/test/scapy/layers/radius.uts @@ -6,7 +6,7 @@ + RADIUS tests = IP/UDP/RADIUS - Build -s = raw(IP()/UDP(sport=1812)/Radius(authenticator="scapy")/RadiusAttribute(value="scapy")) +s = raw(IP(src="127.0.0.1", dst="127.0.0.1")/UDP(sport=1812)/Radius(authenticator="scapy")/RadiusAttribute(value="scapy")) s == b'E\x00\x007\x00\x01\x00\x00@\x11|\xb3\x7f\x00\x00\x01\x7f\x00\x00\x01\x07\x14\x07\x14\x00#U\xb3\x01\x00\x00\x1bscapy\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x07scapy' = IP/UDP/RADIUS - Dissection @@ -234,8 +234,20 @@ assert pkt.attributes[2].len == 18 assert pkt.attributes[3].type == 24 assert pkt.attributes[3].len == 18 -= RadiusAttr_User_Password += RadiusAttr_User_Password - Parse and Decrypt r = b'\x01\x00\x00\x1c0x10x20x30x40x50\x02\x08geheim' p = Radius(r) assert isinstance(p.attributes[0], RadiusAttr_User_Password) + +p = 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\xa7V\xba\xf6\xf0LG') +assert pkt.summary() == "Ether / IP / UDP / RADIUS Access-Request (User:'user' MS-CHAP2)" + +pkt = 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&r0<\xa0\x0e0\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2*\x04(NTLMSSP\x00\x01\x00\x00\x00\x97\x82\x08\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\n\x00aJ\x00\x00\x00\x0f') -assert isinstance(setup_sess.Buffer[0][1].innerContextToken.token.mechToken.value, NTLM_NEGOTIATE) -assert setup_sess.Buffer[0][1].innerContextToken.token.mechToken.value.ProductBuild == 19041 +assert isinstance(setup_sess.Buffer[0][1].innerToken.token.mechToken.value, NTLM_NEGOTIATE) +assert setup_sess.Buffer[0][1].innerToken.token.mechToken.value.ProductBuild == 19041 = Setup Session Response @@ -378,7 +391,7 @@ assert setup_sess.Buffer[0][1].token.responseToken.value.Payload[0] == ('TargetN assert setup_sess.Buffer[0][1].token.responseToken.value.Payload[1][1][-1].AvId == 0 -= SMB2 IOCTL Request += SMB2 IOCTL Request - Validate negotiate info ioctl_req = Ether(b'RT\x00\xb6[=\x16\xb4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x009\x00\x00\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x89\x00\x12\x00\x00\x00\x00\x00\x07\x00\x00\x00\x01\x00\x00\x00d\x00\x00\x00x\x00\x16\x00\x90\x00\x00\x00\xb4\x00\x00\x00d\x00e\x00s\x00k\x00t\x00o\x00p\x00.\x00i\x00n\x00i\x00\x00\x008\x00\x00\x00\x10\x00\x04\x00\x00\x00\x18\x00 \x00\x00\x00DH2Q\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x000\x1d\xb3\xc8\xfa\r\xed\x11\xb7R\x808\xfb\xd6\xa0~\x18\x00\x00\x00\x10\x00\x04\x00\x00\x00\x18\x00\x00\x00\x00\x00MxAc\x00\x00\x00\x00\x18\x00\x00\x00\x10\x00\x04\x00\x00\x00\x18\x00\x00\x00\x00\x00QFid\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x04\x00\x00\x00\x18\x004\x00\x00\x00RqLs\x00\x00\x00\x00\xc8\x9bA\xdb\x8e\xd1\x19\xf4\\;\x846;\xf6\xca\xe0\x07\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') @@ -411,3 +523,57 @@ assert sess_create_context_response.CreateContexts[0].Data.QueryStatus == 0 assert sess_create_context_response.CreateContexts[1].Data.DiskFileId == 2359297 assert sess_create_context_response.CreateContexts[1].Data.Reserved == b'\x00' * 16 += SMB2 Query Info Response with Security Descriptor + +qr = SMB2_Query_Info_Response(b'\t\x00H\x00\xe0\x00\x00\x00\x01\x00\x14\x9c\x14\x00\x00\x004\x00\x00\x00\x00\x00\x00\x00T\x00\x00\x00\x01\x06\x00\x00\x00\x00\x00\x05P\x00\x00\x00\xb5\x89\xfb8\x19\x84\xc2\xcb\\l#mW\x00wn\xc0\x02d\x87\x01\x06\x00\x00\x00\x00\x00\x05P\x00\x00\x00\xb5\x89\xfb8\x19\x84\xc2\xcb\\l#mW\x00wn\xc0\x02d\x87\x02\x00\x8c\x00\x06\x00\x00\x00\x00\x03\x18\x00\xa9\x00\x12\x00\x01\x02\x00\x00\x00\x00\x00\x0f\x02\x00\x00\x00\x01\x00\x00\x00\x00\x0b\x14\x00\xff\x01\x1f\x00\x01\x01\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x03\x14\x00\xff\x01\x1f\x00\x01\x01\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x03\x14\x00\xff\x01\x1f\x00\x01\x01\x00\x00\x00\x00\x00\x05\x12\x00\x00\x00\x00\x03\x18\x00\xff\x01\x1f\x00\x01\x02\x00\x00\x00\x00\x00\x05 \x00\x00\x00 \x02\x00\x00\x00\x03\x18\x00\xa9\x00\x12\x00\x01\x02\x00\x00\x00\x00\x00\x05 \x00\x00\x00!\x02\x00\x00') +sd = SECURITY_DESCRIPTOR(qr.Output) + +assert sd.OwnerSid.summary() == 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464' +assert sd.GroupSid.summary() == 'S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464' + +assert sd.DACL.toSDDL() == [ + '(A;OI+CI;;;;S-1-15-2-1)', + '(A;OI+CI+IO;;;;S-1-3-0)', + '(A;OI+CI;;;;S-1-1-0)', + '(A;OI+CI;;;;S-1-5-18)', + '(A;OI+CI;;;;S-1-5-32-544)', + '(A;OI+CI;;;;S-1-5-32-545)', +] + += SMB2 Set Info Request with Rename + +set_info = NBTSession(b'\x00\x00\x00|\xfeSMB@\x00\x01\x00#\x00\x00\x00\x11\x00\x01\x000\x00\x00\x00\x00\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x00\x00\xff\xfe\x00\x00\x01\x00\x00\x00\x15\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00!\x00\x01\n\x1c\x00\x00\x00`\x00\x00\x00\x00\x00\x00\x00\xb0\n\x9c\xfd@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xc3\x01\xc1\\\\1\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00t\x00e\x00s\x00t\x00') + +assert set_info.FileId.Persistent == 0x40fd9c0ab0 +assert isinstance(set_info.Data, FileRenameInformation) +assert set_info.Data.FileName == "test" +assert not set_info.Data.ReplaceIfExists + += SMB2 - Build and dissect SECURITY_DESCRIPTOR + +sd = SECURITY_DESCRIPTOR( + Control="DACL_PRESENT+DACL_PROTECTED+SELF_RELATIVE", + OwnerSid=WINNT_SID.fromstr("S-1-1-0"), + GroupSid=WINNT_SID.fromstr("S-1-1-0"), + DACL=WINNT_ACL( + Aces=[ + WINNT_ACE_HEADER() / WINNT_ACCESS_ALLOWED_ACE( + Mask=1, + Sid=WINNT_SID.fromstr("S-1-1-0"), + ) + ] + ) +) + +sd = SECURITY_DESCRIPTOR(bytes(sd)) + +assert sd.OwnerSidOffset == 20 +assert sd.GroupSidOffset == 32 +assert sd.SACLOffset == 0 +assert sd.DACLOffset == 44 + +assert sd.OwnerSid.summary() == "S-1-1-0" +assert sd.GroupSid.summary() == "S-1-1-0" +assert sd.DACL.toSDDL() == ['(A;;;;;S-1-1-0)'] + +assert sd.DACL.AclSize == len(sd.DACL) \ No newline at end of file diff --git a/test/scapy/layers/smbclientserver.uts b/test/scapy/layers/smbclientserver.uts new file mode 100644 index 00000000000..101843cbdc6 --- /dev/null +++ b/test/scapy/layers/smbclientserver.uts @@ -0,0 +1,482 @@ +% SMB2 Client and Server tests + ++ SMB2 Client tests +~ linux smbclient samba + += Define samba server + +import subprocess + +# Create a temporary directory to serve +TEMP_DIR = pathlib.Path(get_temp_dir()) +TEMP_DIR.chmod(0o0755) +print(TEMP_DIR) + +# Put stuff in it +SHARE_DIR = TEMP_DIR / "share" +SHARE_DIR.mkdir() +SHARE_DIR.chmod(0o0777) +(SHARE_DIR / "fileA").touch() +(SHARE_DIR / "fileB").touch() +(SHARE_DIR / "fileScapy").touch() +(SHARE_DIR / "ignoredFile").symlink_to("fileA") +(SHARE_DIR / "sub").mkdir() +(SHARE_DIR / "sub").chmod(0o0777) +(SHARE_DIR / "sub" / "secret").touch() + +# required for smb.conf to work in standalone without root.. wtf +LOGS_DIR = TEMP_DIR / "logs" +LOCK_DIR = TEMP_DIR / "lock" +PRIVATE_DIR = TEMP_DIR / "private" +PID_DIR = TEMP_DIR / "pid" +CACHE_DIR = TEMP_DIR / "cache" +STATE_DIRECTORY = TEMP_DIR / "state" +NCALRPC_DIR = TEMP_DIR / "ncalrpc" + +for dir in [LOGS_DIR, LOCK_DIR, PRIVATE_DIR, PID_DIR, CACHE_DIR, STATE_DIRECTORY, NCALRPC_DIR]: + dir.mkdir() + +SMBD_LOG = LOGS_DIR / "log.smbd" +SMBD_LOG.touch() + +# smb.conf +CONF_FILE = get_temp_file(autoext=".conf") +CONF = """ +# Scapy unit tests samba server + +[global] + workgroup = WORKGROUP + server role = standalone server + security = user + map to guest = bad user + log level = 1 smb2:5 auth:3 + + bind interfaces only = yes + interfaces = 127.0.0.0/8 + + lock directory = %s + private directory = %s + cache directory = %s + ncalrpc dir = %s + pid directory = %s + state directory = %s + +[test] + comment = Test share + path = %s + guest ok = yes + browseable = yes + read only = no + public = yes +""" % ( + LOCK_DIR, + PRIVATE_DIR, + CACHE_DIR, + NCALRPC_DIR, + PID_DIR, + STATE_DIRECTORY, + SHARE_DIR, +) + +print(CONF) + +with open(CONF_FILE, "w") as fd: + fd.write(CONF) + +# define server context manager + +class run_smbserver: + def __init__(self): + self.proc = None + + def __enter__(self): + # Empty log + with SMBD_LOG.open('w') as fd: + fd.write("") + print("@ Starting smbd server") + # Start server + self.proc = subprocess.Popen(["/usr/sbin/smbd", "-F", "-p", "12345", "-s", CONF_FILE, "-l", LOGS_DIR]) + # wait for it to start + for i in range(10): + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(1) + try: + sock.connect(("127.0.0.1", 12345)) + break + except Exception: + time.sleep(0.5) + finally: + sock.close() + else: + raise TimeoutError + print("@ Server started !") + + def __exit__(self, exc_type, exc_value, traceback): + print("@ Stopping smbd server !") + self.proc.terminate() + self.proc.wait() + if traceback: + # failed + print("\nTest failed. Smbd logs:") + with SMBD_LOG.open('r') as fd: + print(fd.read()) + print("@ smbd server stopped !") + + + +# define client + +def run_smbclient(max_dialect=0x0202): + return smbclient("localhost", "guest", port=12345, guest=True, cli=False, debug=4, MAX_DIALECT=max_dialect) + + += smbclient: SMB 2.0.2 - connect then list shares + +with run_smbserver(): + try: + cli = run_smbclient() + results = cli.shares() + print(results) + assert ('test', 'DISKTREE', 'Test share') in results + assert any(x[0] == "IPC$" for x in results) + finally: + cli.close() + += smbclient: SMB 2.0.2 - connect to test share and list files + +with run_smbserver(): + try: + cli = run_smbclient() + cli.use("test") + files = cli.ls() + names = [x[0] for x in files] + assert all(x in names for x in ['.', '..', 'sub', 'fileB', 'fileScapy', 'fileA']) + finally: + cli.close() + += smbclient: SMB 2.0.2 - connect to test share and get file + +LOCALPATH = pathlib.Path(get_temp_dir()) + +with run_smbserver(): + try: + cli = run_smbclient() + cli.use("test") + cli.lcd(str(LOCALPATH)) + completions = cli.get_complete("file") + assert all(x in completions for x in ['fileA', 'fileB']) + cli.get('fileA') + assert (LOCALPATH / "fileA").exists() + assert [x.name for x in cli.lls()] == ['fileA'] + finally: + cli.close() + += smbclient: SMB 2.0.2 - connect to test share, cd, put file and cat it + +LOCALPATH = pathlib.Path(get_temp_dir()) +with (LOCALPATH / "fileC").open("w") as fd: + fd.write("Nice\nData") + +with run_smbserver(): + try: + cli = run_smbclient() + cli.use("test") + cli.lcd(str(LOCALPATH)) + cli.cd("sub") + # upload + cli.put('fileC') + # check completion + completions = cli.get_complete("") + assert all(x in completions for x in ['secret', 'fileC']) + # cat + assert cli.cat('fileC') == b'Nice\nData' + # check on disk + with (SHARE_DIR / "sub" / "fileC").open("r") as fd: + assert fd.read() == "Nice\nData" + finally: + cli.close() + + += smbclient: SMB 2.0.2 - connect to test share and recursive get + +LOCALPATH = pathlib.Path(get_temp_dir()) + +with run_smbserver(): + try: + cli = run_smbclient() + cli.use("test") + cli.lcd(str(LOCALPATH)) + cli.get(".", r=True) + # check on disk + finally: + cli.close() + +assert (LOCALPATH / "fileA").exists() +assert (LOCALPATH / "fileB").exists() +assert (LOCALPATH / "fileScapy").exists() +assert (LOCALPATH / "sub").exists() +assert (LOCALPATH / "sub" / "secret").exists() + += smbclient: SMB 3.1.1 - connect to test share and recursive get + +LOCALPATH = pathlib.Path(get_temp_dir()) + +with run_smbserver(): + try: + cli = run_smbclient(max_dialect=0x0311) + cli.use("test") + cli.lcd(str(LOCALPATH)) + cli.get(".", r=True) + # check on disk + finally: + cli.close() + +assert (LOCALPATH / "fileA").exists() +assert (LOCALPATH / "fileB").exists() +assert (LOCALPATH / "fileScapy").exists() +assert (LOCALPATH / "sub").exists() +assert (LOCALPATH / "sub" / "secret").exists() + ++ SMB2 Server tests +~ linux smbserver samba + += Define Scapy smb server + +import subprocess +import select + +ROOTPATH = pathlib.Path(get_temp_dir()) + +# Populate with stuff +(ROOTPATH / "fileA").touch() +(ROOTPATH / "fileB").touch() +(ROOTPATH / "fileScapy").touch() +(ROOTPATH / "sub").mkdir() +(ROOTPATH / "sub" / "secret").touch() + +# content +with (ROOTPATH / "fileScapy").open("w") as fd: + fd.write("Nice\nData") + +class run_smbserver: + def __init__(self, guest=False, readonly=True, encryptshare=False, MAX_DIALECT=0x311): + self.srv = None + self.guest = guest + self.readonly = readonly + self.encryptshare = encryptshare + self.MAX_DIALECT = MAX_DIALECT + + def __enter__(self): + if self.guest: + ssp = None + else: + ssp = SPNEGOSSP([NTLMSSP(IDENTITIES={ + "User1": MD4le("Password1"), + "Administrator": MD4le("Password2") + })]) + self.srv = smbserver( + shares=[SMBShare("Scapy", ROOTPATH, encryptdata=self.encryptshare), + SMBShare("test", ROOTPATH, encryptdata=self.encryptshare)], + iface=conf.loopback_name, + debug=4, + port=12345, + bg=True, + readonly=self.readonly, + MAX_DIALECT=self.MAX_DIALECT, + ssp=ssp, + ) + + def __exit__(self, exc_type, exc_value, traceback): + self.srv.close() + + +# define client + +class run_smbclient: + def __init__(self, user=None, password=None, share=None, list=False, cwd=None, debug=None, maxversion=None, encrypt=False): + args = [ + "smbclient", + ] + (["-L"] if list else []) + [ + "//127.0.0.1%s" % (("/%s" % share) if share else ""), + "-p", "12345", + ] + if user and password: + args.extend([ + "-U", + "DOMAIN/%s" % user, + "--password", + password, + ]) + else: + args.append("-N") + if maxversion: + args.extend(["-m", maxversion]) + if encrypt: + args.extend(["--client-protection", "encrypt"]) + self.args = args + self.proc = subprocess.Popen( + args, + text=True, + bufsize=0, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cwd, + ) + self.output = "" + def cmd(self, command): + # send command + self.proc.stdin.write(command + "\n") + self.proc.stdin.flush() + def getoutput(self): + self.output += self.proc.communicate(input="exit\n", timeout=10)[0] + return [x.strip() for x in self.output.split("\n") if x.strip()] + def close(self): + if self.proc.poll(): + self.proc.terminate() + def printdebug(self): + # Print stuff + print("\nTest failed.") + print("smbclient arguments:", self.args) + print("smbclient output:") + print(self.output) + +cli = None + += smbserver: SMB 3.1.1 - connect then list shares + +with run_smbserver(guest=True): + try: + cli = run_smbclient(list=True) + output = cli.getoutput() + shares = [x[0] for x in (y.split(" ") for y in output if "Disk" in y)] + assert shares == ['Scapy', 'test'] + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.1.1 - connect then ls + +with run_smbserver(): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test") + cli.cmd("ls") + output = cli.getoutput()[1:] + files = [x[0] for x in (y.split(" ") for y in output if "blocks" not in y)] + print(files) + assert files == ['.', 'fileA', 'fileB', 'fileScapy', 'sub'] + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 2.0.2 - connect then ls + +with run_smbserver(): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", maxversion="SMB2_02") + cli.cmd("ls") + output = cli.getoutput()[1:] + files = [x[0] for x in (y.split(" ") for y in output if "blocks" not in y)] + print(files) + assert files == ['.', 'fileA', 'fileB', 'fileScapy', 'sub'] + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.1.1 - connect then get file + +LOCALPATH = pathlib.Path(get_temp_dir()) + +with run_smbserver(): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH) + cli.cmd("get fileScapy") + output = cli.getoutput() + print(output) + assert "size 9" in output[0], "no size" + assert (LOCALPATH / "fileScapy").exists(), "file doesn't exist" + with (LOCALPATH / "fileScapy").open("r") as fd: + assert fd.read() == "Nice\nData", "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.1.1 - connect then put file + +LOCALPATH = pathlib.Path(get_temp_dir()) + +nicedata = ("A" * 100 + "\n") * 5 +with open(LOCALPATH / "newCustomFile", "w") as fd: + fd.write(nicedata) + +with run_smbserver(readonly=False): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH) + cli.cmd("put newCustomFile") + output = cli.getoutput() + print(output) + assert "putting file newCustomFile" in output[0], "strange output" + assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist" + with (ROOTPATH / "newCustomFile").open("r") as fd: + assert fd.read() == nicedata, "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.0.2 - require global encryption + +LOCALPATH = pathlib.Path(get_temp_dir()) + +nicedata = ("A" * 100 + "\n") * 5 +with open(LOCALPATH / "newCustomFile", "w") as fd: + fd.write(nicedata) + +with run_smbserver(readonly=False, MAX_DIALECT=0x0302): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH, encrypt=True) + cli.cmd("put newCustomFile") + output = cli.getoutput() + print(output) + assert "putting file newCustomFile" in output[0], "strange output" + assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist" + with (ROOTPATH / "newCustomFile").open("r") as fd: + assert fd.read() == nicedata, "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close() + += smbserver: SMB 3.1.1 - require share encryption + +LOCALPATH = pathlib.Path(get_temp_dir()) + +nicedata = ("A" * 100 + "\n") * 5 +with open(LOCALPATH / "newCustomFile", "w") as fd: + fd.write(nicedata) + +with run_smbserver(readonly=False, encryptshare=True): + try: + cli = run_smbclient(user="Administrator", password="Password2", share="test", cwd=LOCALPATH) + cli.cmd("put newCustomFile") + output = cli.getoutput() + print(output) + assert "putting file newCustomFile" in output[0], "strange output" + assert (ROOTPATH / "newCustomFile").exists(), "file doesn't exist" + with (ROOTPATH / "newCustomFile").open("r") as fd: + assert fd.read() == nicedata, "invalid data" + except Exception: + cli.printdebug() + raise + finally: + cli.close() diff --git a/test/scapy/layers/snmp.uts b/test/scapy/layers/snmp.uts index 987dcecd1d3..b281a4dd5b3 100644 --- a/test/scapy/layers/snmp.uts +++ b/test/scapy/layers/snmp.uts @@ -44,10 +44,17 @@ 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) += SNMPvarbind noSuchInstance dissection +~ SNMP ASN1 +x = SNMPvarbind(b'0\x10\x06\x0c+\x06\x01\x02\x01/\x01\x01\x01\x01\n\x01\x81\x00') +assert not x.noSuchObject +assert x.noSuchInstance +assert not x.endOfMibView + = 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') + SNMP(b'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 diff --git a/test/scapy/layers/ssh.uts b/test/scapy/layers/ssh.uts new file mode 100644 index 00000000000..b6c82830c9e --- /dev/null +++ b/test/scapy/layers/ssh.uts @@ -0,0 +1,37 @@ +% SSH regression tests for Scapy + ++ SSH tests + += Load SSH and SSH pcap + +from scapy.layers.ssh import * +pkts = rdpcap(scapy_path("/test/pcaps/ssh_ed25519.pcap")) + += Check for SSHVersionExchange + +assert SSHVersionExchange in pkts[3] +assert pkts[3].lines == [b'SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1'] + += Check for SSH KexInit + +assert pkts[8].pay.type == 20 +assert pkts[8].pay.kex_algorithms.names == [b'sntrup761x25519-sha512@openssh.com', b'curve25519-sha256', b'curve25519-sha256@libssh.org', b'ecdh-sha2-nistp256', b'ecdh-sha2-nistp384', b'ecdh-sha2-nistp521', b'diffie-hellman-group-exchange-sha256', b'diffie-hellman-group16-sha512', b'diffie-hellman-group18-sha512', b'diffie-hellman-group14-sha256'] +assert pkts[8].pay.compression_algorithms_client_to_server.names == [b'none', b'zlib@openssh.com'] +assert pkts[8].pay.first_kex_packet_follows == 0 + += Check for SSH Kex DH Init + +assert pkts[9].pay.e.value == 2350579254774455149352841074576538343990628078324267734527140871419932900538846241321452574779013468175018290651674793284015144587365340014962083771650859331476013596977123734998706481058518220105378857548427645559226229797476788395456646389818256564838400739135010680681163456095677232493468587230912056633743356721223955966756143014970734820639779746710511156996619195651512803235508645669051962031830043263352566212925544898655158819407252176433755590900240990111833619058714386338971655960765233885975850331922799954445954999296511309262036243757363804224821843032668273263064448847356873248470173458896243397517402866118125112555466428030166305568609671333602038983517505792245686243281766138834921907336198416449172686346486278292406454736650900489703602941311383274571253473117352192402069818356338637658276508681778175858698292872544113899589241205559671386224999719303843179598966006636814844667908804397048115462945951578163865384872376314062218622823399683168509281546378022234848416282276373248755506541244682438394426446625521247772730497030387798046748675856950435919346613694108323269457293633349708282520556429147475379754811181108827452704284587405668562299209768965780662855380191554093502874303015220051947466960323628390298028334555285261078171376959928596505395834904631983924930632066431620277301098016669514461539415396461120090857273167687140578309080011600491384868409678444601854368584606594031430672989514629489628693896623746874263590779323124144977231406480674784862894820443935735009785417326990153059454525335480905124180809168192695170554860881795940302149730125034860576014797577021907464402342887433222178995989216549376816454492040151004613910636289921361266911700572137074963622410473946132501034758965925448585513804452940921661377181371668124810916661795313840472724039889785883499146147917756012320556865741641210760458632895093416475238323450214341924898457846364890041182141641464569458439289152005646151462927271290045368143992045845833755058875621892664952349241777000175525150234301417133611325716295866942917806328460205857145603834255471170372323643072574234093090258963511137747272416302757220165305443240348220594316744411327905569373474701071080394539231361841208268626239088329642014216286141161796678306068065801822739617419769840590119143287370990841250896367782388086153939896000077437989471526045058990545907990089943059323343511158016141104461822684535344848072465778114834380180144470998703244485996404968078774187171096252472206846575112317045051585989901412734220984229769099373462781991394933642599850052765470790711255335450011529534223200229563089616493679704133500670071803056311472370457584460617950784473886654145020569892882621834458180061425761806601776138949785217977296683442504030198235793054259542236826904098257847794792743244091577002001218915723232763883537852453240602434246755006330557239933 + += Check for SSH Kex DH Reply + +assert isinstance(pkts[10].pay, SSHKexDHReply) +assert isinstance(pkts[10].pay.K_S.value.data, SSHPublicKeyEd25519) +assert pkts[10].pay.f.value == 145420364842225773825302401106325914711274265993324154430728894326534621359109155840425186538544552052796050053335958730866886288740744420249345515750154798851184330959497070659898985425204715378366354679146309457749164371561091155243958216182101971799434050687511559028317449152411472762323723877627671103812914723157350965167617881557068354019877391362267976527576493473875435265184048851428107514944286989616342786043599413975131699425817361398892615937862444397978104862748600515902989933687456311656405442430739088222322061894563421315591443786569893421006874109563323602421056664468719115729999515282373592751468532774515579030227333862046510775187524340678261311443115463596625632382798119365245475607690175500571706486276645913449452000600385503347151840872914773898773488397031589360149311688536059026933073591802120869627115324168091970764557964769308675365930500125154235572029870366848539246435374954851006770023189648291776010080795050223050860998900405137902471191697225277049222592746894837282272020541849100564888026189233806723871439229668619801557051355230295711162074261723735096669381118352514087748543069098521714367520620776857909533548692973709024859908263199571215346407936984296807266382121546828903054910125941912141681820440324847067053005923257053547200527533308902169030187411617725866120378101642906954603853930588700927719183637036840650380578915269559991390749067662922590313459051714023483342069069486856997828131877064697883838294364044597377634856362822832142618450301805844505073311557951656608292385708401134544514223462642265767599035258374748229336714718608533685329531126529049892131138601419901815421341388895007293701087086445997233255224283053634387459108049782685439584490166669027769404082346078709263888381794126372684739109951124329930500566714883267402922809647283904702829959640898613561998011861738175990862617646085551086342592758425640217942375761120002214263525285687683437628809639146334705175599606153814250017075639638206689953262483413749172593472713439934441043308651524160071237216451477801106668255062822659635335764848170476026942604710330092513922989750910298327358097823488084536544440798321307308541642435897397586864585774450444856007727437988290169282904777426371810586287022758237175995926455562260123808781040290584381913532810485127812346450200037604344159195037050778864761984776712383681923622054756893185075573777827838180404632794820045063257647197822508971656160962510350864007240071912329456453627835389896781002210494913596666104457655076724437210855739938617334378596008363125551567605259368940675801716 +assert isinstance(pkts[10].pay.H_hash.value.data, SSHSignatureEd25519) +assert pkts[10].pay.H_hash.value.data.key.value == b"\xef\xecj=~\xe4Y'\xe9\xad\xb7?\xfe?[\xf3\xddn\x1e\x91\xb5\x1c\xb6O\xf5&\xc7$\x9f\x0c\xeb\x1c<\xf2n\x87iH\xb9\xaf\xf5\xdfXB\xb7\x99\xd1\xbe%\x92\x98a)+\x01\\\xa7\xb4\xb2\x82!\x05e\x0e" + += Check for the 2 SSH New Msgs + +assert isinstance(pkts[10][SSH:2].pay, SSHNewKeys) +assert isinstance(pkts[12].pay, SSHNewKeys) diff --git a/test/scapy/layers/tftp.uts b/test/scapy/layers/tftp.uts index f4ec15b8169..c54e271a08e 100644 --- a/test/scapy/layers/tftp.uts +++ b/test/scapy/layers/tftp.uts @@ -1,16 +1,186 @@ -% TFTP regression tests for Scapy +% Regression tests for TFTP # More information at http://www.secdev.org/projects/UTscapy/ ++ TFTP coverage tests -############ -############ -+ TFTP tests += Test answers + +assert TFTP_DATA(block=1).answers(TFTP_RRQ()) +assert not TFTP_WRQ().answers(TFTP_RRQ()) +assert not TFTP_RRQ().answers(TFTP_WRQ()) +assert TFTP_ACK(block=1).answers(TFTP_DATA(block=1)) +assert not TFTP_ACK(block=0).answers(TFTP_DATA(block=1)) +assert TFTP_ACK(block=0).answers(TFTP_RRQ()) +assert not TFTP_ACK().answers(TFTP_ACK()) +assert TFTP_ERROR().answers(TFTP_DATA()) and TFTP_ERROR().answers(TFTP_ACK()) +assert TFTP_OACK().answers(TFTP_WRQ()) = 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")]) + +x=IP(src="127.0.0.1")/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" + + ++ TFTP Automatons +~ linux + += Utilities +~ linux + +from scapy.automaton import select_objects + +class MockTFTPSocket(object): + packets = [] + def __init__(self, iface): + self.iface = iface + def recv(self, n=None): + pkt = self.packets.pop(0) + return pkt + def send(self, *args, **kargs): + pass + def close(self): + pass + @classmethod + def select(classname, inputs, remain): + test = [s for s in inputs if isinstance(s, classname)] + if test: + if len(test[0].packets): + return test + else: + inputs = [s for s in inputs if not isinstance(s, classname)] + return select_objects(inputs, remain) + + += TFTP_read() automaton +~ linux + +class MockReadSocket(MockTFTPSocket): + packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=1) / ("P" * 512), + IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=2) / "<3"] + +tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807, + ll=MockReadSocket, + recvsock=MockReadSocket, debug=5) + +res = tftp_read.run() +assert res == (b"P" * 512 + b"<3") + += TFTP_read() automaton error +~ linux + +class MockReadSocket(MockTFTPSocket): + packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")] + +tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807, + ll=MockReadSocket, + recvsock=MockReadSocket) + +try: + tftp_read.run() + assert False +except Automaton.ErrorState as e: + assert "Reached ERROR" in str(e) + assert "ERROR Access violation" in str(e) + + += TFTP_write() automaton +~ linux + +data_received = b"" + +class MockWriteSocket(MockTFTPSocket): + packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=0), + IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=1) ] + def send(self, *args, **kargs): + if len(args) and Raw in args[0]: + global data_received + data_received += args[0][Raw].load + +tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807, + ll=MockWriteSocket, + recvsock=MockWriteSocket) + +tftp_write.run() +assert data_received == (b"P" * 767 + b"Scapy <3") + += TFTP_write() automaton error +~ linux + +class MockWriteSocket(MockTFTPSocket): + packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")] + +tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807, + ll=MockWriteSocket, + recvsock=MockWriteSocket) + +try: + tftp_write.run() + assert False +except Automaton.ErrorState as e: + assert "Reached ERROR" in str(e) + assert "ERROR Access violation" in str(e) + + += TFTP_WRQ_server() automaton +~ linux + +class MockWRQSocket(MockTFTPSocket): + packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt"), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 512), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"] + +tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807, + ll=MockWRQSocket, + recvsock=MockWRQSocket) +assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 512 + b"<3")) + += TFTP_WRQ_server() automaton with options +~ linux + +class MockWRQSocket(MockTFTPSocket): + packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 100), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"] + +tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807, + ll=MockWRQSocket, + recvsock=MockWRQSocket) +assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 100 + b"<3")) + += TFTP_RRQ_server() automaton +~ linux + +sent_data = "P" * 512 + "<3" +import tempfile +filename = tempfile.mktemp(suffix=".txt") +fdesc = open(filename, "w") +fdesc.write(sent_data) +fdesc.close() + +received_data = "" + +class MockRRQSocket(MockTFTPSocket): + packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename=filename[5:]) / TFTP_Options(), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=1), + IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=2) ] + def send(self, *args, **kargs): + if len(args): + pkt = args[0] + if TFTP_DATA in pkt: + global received_data + received_data += pkt[Raw].load.decode("utf-8") + +tftp_rrq = TFTP_RRQ_server(ip="1.2.3.4", sport=0x2807, dir="/tmp/", serve_one=True, + ll=MockRRQSocket, + recvsock=MockRRQSocket, debug=4) +tftp_rrq.run() +assert received_data == sent_data + +import os +os.unlink(filename) diff --git a/test/tls/__init__.py b/test/scapy/layers/tls/__init__.py similarity index 100% rename from test/tls/__init__.py rename to test/scapy/layers/tls/__init__.py diff --git a/test/cert.uts b/test/scapy/layers/tls/cert.uts similarity index 94% rename from test/cert.uts rename to test/scapy/layers/tls/cert.uts index d75c89c5ac1..f0a258e4db4 100644 --- a/test/cert.uts +++ b/test/scapy/layers/tls/cert.uts @@ -132,6 +132,13 @@ weDU+RsFxcyU/QxD9WYORzYarqxbcA== """) type(y) is PrivKeyECDSA += 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 + = PrivKeyECDSA sign & verify ~ crypto_advanced a = PrivKeyECDSA() @@ -143,8 +150,9 @@ assert not a.verify(b"Hello", data) = PubKeyECDSA verify ~ crypto_advanced -b = PubKeyECDSA() -b.pubkey = a.pubkey +assert isinstance(a.pubkey, PubKeyECDSA) + +b = PubKeyECDSA(cryptography_obj=a.pubkey.pubkey) assert b.verify(msg, data) assert not b.verify(b"Hello", data) @@ -162,12 +170,12 @@ 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 class: Importing PEM-encoded EdDSA private key +y = PrivKey(""" +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGu36oadjA6raCmwtImfAWI/DSCENM/uQCsUaClVoUTZ +-----END PRIVATE KEY----- +""") + PrivKey/Pubkey test signatures @@ -393,7 +401,7 @@ with ContextManagerCaptureOutput() as cmco: y.show() assert cmco.get_output().strip() == awaited.strip() -= Cert: Check split_pem on chained certs with missing end \n += Cert class : Check split_pem on chained certs with missing end \n from scapy.layers.tls.cert import split_pem ks = split_pem(b""" -----BEGIN EC PRIVATE KEY----- @@ -408,6 +416,51 @@ weDU+RsFxcyU/QxD9WYORzYarqxbcA== -----END EC PRIVATE KEY-----""") assert ks[0][:-1] == ks[1] += Cert class : Import PEM-encoded certificate with ed25519 signature +x = Cert(""" +-----BEGIN CERTIFICATE----- +MIICqDCCAZCgAwIBAgIUYYDvh160/Q32Q/MuCGSfIYxTwwEwDQYJKoZIhvcNAQEL +BQAwVDELMAkGA1UEBhMCTU4xFDASBgNVBAcMC1VsYWFuYmFhdGFyMRcwFQYDVQQL +DA5TY2FweSBUZXN0IFBLSTEWMBQGA1UEAwwNU2NhcHkgVGVzdCBDQTAeFw0yNDA3 +MTQxOTU4MzNaFw0zNDA3MTUxOTU4MzNaMFgxCzAJBgNVBAYTAk1OMRQwEgYDVQQH +DAtVbGFhbmJhYXRhcjEXMBUGA1UECwwOU2NhcHkgVGVzdCBQS0kxGjAYBgNVBAMM +EVNjYXB5IFRlc3QgU2VydmVyMCowBQYDK2VwAyEAB8exZcGWUFeio0aPES732u5l +GXRUuaktLmSIQB8PoPejaDBmMA8GA1UdEwEB/wQFMAMCAQEwEwYDVR0lBAwwCgYI +KwYBBQUHAwEwHQYDVR0OBBYEFJOzQR0udLrz7IiLP3q+FehLxijkMB8GA1UdIwQY +MBaAFGZTlPQV0b1naLBRNzI14aSq3gd8MA0GCSqGSIb3DQEBCwUAA4IBAQCRk6TP +XKfSy2fwodsYe1bedhL9mlm9xDDOu6ILkDZtCpbOwrjeSf+U7VQYvdlI8QCeQyEK +ZE/S3S5UzOjEv7fQpyqfG9aJJbH7OQwG25ShiX86Kt/RAkgtjyCmKevhT6uSs5fa +BsdYWnS9WHWH5ZkWkjZt1K2xYJP4Lqg9VpHy/YNz4b5swXEWf+MdayVSgzPxoviG +zXnsTrxiTcGvelGFm/lYc42u6cSqrHoLtfniyaGNvPwrfBsiY/cypN4GZLNgEk80 +/tcAg2TeUGNbMbT4Rko1OMLxMT9zRzgJyjd/XyW/5fCE/Xm0q7VYo1EF1ScywU1B +XwZH9DJ6Ud0s8/j+ +-----END CERTIFICATE----- +""") + += Cert class : Change subject public key identifier and resign +c = 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----- +""") +k = PrivKeyECDSA() +c.setSubjectPublicKeyFromPrivateKey(k) +c.setSubjectPublicKeyFromPrivateKey(k.pubkey) +c = Cert(k.resignCert(c)) + +assert k.verifyCert(c) ########### CRL class ############################################### diff --git a/test/tls/example_client.py b/test/scapy/layers/tls/example_client.py old mode 100755 new mode 100644 similarity index 81% rename from test/tls/example_client.py rename to test/scapy/layers/tls/example_client.py index 374c588138b..9a28f5cc4f0 --- a/test/tls/example_client.py +++ b/test/scapy/layers/tls/example_client.py @@ -12,17 +12,15 @@ import os import socket import sys - -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__),"../../")) -sys.path=[basedir]+sys.path +from argparse import ArgumentParser from scapy.config import conf from scapy.utils import inet_aton from scapy.layers.tls.automaton_cli import TLSClientAutomaton from scapy.layers.tls.basefields import _tls_version_options +from scapy.layers.tls.keyexchange import _tls_hash_sig from scapy.layers.tls.handshake import TLSClientHello, TLS13ClientHello - -from argparse import ArgumentParser +from scapy.tools.UTscapy import scapy_path psk = None parser = ArgumentParser(description='Simple TLS Client') @@ -41,6 +39,7 @@ parser.add_argument("--sni", help="Server Name Indication") parser.add_argument("--curve", help="ECC group to advertise") +parser.add_argument("--sig-algs", help="Signature algorithms to advertise (coma separated)") parser.add_argument("--debug", action="store_const", const=5, default=0, help="Enter debug mode") parser.add_argument("server", nargs="?", default="127.0.0.1", @@ -82,17 +81,25 @@ except socket.error: server_name = args.server +supported_signature_algorithms = None +if args.sig_algs: + supported_signature_algorithms = args.sig_algs.split(",") + for sigalg in supported_signature_algorithms: + if sigalg not in _tls_hash_sig.values(): + sys.exit("Unrecognized signature algorithm: %s" % sigalg) + t = TLSClientAutomaton(server=args.server, dport=args.port, server_name=server_name, client_hello=ch, version=args.version, - mycert=basedir+"/test/tls/pki/cli_cert.pem", - mykey=basedir+"/test/tls/pki/cli_key.pem", + mycert=scapy_path("/test/scapy/layers/tls/pki/cli_cert.pem"), + mykey=scapy_path("/test/scapy/layers/tls/pki/cli_key.pem"), psk=args.psk, psk_mode=psk_mode, resumption_master_secret=args.res_master, session_ticket_file_in=args.session_ticket_file_in, session_ticket_file_out=args.session_ticket_file_out, + supported_signature_algorithms=supported_signature_algorithms, curve=args.curve, debug=args.debug) t.run() diff --git a/test/tls/example_server.py b/test/scapy/layers/tls/example_server.py old mode 100755 new mode 100644 similarity index 74% rename from test/tls/example_server.py rename to test/scapy/layers/tls/example_server.py index f51d3dc7c77..d9ec859a07d --- a/test/tls/example_server.py +++ b/test/scapy/layers/tls/example_server.py @@ -14,19 +14,27 @@ import os import sys - -basedir = os.path.abspath(os.path.join(os.path.dirname(__file__),"../../")) -sys.path=[basedir]+sys.path +from argparse import ArgumentParser from scapy.config import conf from scapy.layers.tls.automaton_srv import TLSServerAutomaton -from argparse import ArgumentParser +from scapy.tools.UTscapy import scapy_path parser = ArgumentParser(description='Simple TLS Server') +parser.add_argument("--cert", + default=scapy_path('/test/scapy/layers/tls/pki/srv_cert.pem'), + help="Cert file.") +parser.add_argument("--key", + default=scapy_path('/test/scapy/layers/tls/pki/srv_key.pem'), + help="Key file.") parser.add_argument("--psk", help="External PSK for symmetric authentication (for TLS 1.3)") # noqa: E501 parser.add_argument("--no_pfs", action="store_true", help="Disable (EC)DHE exchange with PFS") +parser.add_argument("--pcs", + help="Preferred Cipher Suite (ex: 0x1301 = TLS_AES_128_GCM_SHA256)") +parser.add_argument("--psa", + help="Preferred Signature Algorithm (ex: sha256+rsaepss)") # args.curve must be a value in the dict _tls_named_curves (see tls/crypto/groups.py) parser.add_argument("--curve", help="ECC curve to advertise (ex: secp256r1...") parser.add_argument("--cookie", action="store_true", @@ -41,16 +49,16 @@ help="Enter debug mode") args = parser.parse_args() -pcs = None # PFS is set by default... if args.no_pfs and args.psk: psk_mode = "psk_ke" else: psk_mode = "psk_dhe_ke" -t = TLSServerAutomaton(mycert=basedir+'/test/tls/pki/srv_cert.pem', - mykey=basedir+'/test/tls/pki/srv_key.pem', - preferred_ciphersuite=pcs, +t = TLSServerAutomaton(mycert=args.cert, + mykey=args.key, + preferred_ciphersuite=args.pcs, + preferred_signature_algorithm=args.psa, client_auth=args.client_auth, curve=args.curve, cookie=args.cookie, diff --git a/test/scapy/layers/tls/pki/README.md b/test/scapy/layers/tls/pki/README.md new file mode 100644 index 00000000000..a3117f15f98 --- /dev/null +++ b/test/scapy/layers/tls/pki/README.md @@ -0,0 +1,9 @@ +# Notes on how to generate the PKI + +``` +openssl genpkey -algorithm ED25519 -out srv_key_ed25519.pem +openssl req -new -key srv_key_ed25519.pem -out srv_cert_ed25519.csr -addext basicConstraints=critical,CA:FALSE,pathlen:1 -addext "extendedKeyUsage = serverAuth" -subj "/C=MN/L=Ulaanbaatar/OU=Scapy Test PKI/CN=Scapy Test Server" +openssl x509 -req -days 3653 -in srv_cert_ed25519.csr -CA ca_cert.pem -CAkey ca_key.pem -out srv_cert_ed25519.pem -copy_extensions copyall +rm srv_cert_ed25519.csr +openssl x509 -in srv_cert_ed25519.pem -text -noout +``` diff --git a/test/tls/pki/ca_cert.pem b/test/scapy/layers/tls/pki/ca_cert.pem similarity index 100% rename from test/tls/pki/ca_cert.pem rename to test/scapy/layers/tls/pki/ca_cert.pem diff --git a/test/tls/pki/ca_key.pem b/test/scapy/layers/tls/pki/ca_key.pem similarity index 100% rename from test/tls/pki/ca_key.pem rename to test/scapy/layers/tls/pki/ca_key.pem diff --git a/test/tls/pki/cli_cert.pem b/test/scapy/layers/tls/pki/cli_cert.pem similarity index 100% rename from test/tls/pki/cli_cert.pem rename to test/scapy/layers/tls/pki/cli_cert.pem diff --git a/test/tls/pki/cli_key.pem b/test/scapy/layers/tls/pki/cli_key.pem similarity index 100% rename from test/tls/pki/cli_key.pem rename to test/scapy/layers/tls/pki/cli_key.pem diff --git a/test/tls/pki/srv_cert.pem b/test/scapy/layers/tls/pki/srv_cert.pem similarity index 100% rename from test/tls/pki/srv_cert.pem rename to test/scapy/layers/tls/pki/srv_cert.pem diff --git a/test/scapy/layers/tls/pki/srv_cert_ed25519.pem b/test/scapy/layers/tls/pki/srv_cert_ed25519.pem new file mode 100644 index 00000000000..72396340360 --- /dev/null +++ b/test/scapy/layers/tls/pki/srv_cert_ed25519.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICqDCCAZCgAwIBAgIUYYDvh160/Q32Q/MuCGSfIYxTwwEwDQYJKoZIhvcNAQEL +BQAwVDELMAkGA1UEBhMCTU4xFDASBgNVBAcMC1VsYWFuYmFhdGFyMRcwFQYDVQQL +DA5TY2FweSBUZXN0IFBLSTEWMBQGA1UEAwwNU2NhcHkgVGVzdCBDQTAeFw0yNDA3 +MTQxOTU4MzNaFw0zNDA3MTUxOTU4MzNaMFgxCzAJBgNVBAYTAk1OMRQwEgYDVQQH +DAtVbGFhbmJhYXRhcjEXMBUGA1UECwwOU2NhcHkgVGVzdCBQS0kxGjAYBgNVBAMM +EVNjYXB5IFRlc3QgU2VydmVyMCowBQYDK2VwAyEAB8exZcGWUFeio0aPES732u5l +GXRUuaktLmSIQB8PoPejaDBmMA8GA1UdEwEB/wQFMAMCAQEwEwYDVR0lBAwwCgYI +KwYBBQUHAwEwHQYDVR0OBBYEFJOzQR0udLrz7IiLP3q+FehLxijkMB8GA1UdIwQY +MBaAFGZTlPQV0b1naLBRNzI14aSq3gd8MA0GCSqGSIb3DQEBCwUAA4IBAQCRk6TP +XKfSy2fwodsYe1bedhL9mlm9xDDOu6ILkDZtCpbOwrjeSf+U7VQYvdlI8QCeQyEK +ZE/S3S5UzOjEv7fQpyqfG9aJJbH7OQwG25ShiX86Kt/RAkgtjyCmKevhT6uSs5fa +BsdYWnS9WHWH5ZkWkjZt1K2xYJP4Lqg9VpHy/YNz4b5swXEWf+MdayVSgzPxoviG +zXnsTrxiTcGvelGFm/lYc42u6cSqrHoLtfniyaGNvPwrfBsiY/cypN4GZLNgEk80 +/tcAg2TeUGNbMbT4Rko1OMLxMT9zRzgJyjd/XyW/5fCE/Xm0q7VYo1EF1ScywU1B +XwZH9DJ6Ud0s8/j+ +-----END CERTIFICATE----- diff --git a/test/tls/pki/srv_key.pem b/test/scapy/layers/tls/pki/srv_key.pem similarity index 100% rename from test/tls/pki/srv_key.pem rename to test/scapy/layers/tls/pki/srv_key.pem diff --git a/test/scapy/layers/tls/pki/srv_key_ed25519.pem b/test/scapy/layers/tls/pki/srv_key_ed25519.pem new file mode 100644 index 00000000000..ac7560c104e --- /dev/null +++ b/test/scapy/layers/tls/pki/srv_key_ed25519.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIGu36oadjA6raCmwtImfAWI/DSCENM/uQCsUaClVoUTZ +-----END PRIVATE KEY----- diff --git a/test/sslv2.uts b/test/scapy/layers/tls/sslv2.uts similarity index 98% rename from test/sslv2.uts rename to test/scapy/layers/tls/sslv2.uts index bde0a9e0d96..c523e8153ec 100644 --- a/test/sslv2.uts +++ b/test/scapy/layers/tls/sslv2.uts @@ -85,7 +85,7 @@ mk_enc.decryptedkey is None = Reading SSLv2 session - Importing server compromised key import os -filename = scapy_path("/test/tls/pki/srv_key.pem") +filename = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") rsa_key = PrivKeyRSA(filename) t.tls_session.server_rsa_key = rsa_key @@ -273,10 +273,15 @@ assert isinstance(s.wcs.ciphersuite, SSL_CK_DES_192_EDE3_CBC_WITH_MD5) s.rcs.cipher.iv == b'\x01'*8 s.wcs.cipher.iv == b'\x01'*8 += Dissect invalid payload +p = SSLv2() +with no_debug_dissector(): + p.do_dissect_payload(b'\x00') + assert raw(p.payload) == b'\x00' ############################################################################### ############################ Automaton behaviour ############################## ############################################################################### -# see test/tls/tests_tls_netaccess.uts +# see scapy/layers/tls/clientserver.uts diff --git a/test/tls.uts b/test/scapy/layers/tls/tls.uts similarity index 71% rename from test/tls.uts rename to test/scapy/layers/tls/tls.uts index abfa438d044..0fff89f2f37 100644 --- a/test/tls.uts +++ b/test/scapy/layers/tls/tls.uts @@ -591,7 +591,7 @@ _all_aes_gcm_tests() = Crypto - AES cipher in CCM mode, checks from IEEE P1619.1 -~ crypto_advanced +~ crypto_advanced libressl class _aes256ccm_test_1: k= b"\0"*32 @@ -649,7 +649,7 @@ _all_aes_ccm_tests() = Crypto - ChaCha20POly1305 test (test vector A.5 from RFC 7539) -~ crypto_advanced +~ crypto_advanced libressl import binascii def clean(s): @@ -963,6 +963,8 @@ fin.load == b'\xd9\xcb,\x8cM\xfd\xbc9\xaa\x05\xf3\xd3\xf3Z\x8a-' = Reading TLS test session - Ticket, CCS & Finished +~ libressl + from scapy.layers.tls.handshake import TLSNewSessionTicket t6 = TLS(p6_tick_ccs_fin, tls_session=t5.tls_session.mirror()) tick = t6.msg[0] @@ -980,13 +982,14 @@ assert isinstance(rec_fin.msg[0], _TLSEncryptedContent) rec_fin.msg[0].load == b'7\\)`\xaa`\x7ff\xcd\x10\xa9v\xa3*\x17\x1a' = Building x25519 ecdh_Yc +~ libressl from scapy.layers.tls.record import TLS from scapy.layers.tls.handshake import TLSClientKeyExchange -cli_hello = hex_bytes('160303008f0100008b0303000027104268d53e923ce05aa04cb21b8fe33aed93266c00bd1f13ea6a6dad24000018c02cc02bc030c02fc024c023c028c027c00ac009c014c0130100004a00000013001100000e7777772e676f6f676c652e636f6d000500050100000000000a00080006001d00170018000b00020100000d00140012040105010201040305030203020206010603') -ser_hello = hex_bytes('16030300520200004e03035f9b52e4206fdc2410d1d482905c9b45a204641d9d856afb444f574e4752440120c4d1479e11a26edf0dbcb07e7a5f7d41c3d7b500015ff8c1ceed473bf457b193c02b000006000b0002010016030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d205232311330110603') -ser_cert = hex_bytes('16030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3137303631353030303034325a170d3231313231353030303034325a3042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f3130820122300d06092a864886f70d01010105000382010f003082010a0282010100d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac070203010001a38201333082012f300e0603551d0f0101ff040403020186301d0603551d250416301406082b0601050507030106082b0601050507030230120603551d130101ff040830060101ff020100301d0603551d0e0416041498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e303506082b0601050507010104293027302506082b060105050730018619687474703a2f2f6f6373702e706b692e676f6f672f6773723230320603551d1f042b30293027a025a0238621687474703a2f2f63726c2e706b692e676f6f672f677372322f677372322e63726c303f0603551d20043830363034060667810c010202302a302806082b06010505070201161c68747470733a2f2f706b692e676f6f672f7265706f7369746f72792f300d06092a864886f70d01010b050003820101001a803e3679fbf32ea946377d5e541635aec74e0899febdd13469265266073d0aba49cb62f4f11a8efc114f68964c742bd367deb2a3aa058d844d4c20650fa596da0d16f86c3bdb6f0423886b3a6cc160bd689f718eee2d583407f0d554e98659fd7b5e0d2194f58cc9a8f8d8f2adcc0f1af39aa7a90427f9a3c9b0ff02786b61bac7352be856fa4fc31c0cedb63cb44beaedcce13cecdc0d8cd63e9bca42588bcc16211740bca2d666efdac4155bcd89aa9b0926e732d20d6e6720025b10b090099c0c1f9eadd83beaa1fc6ce8105c085219512a71bbac7ab5dd15ed2bc9082a2c8ab4a621ab63ffd7524950d089b7adf2affb50ae2fe1950df346ad9d9cf5ca') +cli_hello = bytes.fromhex('160303008f0100008b0303000027104268d53e923ce05aa04cb21b8fe33aed93266c00bd1f13ea6a6dad24000018c02cc02bc030c02fc024c023c028c027c00ac009c014c0130100004a00000013001100000e7777772e676f6f676c652e636f6d000500050100000000000a00080006001d00170018000b00020100000d00140012040105010201040305030203020206010603') +ser_hello = bytes.fromhex('16030300520200004e03035f9b52e4206fdc2410d1d482905c9b45a204641d9d856afb444f574e4752440120c4d1479e11a26edf0dbcb07e7a5f7d41c3d7b500015ff8c1ceed473bf457b193c02b000006000b0002010016030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d205232311330110603') +ser_cert = bytes.fromhex('16030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3137303631353030303034325a170d3231313231353030303034325a3042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f3130820122300d06092a864886f70d01010105000382010f003082010a0282010100d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac070203010001a38201333082012f300e0603551d0f0101ff040403020186301d0603551d250416301406082b0601050507030106082b0601050507030230120603551d130101ff040830060101ff020100301d0603551d0e0416041498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e303506082b0601050507010104293027302506082b060105050730018619687474703a2f2f6f6373702e706b692e676f6f672f6773723230320603551d1f042b30293027a025a0238621687474703a2f2f63726c2e706b692e676f6f672f677372322f677372322e63726c303f0603551d20043830363034060667810c010202302a302806082b06010505070201161c68747470733a2f2f706b692e676f6f672f7265706f7369746f72792f300d06092a864886f70d01010b050003820101001a803e3679fbf32ea946377d5e541635aec74e0899febdd13469265266073d0aba49cb62f4f11a8efc114f68964c742bd367deb2a3aa058d844d4c20650fa596da0d16f86c3bdb6f0423886b3a6cc160bd689f718eee2d583407f0d554e98659fd7b5e0d2194f58cc9a8f8d8f2adcc0f1af39aa7a90427f9a3c9b0ff02786b61bac7352be856fa4fc31c0cedb63cb44beaedcce13cecdc0d8cd63e9bca42588bcc16211740bca2d666efdac4155bcd89aa9b0926e732d20d6e6720025b10b090099c0c1f9eadd83beaa1fc6ce8105c085219512a71bbac7ab5dd15ed2bc9082a2c8ab4a621ab63ffd7524950d089b7adf2affb50ae2fe1950df346ad9d9cf5ca') r1 = TLS(cli_hello) r2 = TLS(ser_hello, tls_session=r1.tls_session.mirror()) @@ -999,7 +1002,29 @@ bytes(pkt) pkt.exchkeys.fill_missing() assert len(pkt.exchkeys.ecdh_Yc) == 32 += Building secp521r1 ecdh_Yc +~ libressl + +from scapy.layers.tls.record import TLS +from scapy.layers.tls.handshake import TLSClientKeyExchange + +cli_hello = bytes.fromhex('160303008f0100008b0303000027104268d53e923ce05aa04cb21b8fe33aed93266c00bd1f13ea6a6dad24000018c02cc02bc030c02fc024c023c028c027c00ac009c014c0130100004a00000013001100000e7777772e676f6f676c652e636f6d000500050100000000000a00080006001d00170019000b00020100000d00140012040105010201040305030203020206010603') +ser_hello = bytes.fromhex('16030300520200004e03035f9b52e4206fdc2410d1d482905c9b45a204641d9d856afb444f574e4752440120c4d1479e11a26edf0dbcb07e7a5f7d41c3d7b500015ff8c1ceed473bf457b193c02b000006000b0002010016030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d205232311330110603') +ser_cert = bytes.fromhex('16030309270b0009230009200004cc308204c8308203b0a003020102021100b2fe3f66ac447c9f02000000007d9a03300d06092a864886f70d01010b05003042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f31301e170d3230313030363036343132305a170d3230313232393036343132305a3068310b3009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e205669657731133011060355040a130a476f6f676c65204c4c43311730150603550403130e7777772e676f6f676c652e636f6d3059301306072a8648ce3d020106082a8648ce3d030107034200046a7c4a9d35904b2b1b3f7c7326ff2adfb1bcb273b2a1a02003978dd1df8cecce5094e2309201fcd2294270ef359f4bdea177665d9e7321586682392ccc93ac89a382025c30820258300e0603551d0f0101ff04040302078030130603551d25040c300a06082b06010505070301300c0603551d130101ff04023000301d0603551d0e041604145d7ced1d850e92337bf7a0602ae3dd70668b895b301f0603551d2304183016801498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b306806082b06010505070101045c305a302b06082b06010505073001861f687474703a2f2f6f6373702e706b692e676f6f672f677473316f31636f7265302b06082b06010505073002861f687474703a2f2f706b692e676f6f672f677372322f475453314f312e63727430190603551d1104123010820e7777772e676f6f676c652e636f6d30210603551d20041a30183008060667810c010202300c060a2b06010401d67902050330330603551d1f042c302a3028a026a0248622687474703a2f2f63726c2e706b692e676f6f672f475453314f31636f72652e63726c30820104060a2b06010401d6790204020481f50481f200f0007600b21e05cc8ba2cd8a204e8766f92bb98a2520676bdafa70e7b249532def8b905e00000174fcdb8af4000004030047304502207610c2e9b008b8ebf2f27a1e65631b8ae7534294c60f55a4c78c9e4927f56a23022100b61c72d94468cd6b4e376ea5d2ba7a6605a765c575f6a536b819a2d8031be4f7007600e712f2b0377e1a62fb8ec90c6184f1ea7b37cb561d11265bf3e0f34bf241546e00000174fcdb8af7000004030047304502204aae6373290fd01ad53e8ff9d6ef4f21d0badc0e65fef15c52c8f6af9468e12f022100c77dff6b266313b172ef8815beff3032d6058129baaa767361fe95e2c541ad81300d06092a864886f70d01010b05000382010100657bfcc7fd99ffed6f032bb056dc7712ec3ea437cf17750db8527ae0dcdd630f3ec4b2b95b7d482cc3a94082351117e45362319a842556954160d34c9019cc3cd312073ce540d031c4bf197b924a139b0e91bffc168a80c1641834445c509d6edf2daf8a247b72104b184968ef25cc260c75b28f470fc355477ac403da12701556e6e7550a38346f444238a154f1efb1a1a297eff39e35d76bc285599d19b0bd475d6f1daba94f1365dbc930041a79ca2df1bb724d53e4fb8831b8dc486522ee7d8eee0e1fea658352736fe949fcb233c08a3b9e14abc56a5556f9b469925cb5916ed058a4cf332c4c13a75ca83850773f9182ad95fadff51203c08e684df63b00044e3082044a30820332a003020102020d01e3b49aa18d8aa981256950b8300d06092a864886f70d01010b0500304c3120301e060355040b1317476c6f62616c5369676e20526f6f74204341202d20523231133011060355040a130a476c6f62616c5369676e311330110603550403130a476c6f62616c5369676e301e170d3137303631353030303034325a170d3231313231353030303034325a3042310b3009060355040613025553311e301c060355040a1315476f6f676c65205472757374205365727669636573311330110603550403130a47545320434120314f3130820122300d06092a864886f70d01010105000382010f003082010a0282010100d018cf45d48bcdd39ce440ef7eb4dd69211bc9cf3c8e4c75b90f3119843d9e3c29ef500d10936f0580809f2aa0bd124b02e13d9f581624fe309f0b747755931d4bf74de1928210f651ac0cc3b222940f346b981049e70b9d8339dd20c61c2defd1186165e7238320a82312ffd2247fd42fe7446a5b4dd75066b0af9e426305fbe01cc46361af9f6a33ff6297bd48d9d37c1467dc75dc2e69e8f86d7869d0b71005b8f131c23b24fd1a3374f823e0ec6b198a16c6e3cda4cd0bdbb3a4596038883bad1db9c68ca7531bfcbcd9a4abbcdd3c61d7931598ee81bd8fe264472040064ed7ac97e8b9c05912a1492523e4ed70342ca5b4637cf9a33d83d1cd6d24ac070203010001a38201333082012f300e0603551d0f0101ff040403020186301d0603551d250416301406082b0601050507030106082b0601050507030230120603551d130101ff040830060101ff020100301d0603551d0e0416041498d1f86e10ebcf9bec609f18901ba0eb7d09fd2b301f0603551d230418301680149be20757671c1ec06a06de59b49a2ddfdc19862e303506082b0601050507010104293027302506082b060105050730018619687474703a2f2f6f6373702e706b692e676f6f672f6773723230320603551d1f042b30293027a025a0238621687474703a2f2f63726c2e706b692e676f6f672f677372322f677372322e63726c303f0603551d20043830363034060667810c010202302a302806082b06010505070201161c68747470733a2f2f706b692e676f6f672f7265706f7369746f72792f300d06092a864886f70d01010b050003820101001a803e3679fbf32ea946377d5e541635aec74e0899febdd13469265266073d0aba49cb62f4f11a8efc114f68964c742bd367deb2a3aa058d844d4c20650fa596da0d16f86c3bdb6f0423886b3a6cc160bd689f718eee2d583407f0d554e98659fd7b5e0d2194f58cc9a8f8d8f2adcc0f1af39aa7a90427f9a3c9b0ff02786b61bac7352be856fa4fc31c0cedb63cb44beaedcce13cecdc0d8cd63e9bca42588bcc16211740bca2d666efdac4155bcd89aa9b0926e732d20d6e6720025b10b090099c0c1f9eadd83beaa1fc6ce8105c085219512a71bbac7ab5dd15ed2bc9082a2c8ab4a621ab63ffd7524950d089b7adf2affb50ae2fe1950df346ad9d9cf5ca') + +r1 = TLS(cli_hello) +r2 = TLS(ser_hello, tls_session=r1.tls_session.mirror()) +r3 = TLS(ser_cert, tls_session=r2.tls_session) + +s = r3.tls_session.mirror() +s.client_kx_ecdh_params = 25 +pkt = TLSClientKeyExchange(tls_session=s) +bytes(pkt) +pkt.exchkeys.fill_missing() +assert len(pkt.exchkeys.ecdh_Yc) == 133 # len(b'\x04') + ceil(521/8) * 2 + = Reading TLS test session - Extended master secret +~ libressl # See https://github.com/secdev/scapy/issues/2784 @@ -1007,10 +1032,10 @@ from scapy.layers.tls.cert import PrivKey from scapy.layers.tls.handshake import TLSFinished from scapy.layers.tls.record import TLS -chello_extms = hex_bytes(b'1603010200010001fc0303f8b3dbcb70ed3804009c15af4a4298720619b70d1ad4f24d0e99de9e93ce3c3b201c3b2cf3266bcba19b29479ec66fe815f7db0a6b976111f70958395e7aeebaba003e130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff01000175000b000403000102000a000c000a001d0017001e00190018337400000010000e000c02683208687474702f312e31001600000017000000310000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020e8410f5ab09d96b05f10183ccd9e93a057a73290b4c9e1c254cdfc299fc01d41001500d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') -shello_extms = hex_bytes(b'160303005502000051030320a54032477ea3a963b8a700090459f11f1f4ad1896e1d75745b7e2bdc51dde0200600f552db6c51b97a309717ff847bb6e8fef1ce2601544413fda7b66075b887009d000009ff0100010000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') -finished_extms = hex_bytes(b'160303010610000102010007534dd8642e57edd33d156d8002f70562864c1dfe5d721763e8e4ef2c03fb14b4e4eac1864c41fcce57367f95798f04954ef957deb934536b0ac39a72c14f772d0f64b7cc0d8260e2019748fc65fd6f382da6d4f873afe6fc1fa17e786cf6c72b6a46950d2030c7b42ed10f2c4dba37282001132ddb151a44f6face6b049338217784cf2a5ac6a054a2a1d205fb7657d7affa14113c43314b54b28164423455174f57eb50f6eea0836ba1c68616db720641bf18f0cdf7bb729c9cc0b4cfeee8aeed94e00573210eb5328cbcca4ccb1aa29a910c5b5f2c96cf3a431e9677980400d574244ff6bfdabf36ba9dda84703f5760d607e4b731d4f1dc16372b0feac11403030001011603030028269118aa98b35c71e35034f35c23c78d55c04662cdb71c11b1ef862e3b4ebf8ace2aff053257bb08') -key = base64_bytes(b'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRFM5OU5ySXpwV0dUYVAKdzZmYkR5TmszSEdZYmRUdXZMN29XM2crRTV6K3NLZ041UUZIeVRhcWhDTU9MNkxya2U4WE4wRURoN2t5UkFySAp6OW84V1didStJOW9pOHZ1YWwwTitxQjF0MzMrZzI4c3N4ZzNYL1crS3pYMjFpM1h4TEZISWs5bjFUMlZ4L3pCCkhOcDd4aUkybTAxS2FGWlZGcCt5ajJibEVYSlBESnJ5OTA2a3pmQ2JrdmtYSkdwWUwyZjYzajcvZnNvc2VVMXgKUEJQNEROVS9oSHFobHRDdHdFU1VlUXBpampKL1MxUFFXNE1DWEQ2bFFSbGZsVHptL0RmdHpHaW8vVzdLWWg4NAp2QWk4TFkxeXo4MzRYR2o1OVBSSVd6SVRQR01wbjRYLzFpdmVXcDFZWGxxSmJ3Z3hsRWduZnhub2JWMW9lTHhUCmRvc3UrYk1oQWdNQkFBRUNnZ0VBSGQ5NTBISHNrTVNCTlZvL0tvVzZQVTM1eDl2Rml3aXUvN2YwRHRZNEpOaGUKODVpNTFiQm9UVHpvdWRtRStGWnh4SmZPWFBHYkI4TWF3N0JxOXFDeU1xUi9xZzRoa21EOVREMXcrenBBWFFtLwpkRlRuMk85OW5MQUJ0RElmeTYzT2JJUXZPa1MzczczZHpIcUpkWDFZMnVLaXp5WjNFeFZoQjZmR3Fpa09ScU1BCmNYbjJSRzN1UXFNWk4yUkVUK1hFYWdsa1dkbGphVTdaTC9CbklRT2xGS0h2ZzVSeGFwWGpJbTM2NnFUVStreGEKWDJFZnllOUJycWxWK0o4cnYzODVjRDBQc3RkSVFTQzMxZFBzUHMrSnJMVlBKQVpGZTBLVk1lYkk2ODU1cERYZApGd1ZGcC9BOXhFa3NwRW1jS0tnL1ZkZ3JQZUxMQmxhVm9mMVhPeUhWQVFLQmdRRHhPdXFGaXJvNTNQNGZQUGlMCkFnTTNvRnpmY2xwdDFMdnduelprUmVMU1NvVFBvZSt1R2xMdTBpS3lMUHBjWm1DTCt0bldsSXBheHRYOU1CRmUKOWNvMlJpSU9WM2JZM0ZpOTBLYjlvN3NyZURhaWE5NElHNGlBYktyWjJJdktBZmFkWnBqb1hBTXZpWnBEYWxGYgprZWVCd29nV0sreTdic2EwU1RYTGVMdjF5d0tCZ1FEZjRwT2lUZ3RBNFdtMXo2WFB4Z0ZCa3A3OWVjaWhINTlICnF1cVJNNkhtQ2YzSnZqZzJCZnYyb2hYNTlTU2VnZTI5ek0yZEhmVGhSeW1vZlg5VkpyMnRYY2FhVWpkRnp1Ui8Kcm1EblJMTjVDTUFnUWNCU3M5UXFCaXdTM0hqVmpML1REcFMvblJwY2VCQnNZTFYvR1YvQkpvWDkxTlVodVRXcwpjQ0VvRmNVOVF3S0JnUUNjbCtGTHhTMTBpSGZTY1hMcVVla2l3QS9wNFVMQWoxdGRMUTFTOUdiMG1ma3pDKzBaCitPNmpKM2ZzYi9RcDdTOTVUdU1BUDdhOGpOeTJtZkI4MDFOci9nVDNpR0dYRHhyd1JUVlI2MnFDSW14YzdXYloKbm4zeTJCZmtpSVRlSW40ajJVa2pkUytBT1hRUmxUK3hFTHJXNmlBTFBJSlZmZWl4ZWVEWTc4d2NGd0tCZ0Z5aQoxcTFvbDNWd0Q1cGY0ZDdYc2Z0YzNKWkxCcjNNWk01MXBQc1JueUtjN2JyRkQyTWpGTDlYRDdyT09TbXczeHNTCm05MHY0UHc1d3IzcHQzOFhPWko3WThyRXpBUUJlRUJ3ZWI0WGloOUJoS1dVTHl6SkpiZUJ1RWpSbXRuWmxDR1QKUGU4TzVUSnZwM1FBaS9pY0dpZkVkZHF5YnNHMmJjUDgzV3RGbnNnYkFvR0FMOHF4VUx3bGlMck1ML3c3aEJNegpXSHdKM21PK0NXbzFWR3p4bi9lK3I2ejVTUW03M0VuYzlSZnVkN3RBWmU1QUhXYXVSR3RNaVNoY0J1bkl5Q0g1CnU2Q2laZU5UOTBRdElLRmVCS09QSk5WNDR1QzJtK0xKQkNGa0hzU085MHp0dHZzcmVyU0tiNG5oZ2tiZDhxQ24KbDVFZFBpZEx2NXdiY0tyc3dIVzZYSm89Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=') +chello_extms = bytes.fromhex('1603010200010001fc0303f8b3dbcb70ed3804009c15af4a4298720619b70d1ad4f24d0e99de9e93ce3c3b201c3b2cf3266bcba19b29479ec66fe815f7db0a6b976111f70958395e7aeebaba003e130213031301c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff01000175000b000403000102000a000c000a001d0017001e00190018337400000010000e000c02683208687474702f312e31001600000017000000310000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602002b00050403040303002d00020101003300260024001d0020e8410f5ab09d96b05f10183ccd9e93a057a73290b4c9e1c254cdfc299fc01d41001500d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000') +shello_extms = bytes.fromhex('160303005502000051030320a54032477ea3a963b8a700090459f11f1f4ad1896e1d75745b7e2bdc51dde0200600f552db6c51b97a309717ff847bb6e8fef1ce2601544413fda7b66075b887009d000009ff0100010000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') +finished_extms = bytes.fromhex('160303010610000102010007534dd8642e57edd33d156d8002f70562864c1dfe5d721763e8e4ef2c03fb14b4e4eac1864c41fcce57367f95798f04954ef957deb934536b0ac39a72c14f772d0f64b7cc0d8260e2019748fc65fd6f382da6d4f873afe6fc1fa17e786cf6c72b6a46950d2030c7b42ed10f2c4dba37282001132ddb151a44f6face6b049338217784cf2a5ac6a054a2a1d205fb7657d7affa14113c43314b54b28164423455174f57eb50f6eea0836ba1c68616db720641bf18f0cdf7bb729c9cc0b4cfeee8aeed94e00573210eb5328cbcca4ccb1aa29a910c5b5f2c96cf3a431e9677980400d574244ff6bfdabf36ba9dda84703f5760d607e4b731d4f1dc16372b0feac11403030001011603030028269118aa98b35c71e35034f35c23c78d55c04662cdb71c11b1ef862e3b4ebf8ace2aff053257bb08') +key = base64.b64decode(b'LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRFM5OU5ySXpwV0dUYVAKdzZmYkR5TmszSEdZYmRUdXZMN29XM2crRTV6K3NLZ041UUZIeVRhcWhDTU9MNkxya2U4WE4wRURoN2t5UkFySAp6OW84V1didStJOW9pOHZ1YWwwTitxQjF0MzMrZzI4c3N4ZzNYL1crS3pYMjFpM1h4TEZISWs5bjFUMlZ4L3pCCkhOcDd4aUkybTAxS2FGWlZGcCt5ajJibEVYSlBESnJ5OTA2a3pmQ2JrdmtYSkdwWUwyZjYzajcvZnNvc2VVMXgKUEJQNEROVS9oSHFobHRDdHdFU1VlUXBpampKL1MxUFFXNE1DWEQ2bFFSbGZsVHptL0RmdHpHaW8vVzdLWWg4NAp2QWk4TFkxeXo4MzRYR2o1OVBSSVd6SVRQR01wbjRYLzFpdmVXcDFZWGxxSmJ3Z3hsRWduZnhub2JWMW9lTHhUCmRvc3UrYk1oQWdNQkFBRUNnZ0VBSGQ5NTBISHNrTVNCTlZvL0tvVzZQVTM1eDl2Rml3aXUvN2YwRHRZNEpOaGUKODVpNTFiQm9UVHpvdWRtRStGWnh4SmZPWFBHYkI4TWF3N0JxOXFDeU1xUi9xZzRoa21EOVREMXcrenBBWFFtLwpkRlRuMk85OW5MQUJ0RElmeTYzT2JJUXZPa1MzczczZHpIcUpkWDFZMnVLaXp5WjNFeFZoQjZmR3Fpa09ScU1BCmNYbjJSRzN1UXFNWk4yUkVUK1hFYWdsa1dkbGphVTdaTC9CbklRT2xGS0h2ZzVSeGFwWGpJbTM2NnFUVStreGEKWDJFZnllOUJycWxWK0o4cnYzODVjRDBQc3RkSVFTQzMxZFBzUHMrSnJMVlBKQVpGZTBLVk1lYkk2ODU1cERYZApGd1ZGcC9BOXhFa3NwRW1jS0tnL1ZkZ3JQZUxMQmxhVm9mMVhPeUhWQVFLQmdRRHhPdXFGaXJvNTNQNGZQUGlMCkFnTTNvRnpmY2xwdDFMdnduelprUmVMU1NvVFBvZSt1R2xMdTBpS3lMUHBjWm1DTCt0bldsSXBheHRYOU1CRmUKOWNvMlJpSU9WM2JZM0ZpOTBLYjlvN3NyZURhaWE5NElHNGlBYktyWjJJdktBZmFkWnBqb1hBTXZpWnBEYWxGYgprZWVCd29nV0sreTdic2EwU1RYTGVMdjF5d0tCZ1FEZjRwT2lUZ3RBNFdtMXo2WFB4Z0ZCa3A3OWVjaWhINTlICnF1cVJNNkhtQ2YzSnZqZzJCZnYyb2hYNTlTU2VnZTI5ek0yZEhmVGhSeW1vZlg5VkpyMnRYY2FhVWpkRnp1Ui8Kcm1EblJMTjVDTUFnUWNCU3M5UXFCaXdTM0hqVmpML1REcFMvblJwY2VCQnNZTFYvR1YvQkpvWDkxTlVodVRXcwpjQ0VvRmNVOVF3S0JnUUNjbCtGTHhTMTBpSGZTY1hMcVVla2l3QS9wNFVMQWoxdGRMUTFTOUdiMG1ma3pDKzBaCitPNmpKM2ZzYi9RcDdTOTVUdU1BUDdhOGpOeTJtZkI4MDFOci9nVDNpR0dYRHhyd1JUVlI2MnFDSW14YzdXYloKbm4zeTJCZmtpSVRlSW40ajJVa2pkUytBT1hRUmxUK3hFTHJXNmlBTFBJSlZmZWl4ZWVEWTc4d2NGd0tCZ0Z5aQoxcTFvbDNWd0Q1cGY0ZDdYc2Z0YzNKWkxCcjNNWk01MXBQc1JueUtjN2JyRkQyTWpGTDlYRDdyT09TbXczeHNTCm05MHY0UHc1d3IzcHQzOFhPWko3WThyRXpBUUJlRUJ3ZWI0WGloOUJoS1dVTHl6SkpiZUJ1RWpSbXRuWmxDR1QKUGU4TzVUSnZwM1FBaS9pY0dpZkVkZHF5YnNHMmJjUDgzV3RGbnNnYkFvR0FMOHF4VUx3bGlMck1ML3c3aEJNegpXSHdKM21PK0NXbzFWR3p4bi9lK3I2ejVTUW03M0VuYzlSZnVkN3RBWmU1QUhXYXVSR3RNaVNoY0J1bkl5Q0g1CnU2Q2laZU5UOTBRdElLRmVCS09QSk5WNDR1QzJtK0xKQkNGa0hzU085MHp0dHZzcmVyU0tiNG5oZ2tiZDhxQ24KbDVFZFBpZEx2NXdiY0tyc3dIVzZYSm89Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=') # Load key ssl_key = PrivKey(key) @@ -1031,9 +1056,9 @@ assert l3.msg[0][TLSFinished].vdata == b'\x00\x1fG\xd8VD@\x0ctK\xeee' # RC4 case -chello_extms = hex_bytes(b'160301008501000081030360037703ac90bb5e29ae0fca71b68dd8133b17b7060c13779d34f69d5c3255110000060005000400ff01000052337400000010000e000c02683208687474702f312e310016000000170000000d0030002e040305030603080708080809080a080b080408050806040105010601030302030301020103020202040205020602') -shello_extms = hex_bytes(b'1603030055020000510303c985430a03add71566a952a16249e471cd3226c0792ba42c444f574e4752440120e835d66cd3293b9fcb157d5c477848d654a2d3a42fc92bcf9c472171188f69610005000009ff0100010000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') -finished_extms = hex_bytes(b'16030301061000010201004971b89ae4355a001c49ccb49ed0664a9090a2dc0c14c97563b6dd98f13004ac5327c97abf10617b1f5d19b1f6e1091ccf159693497ebda262aedba2f3b76ae217d56477cad45e2ea129c324083701c2e99e65b6d63f916f963de8d98c5357d22272c032a30acccd673d1556d01e22e206186bcda3a5845d6dacee260ab66f47ea86a4c0081faa082b398f2c65da35264428f320c354b97cd96c986da43c8510e914ffb7f8bb73baee2530c4533ae2d6a922771af689c15b42c53428978510a3e3e90a3806f77fc1cb35c2c3f34dd7e3f831a79bc59b333f0c9e8be49390cd2a8e1c88dafbb9e3e24d1e0530703dbff7cd1c516fcc21a7d484f2111f985f03f8140303000101160303002457ed5c62171e4720a5890cf9ef09323f6e2db063aeebea776a54b879ffb6a69182d15cae') +chello_extms = bytes.fromhex('160301008501000081030360037703ac90bb5e29ae0fca71b68dd8133b17b7060c13779d34f69d5c3255110000060005000400ff01000052337400000010000e000c02683208687474702f312e310016000000170000000d0030002e040305030603080708080809080a080b080408050806040105010601030302030301020103020202040205020602') +shello_extms = bytes.fromhex('1603030055020000510303c985430a03add71566a952a16249e471cd3226c0792ba42c444f574e4752440120e835d66cd3293b9fcb157d5c477848d654a2d3a42fc92bcf9c472171188f69610005000009ff0100010000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') +finished_extms = bytes.fromhex('16030301061000010201004971b89ae4355a001c49ccb49ed0664a9090a2dc0c14c97563b6dd98f13004ac5327c97abf10617b1f5d19b1f6e1091ccf159693497ebda262aedba2f3b76ae217d56477cad45e2ea129c324083701c2e99e65b6d63f916f963de8d98c5357d22272c032a30acccd673d1556d01e22e206186bcda3a5845d6dacee260ab66f47ea86a4c0081faa082b398f2c65da35264428f320c354b97cd96c986da43c8510e914ffb7f8bb73baee2530c4533ae2d6a922771af689c15b42c53428978510a3e3e90a3806f77fc1cb35c2c3f34dd7e3f831a79bc59b333f0c9e8be49390cd2a8e1c88dafbb9e3e24d1e0530703dbff7cd1c516fcc21a7d484f2111f985f03f8140303000101160303002457ed5c62171e4720a5890cf9ef09323f6e2db063aeebea776a54b879ffb6a69182d15cae') # Load TLS session r1 = TLS(chello_extms) @@ -1050,14 +1075,16 @@ assert isinstance(l3.msg[0], TLSFinished) assert l3.msg[0][TLSFinished].vdata == b'\x15\xd6\xd5\xea\x84\xee\xb3\xdd\xd6\x10\xd8\x11' = Reading TLS test session - Encrypt-then-MAC extension +~ libressl + from scapy.layers.tls.cert import PrivKey from scapy.layers.tls.handshake import TLSFinished from scapy.layers.tls.record import TLS -client_hello = hex_bytes(b'16030100c9010000c50303611a2f42b70345cfbc5c5c4da1929bea8a2cb8b1fd10ab1341e43ffaa8856a63000038c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff01000064000b000403000102000a000c000a001d0017001e00190018337400000010000e000c02683208687474702f312e310016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602') -server_hello = hex_bytes(b'1603030059020000550303a22c975875df69bea936cbd28b083cde754693b4f34a15a036e5e57b7f4755cf20226e6386f90e3751723beea9196640d5bbe6c7c9f314568fa3645cb7218e9159003d00000dff010001000016000000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') -client_finished = hex_bytes(b'1603030106100001020100482bf86fa7047c767ecc5f46e971f2349232d57d4c40b04856b6ea2b5645b5b233c0cd2ad7b05101d6a3fcbd2698b25064501ba4f0cde40c8189abc29aebfffcb87413d4590cae7cf3589fa371ad5e0d161da9c275a4b8ca1aa9a400a3d76021f92b872403a72a22bad6368276010209ca1344971adf7d7a9cdeefd534cd933ec3d2852ea1dfff217f7cd55eac7d2b18f7c5600c56f28746389d1d6c33cd2ac24817632fc0fbd81ffcf528b1c2a5b328a0105e88513e6b2f95b51ca3adf390146662115a721bfd718eae3033388aaa5cb37e2c16428a6f7c994f961137f6a7f933327ed300f15621500d427d261f39970bbf40f4ba303963609439007d34e6bc1403030001011603030050f4b7962d5455e9244efe886bbd4156ca20936e4b8868d80c82b06ceac7cff6d69f130a610f2aa4c4fd8cb2681f84e3ebecad1b563bcd258255aa509ba2b6388f90ac5f1c1f84f1569dc3809667b86ba4') -server_finished = hex_bytes(b'14030300010116030300509e8e5fd6aebaa98263e98266fffcf7fd21eb50fb0510b8598660afb65c57a025374c1e63aff3e260dd5d027180e8aa0d85d43e0c0b54e8783e4ce51a71ef0ae555ab81404020342ca1a34643ce713688') +client_hello = bytes.fromhex('16030100c9010000c50303611a2f42b70345cfbc5c5c4da1929bea8a2cb8b1fd10ab1341e43ffaa8856a63000038c02cc030009fcca9cca8ccaac02bc02f009ec024c028006bc023c0270067c00ac0140039c009c0130033009d009c003d003c0035002f00ff01000064000b000403000102000a000c000a001d0017001e00190018337400000010000e000c02683208687474702f312e310016000000170000000d002a0028040305030603080708080809080a080b080408050806040105010601030303010302040205020602') +server_hello = bytes.fromhex('1603030059020000550303a22c975875df69bea936cbd28b083cde754693b4f34a15a036e5e57b7f4755cf20226e6386f90e3751723beea9196640d5bbe6c7c9f314568fa3645cb7218e9159003d00000dff010001000016000000170000160303036e0b00036a0003670003643082036030820248a003020102020900eb73b71c3e2f9fdc300d06092a864886f70d01010b05003045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c7464301e170d3139303231353135313430335a170d3239303231323135313430335a3045310b30090603550406130241553113301106035504080c0a536f6d652d53746174653121301f060355040a0c18496e7465726e6574205769646769747320507479204c746430820122300d06092a864886f70d01010105000382010f003082010a0282010100d2f7d36b233a5619368fc3a7db0f2364dc71986dd4eebcbee85b783e139cfeb0a80de50147c936aa84230e2fa2eb91ef1737410387b932440ac7cfda3c5966eef88f688bcbee6a5d0dfaa075b77dfe836f2cb318375ff5be2b35f6d62dd7c4b147224f67d53d95c7fcc11cda7bc622369b4d4a685655169fb28f66e511724f0c9af2f74ea4cdf09b92f917246a582f67fade3eff7eca2c794d713c13f80cd53f847aa196d0adc04494790a628e327f4b53d05b83025c3ea541195f953ce6fc37edcc68a8fd6eca621f38bc08bc2d8d72cfcdf85c68f9f4f4485b32133c63299f85ffd62bde5a9d585e5a896f08319448277f19e86d5d6878bc53768b2ef9b3210203010001a3533051301d0603551d0e041604148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35301f0603551d230418301680148297fb8cf3d9ddf2f60ec90f92815c28e3f3ff35300f0603551d130101ff040530030101ff300d06092a864886f70d01010b050003820101003c001f0f5d106072e0beedfa47895329d4623422080e66caa4beb4c3d9b6868fa107467d6160e512ede7cbbefdeb17c09b9f594f86f1a9922c982ccf9655d4d67eda061f667eeb25fe7203bea00e40f150a490a78ca963d87c185ade8b7c294f5052fb1e1edb3403831e33e4026c8e56717cb77bc32321858be37a77fe7f5511ef8c2013c86e2730c5b2366875131cae0c7616fdc7c8605696133e7a685f20203c0db8e0ff1d1a5991d2f058f48f20b10a5fb0df27a1f9874cc0fe8d6ebf77e9a7ba38490e9d63241a0fb3fd7701ff3b130c9aa7aa77770280b7003c1bb5e0784c34aacb74ce8114960e50eee04602a7ab20e5c878028e4292e90e40fd631fee16030300040e000000') +client_finished = bytes.fromhex('1603030106100001020100482bf86fa7047c767ecc5f46e971f2349232d57d4c40b04856b6ea2b5645b5b233c0cd2ad7b05101d6a3fcbd2698b25064501ba4f0cde40c8189abc29aebfffcb87413d4590cae7cf3589fa371ad5e0d161da9c275a4b8ca1aa9a400a3d76021f92b872403a72a22bad6368276010209ca1344971adf7d7a9cdeefd534cd933ec3d2852ea1dfff217f7cd55eac7d2b18f7c5600c56f28746389d1d6c33cd2ac24817632fc0fbd81ffcf528b1c2a5b328a0105e88513e6b2f95b51ca3adf390146662115a721bfd718eae3033388aaa5cb37e2c16428a6f7c994f961137f6a7f933327ed300f15621500d427d261f39970bbf40f4ba303963609439007d34e6bc1403030001011603030050f4b7962d5455e9244efe886bbd4156ca20936e4b8868d80c82b06ceac7cff6d69f130a610f2aa4c4fd8cb2681f84e3ebecad1b563bcd258255aa509ba2b6388f90ac5f1c1f84f1569dc3809667b86ba4') +server_finished = bytes.fromhex('14030300010116030300509e8e5fd6aebaa98263e98266fffcf7fd21eb50fb0510b8598660afb65c57a025374c1e63aff3e260dd5d027180e8aa0d85d43e0c0b54e8783e4ce51a71ef0ae555ab81404020342ca1a34643ce713688') # Load TLS session r1 = TLS(client_hello) @@ -1072,14 +1099,15 @@ server_finished = r4.getlayer(TLS, 2).msg[0] assert r4.tls_session.encrypt_then_mac assert isinstance(client_finished, TLSFinished) assert isinstance(server_finished, TLSFinished) -assert client_finished.vdata == hex_bytes(b'771049b4ff714ac71253f84f') -assert server_finished.vdata == hex_bytes(b'42c9765e833997b6714fec75') +assert client_finished.vdata == bytes.fromhex('771049b4ff714ac71253f84f') +assert server_finished.vdata == bytes.fromhex('42c9765e833997b6714fec75') ### ### Other/bug tests ### = Reading TLS test session - Full TLSNewSessionTicket captured +~ libressl import os filename = scapy_path("/test/pcaps/tls_new-session-ticket.pcap") a = rdpcap(filename) @@ -1089,6 +1117,7 @@ assert pkt[TLS].msg[0].ticket == b'6k\x8b{\xa8\xaf\xf0\x8aG*\xdd\xc2\xf6\t\xde\x assert pkt[TLS].msg[0].lifetime == 3600 = Reading TLS test session - ApplicationData +~ libressl t7 = TLS(p7_data, tls_session=t6.tls_session.mirror()) assert t7.iv == b'\x00\x00\x00\x00\x00\x00\x00\x01' assert t7.mac == b'>\x1dLb5\x8e+\x01n\xcb\x19\xcc\x17Ey\xc8' @@ -1180,7 +1209,7 @@ load_layer("tls") from scapy.layers.tls.cert import PrivKeyRSA from scapy.layers.tls.record import TLSApplicationData import os -filename = scapy_path("/test/tls/pki/srv_key.pem") +filename = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") key = PrivKeyRSA(filename) ch = b'\x16\x03\x01\x005\x01\x00\x001\x03\x01X\xac\x0e\x8c\xe46\xe9\xedo\xda\x085$M\xae$\x90\xd9\xa93\xb7(\x13J\xf9\xc5?\xef\xf4\x96\xa1\xfa\x00\x00\x04\x00/\x00\xff\x01\x00\x00\x04\x00#\x00\x00' sh = b'\x16\x03\x01\x005\x02\x00\x001\x03\x01\x88\xac\xd4\xaf\x93~\xb5\x1b8c\xe7)\xa6\x9b\xa9\xed\xf3\xf3*\xdb\x00\x8bB\xf6\n\xcbz\x8eP\x83`G\x00\x00/\x00\x00\t\xff\x01\x00\x01\x00\x00#\x00\x00\x16\x03\x01\x03\xac\x0b\x00\x03\xa8\x00\x03\xa5\x00\x03\xa20\x82\x03\x9e0\x82\x02\x86\xa0\x03\x02\x01\x02\x02\t\x00\xfe\x04W\r\xc7\'\xe9\xf60\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000T1\x0b0\t\x06\x03U\x04\x06\x13\x02MN1\x140\x12\x06\x03U\x04\x07\x0c\x0bUlaanbaatar1\x170\x15\x06\x03U\x04\x0b\x0c\x0eScapy Test PKI1\x160\x14\x06\x03U\x04\x03\x0c\rScapy Test CA0\x1e\x17\r160916102811Z\x17\r260915102811Z0X1\x0b0\t\x06\x03U\x04\x06\x13\x02MN1\x140\x12\x06\x03U\x04\x07\x0c\x0bUlaanbaatar1\x170\x15\x06\x03U\x04\x0b\x0c\x0eScapy Test PKI1\x1a0\x18\x06\x03U\x04\x03\x0c\x11Scapy Test Server0\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\xcc\xf1\xf1\x9b`-`\xae\xf2\x98\r\')\xd9\xc0\tYL\x0fJ0\xa8R\xdf\xe5\xb1!\x9fO\xc3=V\x93\xdd_\xc6\xf7\xb3\xf6U\x8b\xe7\x92\xe2\xde\xf2\x85I\xb4\xa1,\xf4\xfdv\xa8g\xca\x04 `\x11\x18\xa6\xf2\xa9\xb6\xa6\x1d\xd9\xaa\xe5\xd9\xdb\xaf\xe6\xafUW\x9f\xffR\x89e\xe6\x80b\x80!\x94\xbc\xcf\x81\x1b\xcbg\xc2\x9d\xb5\x05w\x04\xa6\xc7\x88\x18\x80xh\x956\xde\x97\x1b\xb6a\x87B\x1au\x98E\x82\xeb>2\x11\xc8\x9b\x86B9\x8dM\x12\xb7X\x1b\x19\xf3\x9d+\xa1\x98\x82\xca\xd7;$\xfb\t9\xb0\xbc\xc2\x95\xcf\x82)u\x16)?B \x17+M@\x8cVl\xad\xba\x0f4\x85\xb1\x7f@yqx\xb7\xa5\x04\xbb\x94\xf7\xb5A\x95\xee|\xeb\x8d\x0cyhY\xef\xcb\xb3\xfa>x\x1e\xeegLz\xdd\xe0\x99\xef\xda\xe7\xef\xb2\t]\xbe\x80 !\x05\x83,D\xdb]*v)\xa5\xb0#\x88t\x07T"\xd6)z\x92\xf5o-\x9e\xe7\xf8&+\x9cXe\x02\x03\x01\x00\x01\xa3o0m0\t\x06\x03U\x1d\x13\x04\x020\x000\x0b\x06\x03U\x1d\x0f\x04\x04\x03\x02\x05\xe00\x1d\x06\x03U\x1d\x0e\x04\x16\x04\x14\xa1+ p\xd2k\x80\xe5e\xbc\xeb\x03\x0f\x88\x9ft\xad\xdd\xf6\x130\x1f\x06\x03U\x1d#\x04\x180\x16\x80\x14fS\x94\xf4\x15\xd1\xbdgh\xb0Q725\xe1\xa4\xaa\xde\x07|0\x13\x06\x03U\x1d%\x04\x0c0\n\x06\x08+\x06\x01\x05\x05\x07\x03\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x00\x03\x82\x01\x01\x00\x81\x88\x92sk\x93\xe7\x95\xd6\xddA\xee\x8e\x1e\xbd\xa3HX\xa7A5?{}\xd07\x98\x0e\xb8,\x94w\xc8Q6@\xadY\t(\xc8V\xd6\xea[\xac\xb4\xd8?h\xb7f\xca\xe1V7\xa9\x00e\xeaQ\xc9\xec\xb2iI]\xf9\xe3\xc0\xedaT\xc9\x12\x9f\xc6\xb0\nsU\xe8U5`\xef\x1c6\xf0\xda\xd1\x90wV\x04\xb8\xab8\xee\xf7\t\xc5\xa5\x98\x90#\xea\x1f\xdb\x15\x7f2(\x81\xab\x9b\x85\x02K\x95\xe77Q{\x1bH.\xfb>R\xa3\r\xb4F\xa9\x92:\x1c\x1f\xd7\n\x1eXJ\xfa.Q\x8f)\xc6\x1e\xb8\x0e1\x0es\xf1\'\x88\x17\xca\xc8i\x0c\xfa\x83\xcd\xb3y\x0e\x14\xb0\xb8\x9b/:-\t\xe3\xfc\x06\xf0:n\xfd6;+\x1a\t*\xe8\xab_\x8c@\xe4\x81\xb2\xbc\xf7\x83g\x11nN\x93\xea"\xaf\xff\xa3\x9awWv\xd0\x0b8\xac\xf8\x8a\x945\x8e\xd7\xd4a\xcc\x01\xff$\xb4\x8fa#\xba\x88\xd7Y\xe4\xe9\xba*N\xb5\x15\x0f\x9c\xd0\xea\x06\x91\xd9\xde\xab\x16\x03\x01\x00\x04\x0e\x00\x00\x00' @@ -1198,13 +1227,18 @@ assert isinstance(t.msg[0], TLSApplicationData) assert t.msg[0].data == b"" t.getlayer(TLS, 2).msg[0].data == b"To boldly go where no man has gone before...\n" -= Auto provide the session += Auto-provide the session: use TCPSession with conf.tls_session_enable conf.debug_dissector = 2 + +conf.tls_session_enable = True +conf.tls_sessions.server_rsa_key = key + client = "192.168.0.1" server = "1.2.3.4" -bc = Ether()/IP(src=client, dst=server)/TCP(sport=51478, dport=443, seq=1) -bs = Ether()/IP(src=server, dst=client)/TCP(sport=443, dport=51478, seq=1) +bc = Ether()/IP(src=client, dst=server)/TCP(sport=51478, dport=443, seq=RandShort()) +bs = Ether()/IP(src=server, dst=client)/TCP(sport=443, dport=51478, seq=RandShort()) + pcap = [ bc/ch, bs/sh, @@ -1212,11 +1246,12 @@ pcap = [ bs/fin, bc/data ] -res = sniff(offline=pcap, session=TLSSession(server_rsa_key=key)) +res = sniff(offline=pcap, session=TCPSession) res[4].show() assert res[4].getlayer(TLS, 2).msg[0].data == b"To boldly go where no man has gone before...\n" +conf.tls_session_enable = False ############################################################################### ############################## Building packets ############################### @@ -1331,16 +1366,13 @@ assert not TLSHelloRequest().tls_session_update(None) = Cryptography module is unavailable ~ mock -import scapy.libs.six as six -import mock +from unittest import mock @mock.patch("scapy.layers.tls.crypto.suites.get_algs_from_ciphersuite_name") def test_tls_without_cryptography(get_algs_from_ciphersuite_name_mock): get_algs_from_ciphersuite_name_mock.return_value = (scapy.layers.tls.crypto.kx_algs.KX_ECDHE_RSA, None, None, scapy.layers.tls.crypto.hash.Hash_SHA256, False) sh = IP()/TCP()/TLS(msg=TLSServerHello(cipher=0xc02f)) assert raw(sh) - if six.PY2: - assert str(sh) sh2 = Ether(b"\xaa\xaa\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb\xbb\xbb\x86\xdd`\x04Z\xd8\x02\x19\x06@\xcfm\xack|z\xae\xac\x9d\x8d'\xba\xa2Cs\xcc\x07\x8f\x91\xbdk\x0e\x1e\xdb\xf6\xbe\xc3\xa1\xfc\xa5\x15\xca\xd6#\x01\xbb\xeeC\xc0H\xea\xa2\x9a,P\x18\x00\xffu\xf0\x00\x00\x16\x03\x01\x02\x00\x01\x00\x01\xfc\x03\x03W`\xb4|\n5E\x11\xe8\xb5\xa3\x9c\xea\xa6I\x99N\xcd\xe9j\x8d\xfe\xa8%\x8b\xceC\xf8w\x94gV \x13\x0b\xdf}\xad\xbf\xbe67\xba\xcf\x9c\xfa\x92\xc2\xeeS\xf6DL\x19\xb3\xe4`H\x84\xcb]h\xb4\xbb\xba\x00\x1cZZ\xc0+\xc0/\xc0,\xc00\xcc\xa9\xcc\xa8\xc0\x13\xc0\x14\x00\x9c\x00\x9d\x00/\x005\x00\n\x01\x00\x01\x97\xba\xba\x00\x00\xff\x01\x00\x01\x00\x00\x00\x00\x11\x00\x0f\x00\x00\x0cfacebook.com\x00\x17\x00\x00\x00#\x00\xc0\x8a`K^\x7fF\x05K\x95\x85\x1c\xec\x9f\xff\x9b\x85T\x85=<\xbc\xfb\xe4n4\xe9W+\xfanM\xa7\x8c.\x95\x9e\xf0\xfb\x93\x91\xa9\x87\x12o\xc8\x99\xe8\x94_\xca\xceH(\xcai\xdf\xe8\xcf7\x05v\xd4\x9e\x85\x86\x19\xe4\xb6\xf9K\n\xb2\xfd\xa1\xa3r\x9f\xec\x05\xd4\xbc\x1bU\x9a\x89\x1d)\xc5\x85(?@x\r\x12Ep\xb7\xf8\x0c\xe7\x17Y<\xbd-\xd7\x9a\x9f^\xb1k\x0b\xcb\xfd\xf4\xb1z\x06\xe9Mna\x9a\xc8\xc8\xdd\x95\xa1`N\xbd/\x9d\xd6\xd9\x93\xf4$\xefq\x80R\xc3|\x9f\xe1'\x19\xf2I\xf8\xdbV\x0b/\xaex8q\xb2ZGU\xf7^\xa9\x80\xf9\r\xbfo\xee\t\x01(\x93\x12g\x1frXUa\xdc\x8d*F\xb8\xc6\xe2\xb6\x00\r\x00\x14\x00\x12\x04\x03\x08\x04\x04\x01\x05\x03\x08\x05\x05\x01\x08\x06\x06\x01\x02\x01\x00\x05\x00\x05\x01\x00\x00\x00\x00\x00\x12\x00\x00\x00\x10\x00\x0e\x00\x0c\x02h2\x08http/1.1uP\x00\x00\x00\x0b\x00\x02\x01\x00\x00\n\x00\n\x00\x08jj\x00\x1d\x00\x17\x00\x18zz\x00\x01\x00\x00\x15\x00Y\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\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") assert TLS in sh2 assert isinstance(sh2.msg[0], TLSClientHello) @@ -1350,8 +1382,8 @@ test_tls_without_cryptography() = Truncated TCP segment with no_debug_dissector(): - pkt = Ether(hex_bytes('00155dfb587a00155dfb58430800450005dc54d3400070065564400410d40a00000d01bb044e8b86744e16063ac45010faf06ba9000016030317c30200005503035cb336a067d53a5d2cedbdfec666ac740afbd0637ddd13eddeab768c3c63abee20981a0000d245f1c905b329323ad67127cd4b907a49f775c331d0794149aca7cdc02800000d0005000000170000ff010001000b000ec6000ec300090530820901308206e9a00302010202132000036e72aded906765595fae000000036e72300d06092a864886f70d01010b050030818b310b30090603550406130255533113')) - assert TLSServerHello in pkt + pkt = Ether(bytes.fromhex('00155dfb587a00155dfb58430800450005dc54d3400070065564400410d40a00000d01bb044e8b86744e16063ac45010faf06ba9000016030317c30200005503035cb336a067d53a5d2cedbdfec666ac740afbd0637ddd13eddeab768c3c63abee20981a0000d245f1c905b329323ad67127cd4b907a49f775c331d0794149aca7cdc02800000d0005000000170000ff010001000b000ec6000ec300090530820901308206e9a00302010202132000036e72aded906765595fae000000036e72300d06092a864886f70d01010b050030818b310b30090603550406130255533113')) + assert conf.padding_layer in pkt ############################################################################### ########################### TLS Misc tests #################################### @@ -1479,7 +1511,7 @@ assert raw(p) == a with no_debug_dissector(): p = Ether(b'RU\x10\x00\x02\x02RT\x00\x124V\x08\x00E\x00\x05\xc8\r\xd8\x00\x00@\x06\x96\x9d\x9c&\xce\x12\xc0\xa8\xa5\xd9\x01\xbb\xc0\x1f\x00w$\x02\x03\xbe\xc5#P\x10#(\x0b\x9e\x00\x00\x16\x03\x03\x0e4\x02\x00\x00M\x03\x03^\xfa\xb5~\x88\xdf\xdc#}\'\xa0\xff\xa2\xe2\xb5\xec\x0e\x93\xa8\xe0\xde\x01[\x13[F\x151 x\xc6\xcc `)\x00\x00\x8aZ\x90l\xda\x0b\xe1\xec[i\x13\xa7\x8e\xb9a\x98"\x8a7L\x9d\x90\xe0\x01\x06c$9\xc0\'\x00\x00\x05\xff\x01\x00\x01\x00\x0b\x00\x0c\x8e\x00\x0c\x8b\x00\x06n0\x82\x06j0\x82\x05R\xa0\x03\x02\x01\x02\x02\x10EY\xe8\x1c\x1e\x9a\xe0?X\xaa\xc3\xbc\xcd`jh0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x0b\x05\x000\x81\x8f1\x0b0\t\x06\x03U\x04\x06\x13\x02GB1\x1b0\x19\x06\x03U\x04\x08\x13\x12Greater Manchester1\x100\x0e\x06\x03U\x04\x07\x13\x07Salford1\x180\x16\x06\x03U\x04\n\x13\x0fSectigo Limited1705\x06\x03U\x04\x03\x13.Sectigo RSA Domain Validation Secure Server CA0\x1e\x17\r190309000000Z\x17\r210308235959Z0W1!0\x1f\x06\x03U\x04\x0b\x13\x18Domain Control Validated1\x1d0\x1b\x06\x03U\x04\x0b\x13\x14PositiveSSL Wildcard1\x130\x11\x06\x03U\x04\x03\x0c\n*.mql5.net0\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\xcb\xbcn=\xbaGd\xe1XB\x07\xc9\xb1\xc8/\x86\xaa4Z\xbdNk\xfb\xffR\x8f\xe4\x1c^\x91m8\xb9^\x97\xa5\xd3N\xfb\x80\x92\x8ap\xda\x15\x9f\xee\xe7\xb3\xc8?\xb0>~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr~\xaa\x07\x91\xb1\x99q\xe2\xe5\xc8\x9b\x1d5\xa0\x96,\x98\xdaW\x93\x95\x8e%\xe8\xd4L\xeb\xcbSg\x15"\xba\xb7\xc7\x1f\xe9\xd6\x1a\xe6E\x1d\xc8\x1e%\xd36\xe0/r\xd1\xce1C\xce\x91&\xa1\x08*R\xbf\x8cu\xb0\xda\x0e\x1e2\xd66\x1df&3\x9b\x03\x0b\xcam:\xf7\x12\xd9ud(\xae\xdc\xbci\x85\xbd\xcf\xeb{\x15:\xbd\x0e\x11\x1bi\xd8\xff]y~E\x15\x95\xee\xe9\xea\xc6Cr.Q2MzGY[k@" in packets[13].msg[0].data +packets = sniff(offline=scapy_path("doc/notebooks/tls/raw_data/tls_nss_example.pcap"), session=TCPSession) +assert b"GET /pki/srv_key.pem HTTP/1.0\r\n" in packets[9].msg[0].data +assert b"BEGIN PRIVATE KEY" in packets[10].msg[0].data +assert b"GET /pki/srv_key.pem HTTP/1.0\r\n" in packets[27].inner.msg[0].data +assert b"BEGIN PRIVATE KEY" in packets[28].inner.msg[0].data conf = bck_conf @@ -1581,7 +1625,71 @@ if shutil.which("editcap"): pcapng_path = get_temp_file() exit_status = os.system("editcap --inject-secrets tls,%s %s %s" % (key_log_path, pcap_path, pcapng_path)) assert exit_status == 0 - packets = rdpcap(pcapng_path) - assert b"GET /secret.txt HTTP/1.0\n" in packets[11].msg[0].data - assert b"z2|gxarIKOxt,G1d>.Q2MzGY[k@" in packets[13].msg[0].data + packets = sniff(offline=pcapng_path, session=TCPSession) + assert b"GET /pki/srv_key.pem HTTP/1.0\r\n" in packets[9].msg[0].data + assert b"BEGIN PRIVATE KEY" in packets[10].msg[0].data + assert b"GET /pki/srv_key.pem HTTP/1.0\r\n" in packets[27].inner.msg[0].data + assert b"BEGIN PRIVATE KEY" in packets[28].inner.msg[0].data conf = bck_conf + += pcapng file with a non-UTF-8 Decryption Secrets Block + +# GH3936 + +hdump = """ +00000000 0a 0d 0d 0a c4 00 00 00 4d 3c 2b 1a 01 00 00 00 |........M<+.....| +00000010 ff ff ff ff ff ff ff ff 02 00 37 00 49 6e 74 65 |..........7.Inte| +00000020 6c 28 52 29 20 43 6f 72 65 28 54 4d 29 20 69 37 |l(R) Core(TM) i7| +00000030 2d 36 37 30 30 48 51 20 43 50 55 20 40 20 32 2e |-6700HQ CPU @ 2.| +00000040 36 30 47 48 7a 20 28 77 69 74 68 20 53 53 45 34 |60GHz (with SSE4| +00000050 2e 32 29 00 03 00 2a 00 4c 69 6e 75 78 20 34 2e |.2)...*.Linux 4.| +00000060 32 30 2e 31 32 2d 67 65 6e 74 6f 6f 2d 61 6e 64 |20.12-gentoo-and| +00000070 72 6f 6d 65 64 61 2d 32 30 31 39 30 33 30 35 2d |romeda-20190305-| +00000080 76 31 00 00 04 00 33 00 44 75 6d 70 63 61 70 20 |v1....3.Dumpcap | +00000090 28 57 69 72 65 73 68 61 72 6b 29 20 33 2e 31 2e |(Wireshark) 3.1.| +000000a0 30 20 28 76 33 2e 31 2e 30 72 63 30 2d 34 36 38 |0 (v3.1.0rc0-468| +000000b0 2d 67 65 33 65 34 32 32 32 62 29 00 00 00 00 00 |-ge3e4222b).....| +000000c0 c4 00 00 00 0a 00 00 00 c4 00 00 00 4b 53 4c 54 |............KSLT| +000000d0 b0 00 00 00 43 4c 49 45 4e 54 5f 52 41 4e 44 4f |....CLIENT_RANDO| +000000e0 4d 20 41 36 39 39 35 43 37 44 35 41 35 31 35 42 |M A6995C7D5A515B| +000000f0 30 44 34 39 41 31 42 38 31 33 33 39 33 34 32 37 |0D49A1B813393427| +00000100 43 43 35 43 39 44 42 37 36 36 37 38 45 34 38 44 |CC5C9DB76678E48D| +00000110 31 41 43 35 39 31 44 37 44 37 44 35 42 38 30 31 |1AC591D7D7D5B801| +00000120 44 43 20 34 30 33 37 35 37 34 30 31 42 30 30 37 |DC 403757401B007| +00000130 34 35 33 38 33 41 46 36 41 36 30 38 31 39 42 43 |45383AF6A60819BC| +00000140 37 46 38 42 36 33 39 33 42 37 32 45 44 45 39 46 |7F8B6393B72EDE9F| +00000150 45 42 32 30 44 33 31 33 46 38 31 42 39 c0 bd bb |EB20D313F81B9...| +00000160 c6 36 46 36 41 43 37 34 32 46 46 46 35 45 43 31 |.6F6AC742FFF5EC1| +00000170 44 31 41 32 44 39 39 41 46 34 39 35 33 45 31 33 |D1A2D99AF4953E13| +00000180 33 34 41 0a c4 00 00 00 |34A.....| +00000188 +""".strip() + +assert len(rdpcap(io.BytesIO(import_hexcap(hdump)))) == 0 + += pcap file & external TLS Key Log file with TCPSession (without extms) +* GH3722 + +# Write SSLKEYLOGFILE +temp_sslkeylog = get_temp_file() +with open(temp_sslkeylog, "w") as fd: + fd.write("CLIENT_RANDOM 09F91DA01B1FEB50B691C932959111E5E1D676437F7A42DE47EA881F6295D4E7 EE119869B732F0F9561FFDD95E50A2ACBF268EE0C7C33B409E68C1972E0B280944F7345E845E82F909CCFEB61C456E1F\n") + +bck_conf = conf +conf.tls_session_enable = True +conf.tls_nss_filename = temp_sslkeylog + +packets = sniff(offline=scapy_path("test/pcaps/tls_tcp_frag_withnss.pcap.gz"), session=TCPSession) +packets.show() + +assert packets[8].getlayer(TLS, 3).msg[0].msgtype == 20 +assert packets[8].getlayer(TLS, 3).msg[0].vdata == b'\n\xd4`\xf0\xd9X\x02\x10Z\x81\xf4l' +assert packets[10].getlayer(TLS, 3).msg[0].msgtype == 20 +assert packets[10].getlayer(TLS, 3).msg[0].vdata == b'\xa6>f\xd8\xacf\x99| \xbd<\xa1' +assert packets[11].msg[0].data == b'GET /uuid HTTP/1.1\r\nUser-Agent: Mozilla/5.0 (Windows NT; Windows NT 10.0; en-US) WindowsPowerShell/5.1.22000.832\r\nHost: httpbin.org\r\nConnection: Keep-Alive\r\n\r\n' +assert packets[13].msg[0].data == b'HTTP/1.1 200 OK\r\nDate: Sat, 20 Aug 2022 22:32:24 GMT\r\nContent-Type: application/json\r\nContent-Length: 53\r\nConnection: keep-alive\r\nServer: gunicorn/19.9.0\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Credentials: true\r\n\r\n{\n "uuid": "5bad226d-504a-4416-a11a-8a5f8edbdbbd"\n}\n' + +# Test summary() +assert packets[6].summary() == 'Ether / IP / TCP / TLS 52.87.105.151:443 > 10.211.55.3:51933 / TLS / TLS Handshake - Certificate / TLS / TLS Handshake - Server Key Exchange / TLS / TLS Handshake - Server Hello Done' +assert packets[8].summary() == 'Ether / IP / TCP / TLS 10.211.55.3:51933 > 52.87.105.151:443 / TLS / TLS Handshake - Client Key Exchange / TLS / TLS ChangeCipherSpec / TLS / TLS Handshake - Finished' +conf = bck_conf diff --git a/test/tls13.uts b/test/scapy/layers/tls/tls13.uts similarity index 88% rename from test/tls13.uts rename to test/scapy/layers/tls/tls13.uts index f5eb67d33b2..7a525ef1090 100644 --- a/test/tls13.uts +++ b/test/scapy/layers/tls/tls13.uts @@ -1,8 +1,9 @@ % Tests for TLS 1.3 # # Try me with : -# bash test/run_tests -t test/tls13.uts -F +# bash test/run_tests -t test/scapy/layers/tls/tls13.uts -F +~ libressl + Read a protected TLS 1.3 session # /!\ These tests will not catch our 'INTEGRITY CHECK FAILED's. /!\ @@ -1182,3 +1183,127 @@ adb0114161069d364cceb ae8dab6c88151f297daea ecfd2e1a598a486e2efc9 561298f8dd5f35 fdc7f00e6cc6fc0f96752 76a9d607686c4d779d4bb 7544fb60c7f3079afbc74 61ed67fd55a78c44d6f8d 4eaf386acc17dea11e37a 09f63da3d059243b35f44 9e891255ac7b4f631509d 7060f """) + += Create TLS_Ext_KeyShare_CH: compute several algorithms + +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_KeyShare_CH, KeyShareEntry + +# x25519 +ch = TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group="x25519")]) +ch = TLS_Ext_KeyShare_CH(bytes(ch)) + +assert ch.len == 38 +assert ch.client_shares[0].kxlen == 32 +assert len(ch.client_shares[0].key_exchange) == 32 + +# ffdhe2048 +ch = TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group="ffdhe2048")]) +ch = TLS_Ext_KeyShare_CH(bytes(ch)) + +assert ch.len == 262 +assert ch.client_shares[0].kxlen == 256 +assert len(ch.client_shares[0].key_exchange) == 256 + +# secp384r1 +ch = TLS_Ext_KeyShare_CH(client_shares=[KeyShareEntry(group="secp384r1")]) +ch = TLS_Ext_KeyShare_CH(bytes(ch)) + +assert ch.len == 103 +assert ch.client_shares[0].kxlen == 97 +assert len(ch.client_shares[0].key_exchange) == 97 + += Parse TLS 1.3 Client Hello with non-rfc 5077 ticket + +from scapy.layers.tls.keyexchange_tls13 import TLS_Ext_PreSharedKey_CH + +ch = TLS(b'\x16\x03\x01\x01\x1a\x01\x00\x01\x16\x03\x03\xec\x9c>\xb2\x9e|B\x05\x17f\x86\xc8\x18\x0421\x87\x87\x12\xf6\xec\xa2J\x95\x84[\xf8\xab\xe9gK> \xc6%\xff&wn)\xb2\xf5\xe8_x\x96\xe9\nEsK\xda\x86o\x82f\xa5\xbadk\xf4Ar~}\x00\x08\x13\x02\x13\x03\x13\x01\x00\xff\x01\x00\x00\xc5\x00\x0b\x00\x04\x03\x00\x01\x02\x00\n\x00\x16\x00\x14\x00\x1d\x00\x17\x00\x1e\x00\x19\x00\x18\x01\x00\x01\x01\x01\x02\x01\x03\x01\x04\x00#\x00\x00\x00\x16\x00\x00\x00\x17\x00\x00\x00\r\x00\x1e\x00\x1c\x04\x03\x05\x03\x06\x03\x08\x07\x08\x08\x08\t\x08\n\x08\x0b\x08\x04\x08\x05\x08\x06\x04\x01\x05\x01\x06\x01\x00+\x00\x03\x02\x03\x04\x00-\x00\x02\x01\x01\x003\x00&\x00$\x00\x1d\x00 l\x19\xe1f1 )6\xbf\x91\x9e\xab\xd2\x06\x16\x0b|\x88\xf7,\xf1\x88\x99Z\xb6\xb3\x93\xe4\x08z\x8a\t\x00)\x00:\x00\x15\x00\x0fClient_identity\x00\x00\x00\x00\x00! m\xf3^\xc1l\xac5\xf2\xe3=\xeb\xe3\x81\xd3\xb3\xdd\xbd\xbd\x01\xc9\xdd\x01i\x8c1\xa0ye\xcd\x04\x9e\x9c') + +assert isinstance(ch.msg[0].ext[9], TLS_Ext_PreSharedKey_CH) +assert ch.msg[0].ext[9].identities[0].identity.load == b'Client_identity' +assert ch.msg[0].ext[9].identities[0].obfuscated_ticket_age == 0 + ++ QUIC Transport Parameters + += QUIC Transport Parameters - Parse hex stream +~ quic + +from scapy.layers.tls.quic import * +from scapy.layers.tls.all import TLS13ClientHello, TLS_Ext_QUICTransportParameters + +ch_data = bytes.fromhex("010001e403034f417babafc5dc240c744225bb09b0c5067618b7501ef4bf7ea73c64249e5d0c000006130213011303010001b50033010c010a00170041048497f2dd89fb1d341b02894edd154ebd5ee5e55594d7935d99d2c05733991cccc9af02200e53bcc80208fa1498c5c88ccf643d598cb05c5fde37a1e468cd593200180061045bff37b0fde67fcfc50b7ab6eb139f51998bdb859632138b30caf96882ef871b27aaf534cce0dcfa157be21343fd6b0db5cc306564f19c46d3c9e175e3dbbb594fe7c393e35de695fc84f64ec4a59ee3cea26a0599a61d6dfc18568fb5c0cb85001d00205af975b0ec59288a578c94890d3264f9ac025ab86f7cd718112da6b923b2e54d001e0038f989efd52e4e8ab64491bfd8b8d30481d854b9394f517148dc8d5a50a43ebbdcca6e4b27229acd2f20b6633632d32e9be6999a40d30561e2002b0003020304000d00140012040308040401050308050501020108070808000a000a000800170018001d001e002d000201010000000e000c00000968332d7365727665720010000500030268330039005301048000ea600404801000000508c0000001000000000608c0000001000000000708c00000010000000008024080090240800a01030b01190e01080f087f317d3033e6423e110c00000001000000016b3343cf") + +ch = TLS13ClientHello(ch_data) +tp = ch.ext[-1] +assert isinstance(tp, TLS_Ext_QUICTransportParameters) +assert isinstance(tp.params[0], QUIC_TP_MaxIdleTimeout) +assert tp.params[0].value == 60000 +assert isinstance(tp.params[1], QUIC_TP_InitialMaxData) +assert tp.params[1].value == 1048576 +assert isinstance(tp.params[2], QUIC_TP_InitialMaxStreamDataBidiLocal) +assert tp.params[2].value == 4294967296 +assert isinstance(tp.params[3], QUIC_TP_InitialMaxStreamDataBidiRemote) +assert tp.params[3].value == 4294967296 +assert isinstance(tp.params[4], QUIC_TP_InitialMaxStreamDataUni) +assert tp.params[4].value == 4294967296 +assert isinstance(tp.params[5], QUIC_TP_InitialMaxStreamsBidi) +assert tp.params[5].value == 128 +assert isinstance(tp.params[6], QUIC_TP_InitialMaxStreamsUni) +assert tp.params[6].value == 128 +assert isinstance(tp.params[7], QUIC_TP_AckDelayExponent) +assert tp.params[7].value == 3 +assert isinstance(tp.params[8], QUIC_TP_MaxAckDelay) +assert tp.params[8].value == 25 +assert isinstance(tp.params[9], QUIC_TP_ActiveConnectionIdLimit) +assert tp.params[9].value == 8 +assert isinstance(tp.params[10], QUIC_TP_InitialSourceConnectionId) +assert tp.params[10].value == bytes.fromhex("7f317d3033e6423e") + += QUIC Transport Parameters - Build packet +~ quic + +from scapy.layers.tls.quic import * +from scapy.layers.tls.all import TLS_Ext_QUICTransportParameters + +tp = TLS_Ext_QUICTransportParameters(params=[ + QUIC_TP_MaxIdleTimeout(value=5000), + QUIC_TP_MaxUdpPayloadSize(value=1350), + QUIC_TP_InitialMaxData(value=10000000), + QUIC_TP_InitialMaxStreamDataBidiLocal(value=1000000), + QUIC_TP_InitialMaxStreamDataBidiRemote(value=1000000), + QUIC_TP_InitialMaxStreamDataUni(value=1000000), + QUIC_TP_InitialMaxStreamsBidi(value=100), + QUIC_TP_InitialMaxStreamsUni(value=100), + QUIC_TP_AckDelayExponent(value=3), + QUIC_TP_MaxAckDelay(value=25), + QUIC_TP_DisableActiveMigration(), + QUIC_TP_InitialSourceConnectionId(value=bytes.fromhex("2173071905d778f98e367b8ad8eeb526484e8f5d")), +]) +actual = tp.build() + +# the expected data is extracted from the ClientHello above +expect = bytes.fromhex("0039004601015388030145460401809896800501800f42400601800f42400701800f424008014064090140640a01030b01190c000f142173071905d778f98e367b8ad8eeb526484e8f5d") + +assert actual == expect + += QUIC Transport Parameters - Build empty packet +~ quic + +from scapy.layers.tls.all import TLS_Ext_QUICTransportParameters + +p = TLS_Ext_QUICTransportParameters(params=[]) +actual = p.build() + +assert actual == b'\x009\x00\x00' + += QUIC Transport Parameters - Throw error if value is a big integer +~ quic + +from scapy.layers.tls.quic import QUIC_TP_InitialMaxData + +# A 62-bit left shift results in an integer of 63 bits +p = QUIC_TP_InitialMaxData(value=1<<62) +try: + p.build() + assert False, "QUIC cannot decode integers with more than 62 bits" +except struct.error: + pass diff --git a/test/scapy/layers/tls/tlsclientserver.uts b/test/scapy/layers/tls/tlsclientserver.uts new file mode 100644 index 00000000000..5f1885e9d06 --- /dev/null +++ b/test/scapy/layers/tls/tlsclientserver.uts @@ -0,0 +1,570 @@ +% TLS session establishment tests + +~ crypto + +# More information at http://www.secdev.org/projects/UTscapy/ + +############ +############ + ++ Common util functions + += Load server util functions + +import sys, os, re, time, subprocess +from queue import Queue +import threading + +from ast import literal_eval +import os +import sys +from contextlib import contextmanager +from scapy.autorun import StringWriter + +from scapy.config import conf +from scapy.layers.tls.automaton_srv import TLSServerAutomaton + +conf.verb = 4 +conf.debug_tls = True +conf.debug_dissector = 2 +load_layer("tls") + +@contextmanager +def captured_output(): + old_out, old_err = sys.stdout, sys.stderr + new_out, new_err = StringWriter(debug=old_out), StringWriter(debug=old_out) + try: + sys.stdout, sys.stderr = new_out, new_err + yield sys.stdout, sys.stderr + finally: + sys.stdout, sys.stderr = old_out, old_err + +def check_output_for_data(out, err, expected_data): + errored = err.s.strip() + if errored: + return (False, errored) + output = out.s.strip() + if expected_data: + expected_data = plain_str(expected_data) + print("Testing for output: '%s'" % expected_data) + p = re.compile(r"> Received: b?'([^']*)'") + for s in p.finditer(output): + if s: + data = s.group(1) + print("Found: %s" % data) + if expected_data in data: + return (True, data) + return (False, output) + else: + return (False, None) + + +def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth=False, + psk=None, handle_session_ticket=False, sigalgo="rsa"): + correct = False + print("Server started !") + with captured_output() as (out, err): + # Prepare automaton + if sigalgo == "rsa": + mycert = scapy_path("/test/scapy/layers/tls/pki/srv_cert.pem") + mykey = scapy_path("/test/scapy/layers/tls/pki/srv_key.pem") + elif sigalgo == "ed25519": + mycert = scapy_path("/test/scapy/layers/tls/pki/srv_cert_ed25519.pem") + mykey = scapy_path("/test/scapy/layers/tls/pki/srv_key_ed25519.pem") + else: + raise ValueError + print(mykey) + print(mycert) + assert os.path.exists(mycert) + assert os.path.exists(mykey) + kwargs = dict() + if psk: + kwargs["psk"] = psk + kwargs["psk_mode"] = "psk_dhe_ke" + t = TLSServerAutomaton(mycert=mycert, + mykey=mykey, + curve=curve, + cookie=cookie, + client_auth=client_auth, + handle_session_ticket=handle_session_ticket, + debug=4, + **kwargs) + # Sync threads + q.put(t) + # Run server automaton + t.run() + # Return correct answer + res = check_output_for_data(out, err, expected_data) + # Return data + q.put(res) + + +def wait_tls_test_server_online(): + t = time.time() + while True: + if time.time() - t > 1: + raise RuntimeError("Server socket failed to start in time") + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(1) + s.connect(("127.0.0.1", 4433)) + s.shutdown(socket.SHUT_RDWR) + s.close() + return + except IOError: + try: + s.close() + except: + pass + continue + + +def run_openssl_client(msg, suite="", version="", tls13=False, client_auth=False, + psk=None, sess_out=None): + # Run client + CA_f = scapy_path("/test/scapy/layers/tls/pki/ca_cert.pem") + mycert = scapy_path("/test/scapy/layers/tls/pki/cli_cert.pem") + mykey = scapy_path("/test/scapy/layers/tls/pki/cli_key.pem") + args = [ + "openssl", "s_client", + "-connect", "127.0.0.1:4433", "-debug", + "-ciphersuites" if tls13 else "-cipher", suite, + version, + "-CAfile", CA_f + ] + if client_auth: + args.extend(["-cert", mycert, "-key", mykey]) + if psk: + args.extend(["-psk", str(psk)]) + if sess_out: + args.extend(["-sess_out", sess_out]) + p = subprocess.Popen( + " ".join(args), + shell=True, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + msg += b"\nstop_server\n" + out = p.communicate(input=msg)[0] + print(plain_str(out)) + if p.returncode != 0: + raise RuntimeError("OpenSSL returned with error code %s" % p.returncode) + else: + p = re.compile(br'verify return:(\d+)') + _failed = False + _one_success = False + for match in p.finditer(out): + if match.group(1).strip() != b"1": + _failed = True + break + else: + _one_success = True + break + if _failed or not _one_success: + raise RuntimeError("OpenSSL returned unexpected values") + +def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=None, curve=None, sigalgo="rsa"): + msg = ("TestS_%s_data" % suite).encode() + # Run server + q_ = Queue() + th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), + kwargs={"curve": curve, "cookie": False, "client_auth": client_auth, + "psk": psk, "sigalgo": sigalgo}, + name="test_tls_server %s %s" % (suite, version), daemon=True) + th_.start() + # Synchronise threads + print("Synchronising...") + atmtsrv = q_.get(timeout=5) + if not atmtsrv: + raise RuntimeError("Server hanged on startup") + wait_tls_test_server_online() + print("Thread synchronised") + # Run openssl client + run_openssl_client(msg, suite=suite, version=version, tls13=tls13, client_auth=client_auth, psk=psk) + # Wait for server + ret = q_.get(timeout=5) + if not ret: + raise RuntimeError("Test timed out") + atmtsrv.stop() + print(ret) + assert ret[0] + ++ TLS server automaton tests +~ server needs_root + += Testing TLS server with TLS 1.0 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA +~ open_ssl_client + +test_tls_server("ECDHE-RSA-AES128-SHA", "-tls1") + += Testing TLS server with TLS 1.1 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA +~ open_ssl_client + +test_tls_server("ECDHE-RSA-AES128-SHA", "-tls1_1") + += Testing TLS server with TLS 1.2 and TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 +~ open_ssl_client + +test_tls_server("DHE-RSA-AES128-SHA256", "-tls1_2") + += Testing TLS server with TLS 1.2 and TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 +~ open_ssl_client + +test_tls_server("ECDHE-RSA-AES256-GCM-SHA384", "-tls1_2") + += Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 +~ open_ssl_client + +test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True) + += Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 with x448 curve (+HelloRetryRequest) +~ open_ssl_client + +test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True, curve="x448") + += Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 with Ed25519-signed cert +~ open_ssl_client + +test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True, sigalgo="ed25519") + += Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 and client auth +~ open_ssl_client + +test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True, client_auth=True) + += Testing TLS server with TLS 1.3 and ECDHE-PSK-AES256-CBC-SHA384 and PSK +~ open_ssl_client + +test_tls_server("ECDHE-PSK-AES256-CBC-SHA384", "-tls1_3", tls13=False, psk="1a2b3c4d") + ++ TLS client automaton tests +~ client + += Load client utils functions + +import sys, os, time, threading + +from scapy.layers.tls.automaton_cli import TLSClientAutomaton +from scapy.layers.tls.handshake import TLSClientHello, TLS13ClientHello + +from queue import Queue + +send_data = cipher_suite_code = version = None + +def run_tls_test_client(send_data=None, cipher_suite_code=None, version=None, + client_auth=False, key_update=False, stop_server=True, + session_ticket_file_out=None, session_ticket_file_in=None): + print("Loading client...") + mycert = scapy_path("/test/scapy/layers/tls/pki/cli_cert.pem") if client_auth else None + mykey = scapy_path("/test/scapy/layers/tls/pki/cli_key.pem") if client_auth else None + commands = [send_data] + if key_update: + commands.append(b"key_update") + if stop_server: + commands.append(b"stop_server") + if session_ticket_file_out: + commands.append(b"wait") + commands.append(b"quit") + if version == "0002": + t = TLSClientAutomaton(data=commands, version="sslv2", debug=4, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) + elif version == "0304": + ch = TLS13ClientHello(ciphers=int(cipher_suite_code, 16)) + t = TLSClientAutomaton(client_hello=ch, data=commands, version="tls13", debug=4, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) + else: + ch = TLSClientHello(version=int(version, 16), ciphers=int(cipher_suite_code, 16)) + t = TLSClientAutomaton(client_hello=ch, data=commands, debug=4, mycert=mycert, mykey=mykey, + session_ticket_file_in=session_ticket_file_in, + session_ticket_file_out=session_ticket_file_out) + print("Running client...") + t.run() + +def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, + key_update=False, sess_in_out=False, sigalgo="rsa"): + msg = ("TestC_%s_data" % suite).encode() + # Run server + q_ = Queue() + print("Starting server...") + th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), + kwargs={"curve": None, "cookie": False, "client_auth": client_auth, + "handle_session_ticket": sess_in_out, "sigalgo": sigalgo}, + name="test_tls_client %s %s" % (suite, version), daemon=True) + th_.start() + # Synchronise threads + print("Synchronising...") + atmtsrv = q_.get(timeout=5) + if not atmtsrv: + raise RuntimeError("Server hanged on startup") + wait_tls_test_server_online() + print("Thread synchronised") + # Run client + if sess_in_out: + file_sess = scapy_path("/test/session") + run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_out=file_sess, + stop_server=False) + run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_in=file_sess, + stop_server=True) + else: + run_tls_test_client(msg, suite, version, client_auth, key_update) + # Wait for server + print("Client running, waiting...") + ret = q_.get(timeout=5) + if not ret: + raise RuntimeError("Test timed out") + atmtsrv.stop() + print(ret) + assert ret[0] + += Testing TLS server and client with SSLv2 and SSL_CK_DES_192_EDE3_CBC_WITH_MD5 + +test_tls_client("0700c0", "0002") + += Testing TLS server and client with SSLv2 and SSL_CK_RC2_128_CBC_EXPORT40_WITH_MD5 + +test_tls_client("040080", "0002") + += Testing TLS client with SSLv3 and TLS_RSA_EXPORT_WITH_RC4_40_MD5 + +test_tls_client("0003", "0300") + += Testing TLS client with TLS 1.0 and TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA + +test_tls_client("0088", "0301") + += Testing TLS client with TLS 1.0 and TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5 + +test_tls_client("0006", "0301") + += Testing TLS client with TLS 1.1 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA + +test_tls_client("c013", "0302") + += Testing TLS client with TLS 1.2 and TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 + +test_tls_client("009e", "0303") + += Testing TLS client with TLS 1.2 and TLS_ECDH_anon_WITH_RC4_128_SHA + +test_tls_client("c016", "0303") + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_GCM_SHA256 + +test_tls_client("1301", "0304") + += Testing TLS server and client with TLS 1.3 and TLS_CHACHA20_POLY1305_SHA256 +~ crypto_advanced + +test_tls_client("1303", "0304") + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 +~ crypto_advanced + +test_tls_client("1305", "0304") + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and x448 +~ crypto_advanced + +test_tls_client("1305", "0304", curve="x448") + += Testing TLS server and client with TLS 1.3 and a retry +~ crypto_advanced + +test_tls_client("1302", "0304", curve="secp256r1", cookie=True) + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 with Ed25519-signed cert +~ open_ssl_client + +test_tls_client("1305", "0304", sigalgo="ed25519") + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and client auth +~ crypto_advanced + +test_tls_client("1305", "0304", client_auth=True) + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and key update +~ crypto_advanced + +test_tls_client("1305", "0304", key_update=True) + += Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and session resumption +~ crypto_advanced not_pypy + +test_tls_client("1305", "0304", client_auth=True, sess_in_out=True) + += Clear session file + +file_sess = scapy_path("/test/session") +try: + os.remove(file_sess) +except: + pass + +############ +############ ++ TLS client automaton tests against builtin ssl using Post Handshake Authentication +~ client post_handshake_auth + += Load native server util functions + +# Imports + +import ssl +import contextlib +import threading + +load_layer("tls") +load_layer("http") + +# Define PKI + +root_ca_cert = hex_bytes("0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d4949446c7a4343416e2b6741774942416749555664642b794d436278356772635441773335717939337552517841774451594a4b6f5a496876634e4151454c0a42514177577a454c4d416b474131554542684d4351554578436a414942674e564241674d41574578436a414942674e564241634d41574578436a414942674e560a42416f4d41574578436a414942674e564241734d41574578436a414942674e5642414d4d415745784544414f42676b71686b69473977304243514557415745770a4868634e4d6a4d774e5445344d444d7a4d4455305768634e4d7a67774e5445354d444d7a4d445530576a42624d517377435159445651514745774a425154454b0a4d41674741315545434177425954454b4d41674741315545427777425954454b4d41674741315545436777425954454b4d41674741315545437777425954454b0a4d4167474131554541777742595445514d41344743537147534962334451454a4152594259544343415349774451594a4b6f5a496876634e41514542425141440a676745504144434341516f4367674542414a37775a326b6457577a6b6277725838565176743565747a55587737577967664970475038786543483632446979690a354a48546b3352716a6531444362476369566b4b386956746439507852475478764a6a476a49694b686a3545306e304c336542513771466c6567374a6d3147750a507a4154455779456f6a773975513343794c4f76395742374574434e626647476334544f564649635742684e5a5777324e306e37533834546f435a4942366c4e0a4c4c583639646f65684a33372b55457455553159775a4a474d72586a435653502b6f3136436568306c4d466e6553594d6a376c434b49426666525278725765720a354763733577423548574d636d6630626e774471534d78374d566a746f663678506b7570495039526f497977306b324f71516c4543612b4855556451306346590a564a53506d63424b554e6336787254756c346e447136442b6563594f7461754854726c36326e55434177454141614e544d464577485159445652304f424259450a4650786e62526467356a436549742b65556d314342695245583536334d42384741315564497751594d4261414650786e62526467356a436549742b65556d31430a42695245583536334d41384741315564457745422f7751464d414d42416638774451594a4b6f5a496876634e4151454c4251414467674542414876625a7a572b0a767553313239393268774442424a67586938386f426955787459383931556839364e77315876586841685873745338775551643749497a62795251626b6866530a424e6d626f59656e6b6b4272462b37474e696e394630564c516f7a344c67414c566e376c763635414f51554d7357503859694238563841516c6c447a305a2f770a69335a78423631436c50694f4d347a6e4a6a33324263794f50594267456b4a6c695143503854514c68555067504f742f7a4130453873584e56757354563976690a3168356d6e77332f4248572f52524e79496642365938336c5939345a577933754a72514d674352633957344a5076644e564a61494b38694241743258533276740a5665634a4b6942785347474a4564486561774b6a542f5674736b64432b3357696f756430527652716c7745622f4a50686b686553576d4a6b70436545773253720a6e6f64314c4c346b6a574159344c633d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a") +rsa_cert = hex_bytes("0a2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494944666a4343416d616741774942416749424154414e42676b71686b69473977304241517346414442624d517377435159445651514745774a425154454b0a4d41674741315545434177425954454b4d41674741315545427777425954454b4d41674741315545436777425954454b4d41674741315545437777425954454b0a4d4167474131554541777742595445514d41344743537147534962334451454a4152594259544165467730794d7a41314d5467774d7a51354d445261467730790a4f4441314d5463774d7a51354d4452614d467378437a414a42674e5642415954416b4a434d516f774341594456515149444146694d516f7743415944565151480a444146694d516f77434159445651514b444146694d516f77434159445651514c444146694d516f774341594456515144444146694d5241774467594a4b6f5a490a6876634e41516b42466746694d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b43415145413647667370784a570a655a366231313741312f6f637668303368706e6e366e6b5064487a5a33387956784b4f586a38505a4f4659794d79676a6546742f625a644a6a4b4179716432520a6c4374397a76716b3067306346336552373756457a626b724b6f7a384e73506757566577496e5933436c5633313367666b4e755955652f73666259303448376f0a5455694a73392f524c383975746a444e742b6d7259544f62426e4c7036734546774a646574426f694e6a623767693631363641763471576c50556d5a5331796b0a69386e385867554e5131535a5a4d4776497a4138556148433034684a556c342f4a5944622f51665551715034316464426d3877677252726b553176384136346b0a6a543344334954766f7234516e4b6b61436a32675853486658306e42636e4a644759572f484a38642f426e2b47714f6b324d5a515636656649722b4f6b5948330a7448575753543271676f6c6930514944415141426f303077537a414a42674e5648524d45416a41414d4230474131556444675157424254754631747a507a557a0a6b726471483838483850443354485269637a416642674e5648534d4547444157674254385a323058594f59776e694c666e6c4a745167596b52462b65747a414e0a42676b71686b6947397730424151734641414f4341514541484278614d6d68744a5035524d306b48595932486952755862635677455a2b6a46745968636252460a53484d32562f59526d55576f324f78666236574c727679482f65703552792f525a4c737261426a4e53495749394774462b3457794c305949482b52436e3235550a35316a34724e587269484d5a6c2f796375686d7456496c754a4f4d6a67572b44684b6b4568726e307a674653537654636c797a6843726653556f52595a7a362b0a474e305a705476486f35512f746d72752f6f6c47695a4271464d30554d4e4f4577444251586c68645964365134313479793574616c2f524f4c424b64595949420a534744696b552b356a75764e613761686e6f726365314c5a6d6d6e332b576530673052792f73362f39555135577339336f39635136335458654775773078674b0a7a496744627a38534948634c2b747559784b68364357636b4f436b67366e564e63616b45554c2f3243674b687a413d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a") +rsa_key = hex_bytes("0a2d2d2d2d2d424547494e2050524956415445204b45592d2d2d2d2d0a4d494945766749424144414e42676b71686b6947397730424151454641415343424b67776767536b41674541416f49424151446f5a2b796e456c5a356e7076580a587344582b68792b485465476d65667165513930664e6e667a4a58456f35655077396b34566a497a4b434e34573339746c306d4d6f444b70335a47554b33334f0a2b71545344527758643548767455544e755373716a507732772b425a56374169646a634b5658665865422b5132356852372b7839746a54676675684e53496d7a0a333945767a3236324d4d3233366174684d35734763756e71775158416c313630476949324e7675434c7258726f432f6970615539535a6c4c584b534c796678650a42513144564a6c6b7761386a4d4478526f634c5469456c53586a386c674e7639423952436f2f6a56313047627a43437447755254572f77447269534e506350630a684f2b697668436371526f4b506142644964396653634679636c305a686238636e783338476634616f365459786c4258703538697634365267666530645a5a4a0a5061714369574c5241674d42414145436767454142756750447342516768446f317475357744617555394774394b6e4f5958665973667444685553726c4754370a3173373436465646624d3259704f73576763543778507054627877477832713179644e77676b364237637045383770464563454669364241795962614a7241320a414e777355726f4c55356a2b425363617a63714e765162365a336141727656457a774532665539394d7a47786c31776e612b6a5152716d4a456f764c466a66310a68584841786e4d6765514f73556c6f506e6833682f4159774b3934385444732b634a4b4a33776a376a6335794a66456e70352f73784268433165356738594f450a563671426c682f702f3462615074757a49726a324d384f44566772304661624945362b537530577a4c6366597a50432b35536930543345673735672f736e666b0a724473703743517a55644973696d3443485432627a44483656775749774271386d4f645961766e592f514b426751442b764d626b414d54714c2f4d482f70614c0a46672f505272322f502b384c745a555247593477414138566c4b4334664342473250544a474837475231546559386e5a466d584878526561534a4667365855690a6153534f484b39586d2f43715962477664624a7553426f42492f6562566264706c504454376143374a52697766704176504d7a516b6552326d36556775516e720a6b49474376584f2f673874525357494e6d68354e5a46364533514b42675144706a732f78783531423753544c386d5946544e7147506a52316669697635684b2b0a492b6255643975585a33527445503078666e682f344f6c682b7a6c664d596b7a49356c376a68384c74326a6b31364978426a38376e774366566c636b5044464d0a516c4f624a676376383632364a5843377745666c3837594e77524d426b5238776964685a774b5052464a79395072315270782b715176507054483633704368770a704f435a7273514d68514b4267472b73334e6936435a6e4e575a4d6f706d446c5642722f6e56484a756f64386e4a5135697438364e324b7a6e4e346a394a5a360a714a3238636c2b4569413153322f7569325134434e7232356b4a7057337259754f41746851664637654c2b4a517264304e72776f4f645a454b566e6338794b440a58437a636f546c4b49772f452f487270416256794d434662544d4953764f6d626d567479714e724e38595636555655374f6f75644d393631416f4742414a4d630a6f5635706e5751704f3051374b6f657349506a74745a314d4764537831707874674c6654787a3157724c38474e48553464433459504f69366c536967797771720a49634878677879654b6a50366e753743514a494e56526349433175486a6f573651573834524d3676626e34526c7a4372724a33724a49454658444e67645954640a54716b3537665745526a58746a74496673704a4d4764615a6d446554377555453958505834535542416f4742414e4466535966544239774330334859415846550a78553554682f763075387a7a2b7235477a586863342b33513446746769336b51743164682f702b47384c764257744b65354d622f6651424c77514154613143330a735837786863612b66553467642f536638526a6a54783634696b413545585147306c6443696a6c4463554c4f5868386d4557574d636b2b333932416648584a740a4a687951526b427a453941664339526f642b61365455686f0a2d2d2d2d2d454e442050524956415445204b45592d2d2d2d2d0a") + +cafile = get_temp_file() +certfile = get_temp_file() +keyfile = get_temp_file() + +with open(cafile, "wb") as fd: + fd.write(root_ca_cert) + +with open(certfile, "wb") as fd: + fd.write(rsa_cert) + +with open(keyfile, "wb") as fd: + fd.write(rsa_key) + +# Define server + +REQS = [ + HTTP() / HTTPRequest(Path="/a.txt", Host="127.0.0.1:59000") / b"hey1", + HTTP() / HTTPRequest(Path="/b.txt", Host="127.0.0.1:59000") / b"hey2", +] + +RESPS = [ + HTTP() / HTTPResponse(Status_Code="401", Reason_Phrase="Unauthorized") / "Please login", + HTTP() / HTTPResponse(Status_Code="200", Reason_Phrase="OK") / "Welcome", +] + +def run_tls_native_test_server(post_handshake_auth=False, + with_hello_retry=False): + # Create + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.load_verify_locations(cafile=cafile) + if post_handshake_auth: + context.post_handshake_auth = True + if with_hello_retry: + context.set_ecdh_curve("prime256v1") + context.verify_mode = ssl.CERT_REQUIRED + context.load_cert_chain(certfile=certfile, keyfile=keyfile) + + port = [None] + lock = threading.Lock() + lock.acquire() + + def ssl_server(): + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.settimeout(1) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(("0.0.0.0", 0)) + server.listen(5) + port[0] = server.getsockname()[1] + # Sync + lock.release() + # Accept socket + client_socket, addr = server.accept() + ssl_client_socket = context.wrap_socket(client_socket, server_side=True) + # Receive / send data + resp = ssl_client_socket.read(len(REQS[0])) + assert resp == bytes(REQS[0]) + ssl_client_socket.send(bytes(RESPS[0])) + if post_handshake_auth: + # Post-handshake + t = ssl_client_socket.verify_client_post_handshake() + # Receive / send data + resp = ssl_client_socket.read(len(REQS[1])) + assert resp == bytes(REQS[1]) + ssl_client_socket.send(bytes(RESPS[1])) + # close socket + try: + ssl_client_socket.shutdown(socket.SHUT_RDWR) + finally: + ssl_client_socket.close() + try: + server.shutdown(socket.SHUT_RDWR) + finally: + server.close() + + server = threading.Thread(target=ssl_server) + server.start() + assert lock.acquire(timeout=5), "Server failed to start in time !" + return server, port[0] + + +def test_tls_client_native(post_handshake_auth=False, + with_hello_retry=False): + server, port = run_tls_native_test_server( + post_handshake_auth=post_handshake_auth, + with_hello_retry=with_hello_retry, + ) + + a = TLSClientAutomaton.tlslink( + HTTP, + server="127.0.0.1", + dport=port, + version="tls13", + mycert=certfile, + mykey=keyfile, + # we select x25519 but the server enforces seco256r1, so a Hello Retry will be issued + curve="x25519" if with_hello_retry else None, + # debug=4, + ) + # First request + pkt = a.sr1(REQS[0], timeout=1, verbose=0) + assert pkt.load == b"Please login" + # Second request + a.send(REQS[1]) + pkt = a.sr1(REQS[1], timeout=1, verbose=0) + assert pkt.load == b"Welcome" + # Close + a.close() + # Wait for server to close + server.join(3) + assert not server.is_alive() + + +# XXX: Ugh, Appveyor uses an ancient Windows 10 build that doesn't support TLS 1.3 natively. + += Testing TLS client against ssl.SSLContext server with TLS 1.3 and a post-handshake authentication +~ native_tls13 + +test_tls_client_native(post_handshake_auth=True) + += Testing TLS client against ssl.SSLContext server with TLS 1.3 and a Hello-Retry request +~ native_tls13 + +test_tls_client_native(with_hello_retry=True) + +# Automaton as Socket tests + ++ TLSAutomatonClient socket tests +~ netaccess needs_root + += Connect to google.com + +load_layer("tls") +load_layer("http") + +def _test_connection(): + a = TLSClientAutomaton.tlslink(HTTP, server="www.google.com", dport=443, + server_name="www.google.com", debug=4) + pkt = a.sr1(HTTP()/HTTPRequest(Host="www.google.com"), + session=TCPSession(app=True), timeout=2, retry=3) + a.close() + assert pkt + assert HTTPResponse in pkt + assert b"" in pkt[HTTPResponse].load + +retry_test(_test_connection) diff --git a/test/scapy/layers/usb.uts b/test/scapy/layers/usb.uts index bf7fc5f29e1..1ef2aaf197f 100644 --- a/test/scapy/layers/usb.uts +++ b/test/scapy/layers/usb.uts @@ -8,8 +8,10 @@ load_layer("usb") = linklayer test +from io import BytesIO + data = b"\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\x00\x00\xf9\x00\x00\x00\xb6\xaau[B\xd7\n\x00'\x00\x00\x00'\x00\x00\x00\x1b\x00\x008\xeeM\n\x97\xff\xff\x00\x00\x00\x00\t\x00\x01\x01\x00\x04\x00\x81\x01\x0c\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbd\xaau[\xdc\x88\x0c\x00$\x00\x00\x00$\x00\x00\x00\x1c\x000g4K\n\x97\xff\xff\x00\x00\x00\x00\x0b\x00\x00\x01\x00\x05\x00\x00\x02\x08\x00\x00\x00\x00\x80\x06\x00\x01\x00\x00\x12\x00\xbd\xaau[}\xa7\x0c\x00.\x00\x00\x00.\x00\x00\x00\x1c\x000g4K\n\x97\xff\xff\x00\x00\x00\x00\x0b\x00\x01\x01\x00\x05\x00\x00\x02\x12\x00\x00\x00\x01\x12\x01\x10\x02\x00\x00\x00@^\x04\xe8\x07\x07\x02\x01\x02\x00\x01\xbd\xaau[\x7f\xa7\x0c\x00\x1c\x00\x00\x00\x1c\x00\x00\x00\x1c\x000g4K\n\x97\xff\xff\x00\x00\x00\x00\x0b\x00\x01\x01\x00\x05\x00\x00\x02\x00\x00\x00\x00\x02\xbd\xaau[\x8d\xa7\x0c\x00$\x00\x00\x00$\x00\x00\x00\x1c\x00\x10\xe0\x98J\n\x97\xff\xff\x00\x00\x00\x00\x0b\x00\x00\x01\x00\x05\x00\x00\x02\x08\x00\x00\x00\x00\x80\x06\x00\x02\x00\x00\t\x00" -pcap = rdpcap(six.BytesIO(data)) +pcap = rdpcap(BytesIO(data)) pkt1 = USBpcap(function=9, info=1, endpoint=129, res=0, transfer=1, usbd_status=0, dataLength=12, bus=1, device=4, irpId=18446628669245765632, headerLen=27)/Raw(load=b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') assert raw(pcap[0]) == raw(pkt1) @@ -34,55 +36,3 @@ assert raw(pkt) == b"'\x00u\x925\x00\x00\x00\x00\x00\x00\x00\x00\x005\x12\n#\x00 pkt = USBpcap(irpId=0x359275, function=0x1235, info=10, bus=35)/USBpcapTransferControl(stage=11) assert raw(pkt) == b'\x1c\x00u\x925\x00\x00\x00\x00\x00\x00\x00\x00\x005\x12\n#\x00\x00\x00\x00\x02\x01\x00\x00\x00\x0b' - -= mocked get_usbpcap_interfaces() -~ mock windows - -import mock - -@mock.patch("scapy.layers.usb.subprocess.Popen") -def test_get_usbpcap_interfaces(mock_Popen): - conf.prog.usbpcapcmd = "C:/the_program_is_not_installed__test_only" - data = """ -interface {value=\\\\.\\USBPcap1}{display=USBPcap1} -""" - mock_Popen.side_effect = lambda *args, **kwargs: Bunch(returncode=0, communicate=(lambda *args, **kargs: (data,None))) - assert get_usbpcap_interfaces() == [('\\\\.\\USBPcap1', 'USBPcap1')] - - -test_get_usbpcap_interfaces() - -= mocked get_usbpcap_devices() -~ mock windows - -import mock - -@mock.patch("scapy.layers.usb.subprocess.Popen") -def test_get_usbpcap_devices(mock_Popen): - conf.prog.usbpcapcmd = "C:/the_program_is_not_installed__test_only" - data = """ -arg {number=0}{call=--snaplen}{display=Snapshot length}{tooltip=Snapshot length}{type=integer}{range=0,65535}{default=65535} -arg {number=1}{call=--bufferlen}{display=Capture buffer length}{tooltip=USBPcap kernel-mode capture buffer length in bytes}{type=integer}{range=0,134217728}{default=1048576} -arg {number=2}{call=--capture-from-all-devices}{display=Capture from all devices connected}{tooltip=Capture from all devices connected despite other options}{type=boolflag}{default=true} -arg {number=3}{call=--capture-from-new-devices}{display=Capture from newly connected devices}{tooltip=Automatically start capture on all newly connected devices}{type=boolflag}{default=true} -arg {number=99}{call=--devices}{display=Attached USB Devices}{tooltip=Select individual devices to capture from}{type=multicheck} -value {arg=99}{value=2}{display=[2] Marvell AVASTAR Bluetooth Radio Adapter}{enabled=true} -value {arg=99}{value=3}{display=[3] Peripherique d entree USB}{enabled=true} -value {arg=99}{value=3_1}{display=Surface Type Cover Filter Device}{enabled=false}{parent=3} -value {arg=99}{value=3_2}{display=Souris HID}{enabled=false}{parent=3} -value {arg=99}{value=3_3}{display=Peripherique de control consommateur conforme aux Peripheriques d'interface utilisateur (HID)}{enabled=false}{parent=3} -value {arg=99}{value=3_4}{display=Surface Pro 4 Type Cover Integration}{enabled=false}{parent=3} -value {arg=99}{value=3_5}{display=Surface Keyboard Backlight}{enabled=false}{parent=3_4} -value {arg=99}{value=3_6}{display=Surface Pro 4 Firmware Update}{enabled=false}{parent=3_4} -value {arg=99}{value=3_7}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_8}{display=Surface PTP Filter}{enabled=false}{parent=3} -value {arg=99}{value=3_9}{display=Microsoft Input Configuration Device}{enabled=false}{parent=3} -value {arg=99}{value=3_10}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_11}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -value {arg=99}{value=3_12}{display=Peripherique fournisseur HID}{enabled=false}{parent=3} -""" - mock_Popen.side_effect = lambda *args, **kwargs: Bunch(returncode=0, communicate=(lambda *args, **kargs: (data,None))) - assert get_usbpcap_devices('\\\\.\\USBPcap1') == [('2', '[2] Marvell AVASTAR Bluetooth Radio Adapter', True),('3', '[3] Peripherique d entree USB', True)] - - -test_get_usbpcap_devices() diff --git a/test/scapy/layers/x509.uts b/test/scapy/layers/x509.uts index fce5496c94e..cc561089b34 100644 --- a/test/scapy/layers/x509.uts +++ b/test/scapy/layers/x509.uts @@ -17,7 +17,7 @@ p = ASN1P_PRIVSEQ(s) + Private RSA & ECDSA keys class tests = Key class : Importing DER encoded RSA private key from scapy.layers.x509 import RSAPrivateKey -k = base64_bytes('MIIEowIBAAKCAQEAmFdqP+nTEZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5QReDbb2L5/HL\nA9pPmIeQLSq/BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3yilvYJ4W9\n/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA1o32zpuFBrJd\nI8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkmTL1SGhzwfinM\nE1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvzAwIDAQABAoIBAH3KeJZL2hhI\n/1GXNMaU/PfDgFkgmYbxMA8JKusnm/SFjxAwBGnGI6UjBXpBgpQs2Nqm3ZseF9u8hmCKvGiCEX2G\nesCo2mSfmSQxD6RBrMTuQ99UXpxzBIscFnM/Zrs8lPBARGzmF2nI3qPxXtex4ABX5o0Cd4NfZlZj\npj96skUoO8+bd3I4OPUFYFFFuv81LoSQ6Hew0a8xtJXtKkDp9h1jTGGUOc189WACNoBLH0MGeVoS\nUfc1++RcC3cypUZ8fNP1OO6GBfv06f5oXES4ZbxGYpa+nCfNwb6V2gWbkvaYm7aFn0KWGNZXS1P3\nOcWv6IWdOmg2CI7MMBLJ0LyWVCECgYEAyMJYw195mvHl8VyxJ3HkxeQaaozWL4qhNQ0Kaw+mzD+j\nYdkbHb3aBYghsgEDZjnyOVblC7I+4smvAZJLWJaf6sZ5HAw3zmj1ibCkXx7deoRc/QVcOikl3dE/\nymO0KGJNiGzJZmxbRS3hTokmVPuxSWW4p5oSiMupFHKa18Uv8DECgYEAwkJ7iTOUL6b4e3lQuHQn\nJbsiQpd+P/bsIPP7kaaHObewfHpfOOtIdtN4asxVFf/PgW5uWmBllqAHZYR14DEYIdL+hdLrdvk5\nnYQ3YfhOnp+haHUPCdEiXrRZuGXjmMA4V0hL3HPF5ZM8H80fLnN8Pgn2rIC7CZQ46y4PnoV1nXMC\ngYBBwCUCF8rkDEWa/ximKo8aoNJmAypC98xEa7j1x3KBgnYoHcrbusok9ajTe7F5UZEbZnItmnsu\nG4/Nm/RBV1OYuNgBb573YzjHl6q93IX9EkzCMXc7NS7JrzaNOopOj6OFAtwTR3m89oHMDu8W9jfi\nKgaIHdXkJ4+AuugrstE4gQKBgFK0d1/8g7SeA+Cdz84YNaqMt5NeaDPXbsTA23QxUBU0rYDxoKTd\nFybv9a6SfA83sCLM31K/A8FTNJL2CDGA9WNBL3fOSs2GYg88AVBGpUJHeDK+0748OcPUSPaG+pVI\nETSn5RRgffq16r0nWYUvSdAn8cuTqw3y+yC1pZS6AU8dAoGBAL5QCi0dTWKN3kf3cXaCAnYiWe4Q\ng2S+SgLE+F1U4Xws2rqAuSvIiuT5i5+Mqk9ZCGdoReVbAovJFoRqe7Fj9yWM+b1awGjL0bOTtnqx\n0iljob6uFyhpl1xgW3a3ICJ/ZYLvkgb4IBEteOwWpp37fX57vzhW8EmUV2UX7ve1uNRI') +k = base64.b64decode('MIIEowIBAAKCAQEAmFdqP+nTEZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5QReDbb2L5/HL\nA9pPmIeQLSq/BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3yilvYJ4W9\n/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA1o32zpuFBrJd\nI8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkmTL1SGhzwfinM\nE1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvzAwIDAQABAoIBAH3KeJZL2hhI\n/1GXNMaU/PfDgFkgmYbxMA8JKusnm/SFjxAwBGnGI6UjBXpBgpQs2Nqm3ZseF9u8hmCKvGiCEX2G\nesCo2mSfmSQxD6RBrMTuQ99UXpxzBIscFnM/Zrs8lPBARGzmF2nI3qPxXtex4ABX5o0Cd4NfZlZj\npj96skUoO8+bd3I4OPUFYFFFuv81LoSQ6Hew0a8xtJXtKkDp9h1jTGGUOc189WACNoBLH0MGeVoS\nUfc1++RcC3cypUZ8fNP1OO6GBfv06f5oXES4ZbxGYpa+nCfNwb6V2gWbkvaYm7aFn0KWGNZXS1P3\nOcWv6IWdOmg2CI7MMBLJ0LyWVCECgYEAyMJYw195mvHl8VyxJ3HkxeQaaozWL4qhNQ0Kaw+mzD+j\nYdkbHb3aBYghsgEDZjnyOVblC7I+4smvAZJLWJaf6sZ5HAw3zmj1ibCkXx7deoRc/QVcOikl3dE/\nymO0KGJNiGzJZmxbRS3hTokmVPuxSWW4p5oSiMupFHKa18Uv8DECgYEAwkJ7iTOUL6b4e3lQuHQn\nJbsiQpd+P/bsIPP7kaaHObewfHpfOOtIdtN4asxVFf/PgW5uWmBllqAHZYR14DEYIdL+hdLrdvk5\nnYQ3YfhOnp+haHUPCdEiXrRZuGXjmMA4V0hL3HPF5ZM8H80fLnN8Pgn2rIC7CZQ46y4PnoV1nXMC\ngYBBwCUCF8rkDEWa/ximKo8aoNJmAypC98xEa7j1x3KBgnYoHcrbusok9ajTe7F5UZEbZnItmnsu\nG4/Nm/RBV1OYuNgBb573YzjHl6q93IX9EkzCMXc7NS7JrzaNOopOj6OFAtwTR3m89oHMDu8W9jfi\nKgaIHdXkJ4+AuugrstE4gQKBgFK0d1/8g7SeA+Cdz84YNaqMt5NeaDPXbsTA23QxUBU0rYDxoKTd\nFybv9a6SfA83sCLM31K/A8FTNJL2CDGA9WNBL3fOSs2GYg88AVBGpUJHeDK+0748OcPUSPaG+pVI\nETSn5RRgffq16r0nWYUvSdAn8cuTqw3y+yC1pZS6AU8dAoGBAL5QCi0dTWKN3kf3cXaCAnYiWe4Q\ng2S+SgLE+F1U4Xws2rqAuSvIiuT5i5+Mqk9ZCGdoReVbAovJFoRqe7Fj9yWM+b1awGjL0bOTtnqx\n0iljob6uFyhpl1xgW3a3ICJ/ZYLvkgb4IBEteOwWpp37fX57vzhW8EmUV2UX7ve1uNRI') x=RSAPrivateKey(k) = Key class : key version @@ -53,7 +53,7 @@ x.coefficient == ASN1_INTEGER(13364209135497709980522851534062695694375984073722 + X509_Cert class tests = Cert class : Importing DER encoded X.509 Certificate with RSA public key from scapy.layers.x509 import X509_Cert -c = base64_bytes('MIIFEjCCA/qgAwIBAgIJALRecEPnCQtxMA0GCSqGSIb3DQEBBQUAMIG2MQswCQYDVQQGEwJGUjEO\nMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMRcwFQYDVQQKEw5NdXNocm9vbSBDb3JwLjEe\nMBwGA1UECxMVTXVzaHJvb20gVlBOIFNlcnZpY2VzMSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0\nIGNlcnRpZmljYXRlMScwJQYJKoZIhvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnAwHhcN\nMDYwNzEzMDczODU5WhcNMjYwMzMwMDczODU5WjCBtjELMAkGA1UEBhMCRlIxDjAMBgNVBAgTBVBh\ncmlzMQ4wDAYDVQQHEwVQYXJpczEXMBUGA1UEChMOTXVzaHJvb20gQ29ycC4xHjAcBgNVBAsTFU11\nc2hyb29tIFZQTiBTZXJ2aWNlczElMCMGA1UEAxMcSUtFdjIgWC41MDkgVGVzdCBjZXJ0aWZpY2F0\nZTEnMCUGCSqGSIb3DQEJARYYaWtldjItdGVzdEBtdXNocm9vbS5jb3JwMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAmFdqP+nTEZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5QReD\nbb2L5/HLA9pPmIeQLSq/BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3y\nilvYJ4W9/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA1o32\nzpuFBrJdI8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkmTL1S\nGhzwfinME1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvzAwIDAQABo4IBHzCC\nARswHQYDVR0OBBYEFPPYTt6Q9+Zd0s4zzVxWjG+XFDFLMIHrBgNVHSMEgeMwgeCAFPPYTt6Q9+Zd\n0s4zzVxWjG+XFDFLoYG8pIG5MIG2MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNV\nBAcTBVBhcmlzMRcwFQYDVQQKEw5NdXNocm9vbSBDb3JwLjEeMBwGA1UECxMVTXVzaHJvb20gVlBO\nIFNlcnZpY2VzMSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0IGNlcnRpZmljYXRlMScwJQYJKoZI\nhvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnCCCQC0XnBD5wkLcTAMBgNVHRMEBTADAQH/\nMA0GCSqGSIb3DQEBBQUAA4IBAQA2zt0BvXofiVvHMWlftZCstQaawej1SmxrAfDB4NUM24NsG+UZ\nI88XA5XM6QolmfyKnNromMLC1+6CaFxjq3jC/qdS7ifalFLQVo7ik/te0z6Olo0RkBNgyagWPX2L\nR5kHe9RvSDuoPIsbSHMmJA98AZwatbvEhmzMINJNUoHVzhPeHZnIaBgUBg02XULk/ElidO51Rf3g\nh8dR/kgFQSQT687vs1x9TWD00z0Q2bs2UF3Ob3+NYkEGEo5F9RePQm0mY94CT2xs6WpHo060Fo7f\nVpAFktMWx1vpu+wsEbQAhgGqV0fCR2QwKDIbTrPW/p9HJtJDYVjYdAFxr3s7V77y') +c = base64.b64decode('MIIFEjCCA/qgAwIBAgIJALRecEPnCQtxMA0GCSqGSIb3DQEBBQUAMIG2MQswCQYDVQQGEwJGUjEO\nMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMRcwFQYDVQQKEw5NdXNocm9vbSBDb3JwLjEe\nMBwGA1UECxMVTXVzaHJvb20gVlBOIFNlcnZpY2VzMSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0\nIGNlcnRpZmljYXRlMScwJQYJKoZIhvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnAwHhcN\nMDYwNzEzMDczODU5WhcNMjYwMzMwMDczODU5WjCBtjELMAkGA1UEBhMCRlIxDjAMBgNVBAgTBVBh\ncmlzMQ4wDAYDVQQHEwVQYXJpczEXMBUGA1UEChMOTXVzaHJvb20gQ29ycC4xHjAcBgNVBAsTFU11\nc2hyb29tIFZQTiBTZXJ2aWNlczElMCMGA1UEAxMcSUtFdjIgWC41MDkgVGVzdCBjZXJ0aWZpY2F0\nZTEnMCUGCSqGSIb3DQEJARYYaWtldjItdGVzdEBtdXNocm9vbS5jb3JwMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAmFdqP+nTEZukS0lLP+yj1gNImsEIf7P2ySTunceYxwkm4VE5QReD\nbb2L5/HLA9pPmIeQLSq/BgO1meOcbOSJ2YVHQ28MQ56+8Crb6n28iycX4hp0H3AxRAjh0edX+q3y\nilvYJ4W9/NnIb/wAZwS0oJif/tTkVF77HybAfJde5Eqbp+bCKIvMWnambh9DRUyjrBBZo5dA1o32\nzpuFBrJdI8dmUpw9gtf0F0Ba8lGZm8Uqc0GyXeXOJUE2u7CiMu3M77BM6ZLLTcow5+bQImkmTL1S\nGhzwfinME1e6p3Hm//pDjuJvFaY22k05LgLuyqc59vFiB3Toldz8+AbMNjvzAwIDAQABo4IBHzCC\nARswHQYDVR0OBBYEFPPYTt6Q9+Zd0s4zzVxWjG+XFDFLMIHrBgNVHSMEgeMwgeCAFPPYTt6Q9+Zd\n0s4zzVxWjG+XFDFLoYG8pIG5MIG2MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNV\nBAcTBVBhcmlzMRcwFQYDVQQKEw5NdXNocm9vbSBDb3JwLjEeMBwGA1UECxMVTXVzaHJvb20gVlBO\nIFNlcnZpY2VzMSUwIwYDVQQDExxJS0V2MiBYLjUwOSBUZXN0IGNlcnRpZmljYXRlMScwJQYJKoZI\nhvcNAQkBFhhpa2V2Mi10ZXN0QG11c2hyb29tLmNvcnCCCQC0XnBD5wkLcTAMBgNVHRMEBTADAQH/\nMA0GCSqGSIb3DQEBBQUAA4IBAQA2zt0BvXofiVvHMWlftZCstQaawej1SmxrAfDB4NUM24NsG+UZ\nI88XA5XM6QolmfyKnNromMLC1+6CaFxjq3jC/qdS7ifalFLQVo7ik/te0z6Olo0RkBNgyagWPX2L\nR5kHe9RvSDuoPIsbSHMmJA98AZwatbvEhmzMINJNUoHVzhPeHZnIaBgUBg02XULk/ElidO51Rf3g\nh8dR/kgFQSQT687vs1x9TWD00z0Q2bs2UF3Ob3+NYkEGEo5F9RePQm0mY94CT2xs6WpHo060Fo7f\nVpAFktMWx1vpu+wsEbQAhgGqV0fCR2QwKDIbTrPW/p9HJtJDYVjYdAFxr3s7V77y') x=X509_Cert(c) = Cert class : Rebuild certificate @@ -150,12 +150,48 @@ except: else: assert False += Cert class: Import Windows AD certificate +from scapy.layers.x509 import X509_Cert +c = base64.b64decode('MIIHKjCCBRKgAwIBAgITEgAAAAerpFLcIBwL6QAAAAAABzANBgkqhkiG9w0BAQsFADBHMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxFjAUBgoJkiaJk/IsZAEZFgZkb21haW4xFjAUBgNVBAMTDWRvbWFpbi1EQzEtQ0EwHhcNMjQwNDMwMTEyOTA5WhcNMjUwNDMwMTEyOTA5WjAbMRkwFwYDVQQDExBEQzEuZG9tYWluLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTvRYsSLoBJnHA+L62fgLUTN0JmBGONhz4qduRWBcpqOJIivxK2AcPThr8xdVcS5T80vUaT2SIzSvSp2RGdDbBWYGhRpZKkuCGA94PBYowb6aZuWF3RCm3kyySa/hisx4rlly+oERMtjvtgIHFAodu14gtA4YwKDwUwHY2bAE2Btxfsqrmzk8ezGpEB7/wO83zhLbc05ZMD43VwUEmTS5RSE2/1B/6gnO1KeAOrvUD6aiybvWKLNaEKsecsmqay60S+kFGcnXyji/CSv78URaetkJ7mRqPDR5E9DnWjfgAFBOYPoGE/XlV2duo3vBzasYIQtkBZvqeb9n/PkbIKmbQIDAQABo4IDOTCCAzUwLwYJKwYBBAGCNxQCBCIeIABEAG8AbQBhAGkAbgBDAG8AbgB0AHIAbwBsAGwAZQByMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNVHQ8BAf8EBAMCBaAweAYJKoZIhvcNAQkPBGswaTAOBggqhkiG9w0DAgICAIAwDgYIKoZIhvcNAwQCAgCAMAsGCWCGSAFlAwQBKjALBglghkgBZQMEAS0wCwYJYIZIAWUDBAECMAsGCWCGSAFlAwQBBTAHBgUrDgMCBzAKBggqhkiG9w0DBzAdBgNVHQ4EFgQU1vUiq6+MemfH69K9TnY2VDcBzdIwHwYDVR0jBBgwFoAUP8rKky+uwfavmkn3YezKPryPZXkwgcgGA1UdHwSBwDCBvTCBuqCBt6CBtIaBsWxkYXA6Ly8vQ049ZG9tYWluLURDMS1DQSxDTj1EQzEsQ049Q0RQLENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZpZ3VyYXRpb24sREM9ZG9tYWluLERDPWxvY2FsP2NlcnRpZmljYXRlUmV2b2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmlidXRpb25Qb2ludDCBwAYIKwYBBQUHAQEEgbMwgbAwga0GCCsGAQUFBzAChoGgbGRhcDovLy9DTj1kb21haW4tREMxLUNBLENOPUFJQSxDTj1QdWJsaWMlMjBLZXklMjBTZXJ2aWNlcyxDTj1TZXJ2aWNlcyxDTj1Db25maWd1cmF0aW9uLERDPWRvbWFpbixEQz1sb2NhbD9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTA8BgNVHREENTAzoB8GCSsGAQQBgjcZAaASBBBzEAh+YqaMQ5DcXUF1z8mXghBEQzEuZG9tYWluLmxvY2FsME0GCSsGAQQBgjcZAgRAMD6gPAYKKwYBBAGCNxkCAaAuBCxTLTEtNS0yMS0xOTI0MTM3MjE0LTM3MTg2NDYyNzQtNDAyMTU3MjEtMTAwMDANBgkqhkiG9w0BAQsFAAOCAgEAWwJuAQIRP3w9XheBdw+PgvMlfeIPV615Ce9C47HJto0kJOWtlBk3gF0WEjP7l8sToBU9v9L1zkczDh42XvSYSipv1q+20fRiXWQj0HqZRPt7yKcN3nnW4Foj6nFUlKjp8WIViQvJxUP2IP/SeblPRADry4AfRgxipq5rikl1PIQTH99u5MNEIePeP7apCcMizOd72RE/S9bPpQ4vB6vJ5T20YNSspHqC2qQnqOUqQwKrd+0i44bV4NANDPwv8wqzTvbDA9JMWm7sUanrl0x2yvfB9JyuZmo8y3JE7D8RFs/Z5btvWvQ4CWWIgVKnVncXOr98ytSaGNOift2NNz/2sox26Dgls4xklllnHiF2353IDSNPZqTNruWjUyM+4RuGKu6djqlaTneNEOi9Cu5HSE95JC03k9NhYyDW8PUIAWksLiWMYFng4KH37U9P15EiPsgPY70nP4ll6NqKt7RfXnSH7AmvacvY7dazsKOulAdzp8YuQ5vjR61FsbB/jn1hwtR7OdNYFKd9KK66zFSrX+n0sTXMou1FzvqDUj5+qLlbyEzYvU/QbNTxYUIjjNv+asXtD9T+UaKoI5PyeRBA4cnU7+klduy0vVh2Lx6lnIZPVCG7i1sQYRQQ3ESP7QSUuJtG/wgJZ5KspzfIHBjt62549oVj0CoJcvMZ2wOr8iY=') +x=X509_Cert(c) + += Cert class: Check some Windows-specific extensions +tbs = x.tbsCertificate +ext = tbs.extensions +assert type(ext) is list +assert len(ext) == 10 + +assert [x[0].extnID.oidname for x in ext] == [ + 'ENROLL_CERTTYPE', + 'extKeyUsage', + 'keyUsage', + 'smimeCapabilities', + 'subjectKeyIdentifier', + 'authorityKeyIdentifier', + 'cRLDistributionPoints', + 'authorityInfoAccess', + 'subjectAltName', + 'NTDS_CA_SECURITY_EXT', +] +assert ext[0].extnValue.Name == b'\x00D\x00o\x00m\x00a\x00i\x00n\x00C\x00o\x00n\x00t\x00r\x00o\x00l\x00l\x00e\x00r' +assert ext[1].extnValue.extendedKeyUsage[0].oid == '1.3.6.1.5.5.7.3.2' +assert ext[6].extnValue.cRLDistributionPoints[0].distributionPoint.distributionPointName.fullName[0].generalName.uniformResourceIdentifier == b'ldap:///CN=domain-DC1-CA,CN=DC1,CN=CDP,CN=Public%20Key%20Services,CN=Services,CN=Configuration,DC=domain,DC=local?certificateRevocationList?base?objectClass=cRLDistributionPoint' +assert ext[8].extnValue.subjectAltName[1].generalName.dNSName == b"DC1.domain.local" +assert ext[9].extnValue.value == b'S-1-5-21-1924137214-3718646274-40215721-1000' + += Cert class : X509 Certificate with rare fields types +cert_with_bmp_string = base64.b64decode('MIIB3DCCAaagAwIBAgIBATANBgkqhkiG9w0BAQsFADCB9jELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMQswCQYDVQQHEwJMRzEXMBUGA1UEChMOV2Vic2Vuc2UsIEluYy4xGjAYBgNVBAsTEVdlYnNlbnNlIEVuZHBvaW50MSMwIQYJKoZIhvcNAQkBFhRzdXBwb3J0QHdlYnNlbnNlLmNvbTE2MDQGA1UEAxMtV2Vic2Vuc2UgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MTswOQYDVQQNHjIAMQAyADQANgAxADgAMwA1ADEANABFAFAAQAB3AGUAYgBzAGUAbgBzAGUALgBjAG8AbTAeFw0yNDExMDUxMDA0MjlaFw0yNDExMDYxMDE0MjlaMEMxCzAJBgNVBAYTAkZSMRQwEgYDVQQKEwtTY2FweSwgSW5jLjEeMBwGA1UEAxMVU2NhcHkgRGVmYXVsdCBTdWJqZWN0MBowDQYJKoZIhvcNAQELBQADCQAwBgIBCgIBA6MTMBEwDwYDVR0TAQEABAUwAwEBADANBgkqhkiG9w0BAQsFAAMhAGRlZmF1bHRzaWduYXR1cmVkZWZhdWx0c2lnbmF0dXJl') +c = X509_Cert(cert_with_bmp_string) +bmp_field_value = str(c.tbsCertificate.issuer[7].rdn[0].value.val, "utf-16be") +assert bmp_field_value == '1246183514EP@websense.com' + + ############ CRL class ############################################### + X509_CRL class tests = CRL class : Importing DER encoded X.509 CRL from scapy.layers.x509 import X509_CRL -c = base64_bytes('MIICHjCCAYcwDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWdu\nLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\naG9yaXR5Fw0wNjExMDIwMDAwMDBaFw0wNzAyMTcyMzU5NTlaMIH2MCECECzSS2LEl6QXzW6jyJx6\nLcgXDTA0MDQwMTE3NTYxNVowIQIQOkXeVssCzdzcTndjIhvU1RcNMDEwNTA4MTkyMjM0WjAhAhBB\nXYg2gRUg1YCDRqhZkngsFw0wMTA3MDYxNjU3MjNaMCECEEc5gf/9hIHxlfnrGMJ8DfEXDTAzMDEw\nOTE4MDYxMlowIQIQcFR+auK62HZ/R6mZEEFeZxcNMDIwOTIzMTcwMDA4WjAhAhB+C13eGPI5ZoKm\nj2UiOCPIFw0wMTA1MDgxOTA4MjFaMCICEQDQVEhgGGfTrTXKLw1KJ5VeFw0wMTEyMTExODI2MjFa\nMA0GCSqGSIb3DQEBBQUAA4GBACLJ9rsdoaU9JMf/sCIRs3AGW8VV3TN2oJgiCGNEac9PRyV3mRKE\n0hmuIJTKLFSaa4HSAzimWpWNKuJhztsZzXUnWSZ8VuHkgHEaSbKqzUlb2g+o/848CvzJrcbeyEBk\nDCYJI5C3nLlQA49LGJ+w4GUPYBwaZ+WFxCX1C8kzglLm') +c = base64.b64decode('MIICHjCCAYcwDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWdu\nLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAxIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0\naG9yaXR5Fw0wNjExMDIwMDAwMDBaFw0wNzAyMTcyMzU5NTlaMIH2MCECECzSS2LEl6QXzW6jyJx6\nLcgXDTA0MDQwMTE3NTYxNVowIQIQOkXeVssCzdzcTndjIhvU1RcNMDEwNTA4MTkyMjM0WjAhAhBB\nXYg2gRUg1YCDRqhZkngsFw0wMTA3MDYxNjU3MjNaMCECEEc5gf/9hIHxlfnrGMJ8DfEXDTAzMDEw\nOTE4MDYxMlowIQIQcFR+auK62HZ/R6mZEEFeZxcNMDIwOTIzMTcwMDA4WjAhAhB+C13eGPI5ZoKm\nj2UiOCPIFw0wMTA1MDgxOTA4MjFaMCICEQDQVEhgGGfTrTXKLw1KJ5VeFw0wMTEyMTExODI2MjFa\nMA0GCSqGSIb3DQEBBQUAA4GBACLJ9rsdoaU9JMf/sCIRs3AGW8VV3TN2oJgiCGNEac9PRyV3mRKE\n0hmuIJTKLFSaa4HSAzimWpWNKuJhztsZzXUnWSZ8VuHkgHEaSbKqzUlb2g+o/848CvzJrcbeyEBk\nDCYJI5C3nLlQA49LGJ+w4GUPYBwaZ+WFxCX1C8kzglLm') x=X509_CRL(c) = CRL class : Rebuild crl @@ -257,6 +293,13 @@ assert responseData.producedAt == ASN1_GENERALIZED_TIME("20160914121000Z") assert len(responseData.responses) == 1 responseData.responseExtensions is None += OCSP class : OCSP ResponseData dissection with RecokedInfo +from scapy.layers.x509 import OCSP_ResponseData +pkt = OCSP_ResponseData(b"0\x81\xdf\xa2\x16\x04\x14\x11\x7f\x8eD\xbb\xe9\x7f\xca'\xfeG\x90\x89\\\x18\xea\x0e\xa5#W\x18\x0f20240121133708Z0\x81\x8e0\x81\x8b0M0\t\x06\x05+\x0e\x03\x02\x1a\x05\x00\x04\x14\x0b\xaf\xcc#$\xb8\xb0\xf8\xb02,\x9aPn9VSW\x14\x14\x04\x14\x11\x7f\x8eD\xbb\xe9\x7f\xca'\xfeG\x90\x89\\\x18\xea\x0e\xa5#W\x02\x14\x10&\x99j\t\xaa\xb9>\xde\x06\xb6#b\xa9\xe4GA\x07\x1b2\xa1\x16\x18\x0f20240120133708Z\xa0\x03\n\x01\x01\x18\x0f20240121133708Z\xa0\x11\x18\x0f20240122133708Z\xa1#0!0\x1f\x06\t+\x06\x01\x05\x05\x070\x01\x02\x04\x12\x04\x10\xfc\xb6\x92\xdf^\xf3\x03{\tH}\x12\x9f\xaa\x13^") +assert pkt.responderID.responderID.byKey == b"\x11\x7f\x8eD\xbb\xe9\x7f\xca'\xfeG\x90\x89\\\x18\xea\x0e\xa5#W" +assert pkt.responses[0].certID.issuerNameHash == b'\x0b\xaf\xcc#$\xb8\xb0\xf8\xb02,\x9aPn9VSW\x14\x14' +assert pkt.responses[0].certStatus.certStatus.revocationReason.cRLReason == 0x1 + = OCSP class : OCSP SingleResponse checks from scapy.layers.x509 import OCSP_GoodInfo singleResponse = responseData.responses[0] @@ -273,3 +316,10 @@ singleResponse.singleExtensions is None = OCSP class : OCSP Response reconstruction raw(response) == s += OSCP class : OSCP Response with ECDSA +response = OCSP_ResponseBytes() +assert bytes(response.signature) == b'\x03!\x00defaultsignaturedefaultsignature' +response.signatureAlgorithm.algorithm = ASN1_OID('1.2.840.10045.4.3.2') +assert bytes(response.signature) == b'\x03\t\x000\x06\x02\x01\x00\x02\x01\x00' +response = OCSP_ResponseBytes(bytes(response)) +assert isinstance(response.signature, ECDSASignature) diff --git a/test/sendsniff.uts b/test/sendsniff.uts index 9a692774378..4a69695f666 100644 --- a/test/sendsniff.uts +++ b/test/sendsniff.uts @@ -15,10 +15,7 @@ from threading import Thread tap0, tap1 = [TunTapInterface("tap%d" % i) for i in range(2)] -if six.PY2: - chk_kwargs = {} -else: - chk_kwargs = {"timeout": 3} +chk_kwargs = {"timeout": 3} if LINUX: for i in range(2): @@ -123,10 +120,7 @@ from threading import Thread tun0, tun1 = [TunTapInterface("tun%d" % i) for i in range(2)] -if six.PY2: - chk_kwargs = {} -else: - chk_kwargs = {"timeout": 3} +chk_kwargs = {"timeout": 3} if LINUX: for i in range(2): @@ -282,7 +276,7 @@ with VEthPair('a_0', 'a_1') as veth_0: = Create a tap interface -import mock +from unittest import mock import struct import subprocess from threading import Thread @@ -290,10 +284,7 @@ import time tap0 = TunTapInterface("tap0") -if six.PY2: - chk_kwargs = {} -else: - chk_kwargs = {"timeout": 3} +chk_kwargs = {"timeout": 3} if LINUX: assert subprocess.check_call(["ip", "link", "set", "tap0", "up"], **chk_kwargs) == 0 @@ -368,3 +359,78 @@ if conf.use_pypy: tap0.close() else: del tap0 + +##### +##### ++ Test sr() on multiple interfaces + += Setup multiple linux interfaces and ranges +~ linux needs_root dbg + +import os +exit_status = os.system("ip netns add blob0") +exit_status |= os.system("ip netns add blob1") +exit_status |= os.system("ip link add name scapy0.0 type veth peer name scapy0.1") +exit_status |= os.system("ip link add name scapy1.0 type veth peer name scapy1.1") +exit_status |= os.system("ip link set scapy0.1 netns blob0 up") +exit_status |= os.system("ip link set scapy1.1 netns blob1 up") +exit_status |= os.system("ip addr add 100.64.2.1/24 dev scapy0.0") +exit_status |= os.system("ip addr add 100.64.3.1/24 dev scapy1.0") +exit_status |= os.system("ip --netns blob0 addr add 100.64.2.2/24 dev scapy0.1") +exit_status |= os.system("ip --netns blob1 addr add 100.64.3.2/24 dev scapy1.1") +exit_status |= os.system("ip link set scapy0.0 up") +exit_status |= os.system("ip link set scapy1.0 up") +assert exit_status == 0 + +conf.ifaces.reload() +conf.route.resync() + +try: + pkts = sr(IP(dst=["100.64.2.2", "100.64.3.2"])/ICMP(), timeout=1)[0] + assert len(pkts) == 2 + assert pkts[0].answer.src in ["100.64.2.2", "100.64.3.2"] + assert pkts[1].answer.src in ["100.64.2.2", "100.64.3.2"] +finally: + e = os.system("ip netns del blob0") + e = os.system("ip netns del blob1") + conf.ifaces.reload() + conf.route.resync() + + += sr() performance test +~ linux needs_root veth not_pypy + +import subprocess +import shlex + +try: + # Create a dedicated network name space to simulate remote host + subprocess.check_call(shlex.split("sudo ip netns add scapy")) + # Create a virtual Ethernet pair to connect default and new NS + subprocess.check_call(shlex.split("sudo ip link add type veth")) + # Move veth1 to the new NS + subprocess.check_call(shlex.split("sudo ip link set veth1 netns scapy")) + # Setup vNIC in the default NS + subprocess.check_call(shlex.split("sudo ip link set veth0 up")) + subprocess.check_call(shlex.split("sudo ip addr add 192.168.168.1/24 dev veth0")) + # Setup vNIC in the dedicated NS + subprocess.check_call(shlex.split("sudo ip netns exec scapy ip link set lo up")) + subprocess.check_call(shlex.split("sudo ip netns exec scapy ip link set veth1 up")) + subprocess.check_call(shlex.split("sudo ip netns exec scapy ip addr add 192.168.168.2/24 dev veth1")) + # Perform test + conf.route.resync() + res, unansw = sr(IP(dst='192.168.168.2') / ICMP(seq=(1, 1000)), timeout=1, verbose=False) +finally: + try: + # Bring down the interfaces + subprocess.check_call(shlex.split("sudo ip netns exec scapy ip link set veth1 down")) + subprocess.check_call(shlex.split("sudo ip netns exec scapy ip link set lo down")) + # Delete the namespace + subprocess.check_call(shlex.split("sudo ip netns delete scapy")) + # Remove the virtual Ethernet pair + subprocess.check_call(shlex.split("sudo ip link delete veth0")) + except subprocess.CalledProcessError as e: + print(f"Error during cleanup: {e}") + +len(res) == 1000 + diff --git a/test/testsocket.py b/test/testsocket.py index 81e71e16acf..d1a90a4da51 100644 --- a/test/testsocket.py +++ b/test/testsocket.py @@ -9,7 +9,6 @@ import time import random -from socket import socket from threading import Lock from scapy.config import conf @@ -17,9 +16,22 @@ from scapy.data import MTU from scapy.packet import Packet from scapy.error import Scapy_Exception -from scapy.compat import Optional, Type, Tuple, Any, List, cast + +# Typing imports +from typing import ( + Optional, + Type, + Tuple, + Any, + List, +) from scapy.supersocket import SuperSocket +from scapy.plist import ( + PacketList, + SndRcvList, +) + open_test_sockets = list() # type: List[TestSocket] @@ -50,6 +62,25 @@ def __exit__(self, exc_type, exc_value, traceback): """Close the socket""" self.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, threaded=False, **kargs) + + def sr1(self, *args, **kargs): + # type: (Any, Any) -> Optional[Packet] + """Send one packet and receive one answer + """ + 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 + def close(self): # type: () -> None global open_test_sockets @@ -127,8 +158,8 @@ def send(self, x): self.no_error_for_x_tx_pkts -= 1 return super(UnstableSocket, self).send(x) - def recv(self, x=MTU): - # type: (int) -> Optional[Packet] + def recv(self, x=MTU, **kwargs): + # type: (int, **Any) -> Optional[Packet] if self.no_error_for_x_tx_pkts == 0: if random.randint(0, 1000) == 42: self.no_error_for_x_tx_pkts = 10 @@ -144,7 +175,7 @@ def recv(self, x=MTU): return None if self.no_error_for_x_tx_pkts > 0: self.no_error_for_x_tx_pkts -= 1 - return super(UnstableSocket, self).recv(x) + return super(UnstableSocket, self).recv(x, **kwargs) def cleanup_testsockets(): diff --git a/test/tftp.uts b/test/tftp.uts deleted file mode 100644 index b8968457767..00000000000 --- a/test/tftp.uts +++ /dev/null @@ -1,191 +0,0 @@ -% Regression tests for TFTP - -# More information at http://www.secdev.org/projects/UTscapy/ - -+ TFTP coverage tests - -= Test answers - -assert TFTP_DATA(block=1).answers(TFTP_RRQ()) -assert not TFTP_WRQ().answers(TFTP_RRQ()) -assert not TFTP_RRQ().answers(TFTP_WRQ()) -assert TFTP_ACK(block=1).answers(TFTP_DATA(block=1)) -assert not TFTP_ACK(block=0).answers(TFTP_DATA(block=1)) -assert TFTP_ACK(block=0).answers(TFTP_RRQ()) -assert not TFTP_ACK().answers(TFTP_ACK()) -assert TFTP_ERROR().answers(TFTP_DATA()) and TFTP_ERROR().answers(TFTP_ACK()) -assert TFTP_OACK().answers(TFTP_WRQ()) - -+ TFTP Automatons -~ linux - -= Utilities -~ linux - -legacy_select_objects = None -def patch_select_objects(classname): - global legacy_select_objects - legacy_select_objects = select_objects - def mock_select_objects(inputs, remain): - test = [s for s in inputs if isinstance(s, classname)] - if test: - if len(test[0].packets): - return test - else: - inputs = [s for s in inputs if not isinstance(s, classname)] - return legacy_select_objects(inputs, remain) - scapy.automaton.select_objects = mock_select_objects - -class MockTFTPSocket(object): - packets = [] - def recv(self, n): - pkt = self.packets.pop(0) - return pkt - def send(self, *args, **kargs): - pass - - -= TFTP_read() automaton -~ linux - -class MockReadSocket(MockTFTPSocket): - packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=1) / ("P" * 512), - IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_DATA(block=2) / "<3"] - -patch_select_objects(MockReadSocket) - -tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807, - ll=MockReadSocket, - recvsock=MockReadSocket) - -res = tftp_read.run() -scapy.automaton.select_objects = legacy_select_objects -assert res == (b"P" * 512 + b"<3") - -= TFTP_read() automaton error -~ linux - -class MockReadSocket(MockTFTPSocket): - packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")] - -patch_select_objects(MockReadSocket) - -tftp_read = TFTP_read("file.txt", "1.2.3.4", sport=0x2807, - ll=MockReadSocket, - recvsock=MockReadSocket) - -try: - tftp_read.run() - assert False -except Automaton.ErrorState as e: - assert "Reached ERROR" in str(e) - assert "ERROR Access violation" in str(e) - -scapy.automaton.select_objects = legacy_select_objects - -= TFTP_write() automaton -~ linux - -data_received = b"" - -class MockWriteSocket(MockTFTPSocket): - packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=0), - IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ACK(block=1) ] - def send(self, *args, **kargs): - if len(args) and Raw in args[0]: - global data_received - data_received += args[0][Raw].load - -tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807, - ll=MockWriteSocket, - recvsock=MockWriteSocket) - -patch_select_objects(MockWriteSocket) -tftp_write.run() -scapy.automaton.select_objects = legacy_select_objects -assert data_received == (b"P" * 767 + b"Scapy <3") - -= TFTP_write() automaton error -~ linux - -class MockWriteSocket(MockTFTPSocket): - packets = [IP(src="1.2.3.4") / UDP(dport=0x2807) / TFTP_ERROR(errorcode=2, errormsg="Fatal error")] - -tftp_write = TFTP_write("file.txt", "P" * 767 + "Scapy <3", "1.2.3.4", sport=0x2807, - ll=MockWriteSocket, - recvsock=MockWriteSocket) - -patch_select_objects(MockWriteSocket) -try: - tftp_write.run() - assert False -except Automaton.ErrorState as e: - assert "Reached ERROR" in str(e) - assert "ERROR Access violation" in str(e) - -scapy.automaton.select_objects = legacy_select_objects - -= TFTP_WRQ_server() automaton -~ linux - -class MockWRQSocket(MockTFTPSocket): - packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt"), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 512), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"] - -tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807, - ll=MockWRQSocket, - recvsock=MockWRQSocket) -patch_select_objects(MockWRQSocket) -assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 512 + b"<3")) -scapy.automaton.select_objects = legacy_select_objects - -= TFTP_WRQ_server() automaton with options -~ linux - -class MockWRQSocket(MockTFTPSocket): - packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_WRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=1) / ("P" * 100), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_DATA(block=2) / "<3"] - -tftp_wrq = TFTP_WRQ_server(ip="1.2.3.4", sport=0x2807, - ll=MockWRQSocket, - recvsock=MockWRQSocket) -patch_select_objects(MockWRQSocket) -assert tftp_wrq.run() == (b"scapy.txt", (b"P" * 100 + b"<3")) -scapy.automaton.select_objects = legacy_select_objects - -= TFTP_RRQ_server() automaton -~ linux - -sent_data = "P" * 512 + "<3" -import tempfile -filename = tempfile.mktemp(suffix=".txt") -fdesc = open(filename, "w") -fdesc.write(sent_data) -fdesc.close() - -received_data = "" - -class MockRRQSocket(MockTFTPSocket): - packets = [IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename="scapy.txt") / TFTP_Options(options=[TFTP_Option(oname="blksize", value="100")]), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_RRQ(filename=filename[5:]) / TFTP_Options(), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=1), - IP(dst="1.2.3.4") / UDP(dport=0x2807) / TFTP() / TFTP_ACK(block=2) ] - def send(self, *args, **kargs): - if len(args): - pkt = args[0] - if TFTP_DATA in pkt: - global received_data - received_data += pkt[Raw].load.decode("utf-8") - -tftp_rrq = TFTP_RRQ_server(ip="1.2.3.4", sport=0x2807, dir="/tmp/", serve_one=True, - ll=MockRRQSocket, - recvsock=MockRRQSocket) -patch_select_objects(MockRRQSocket) -tftp_rrq.run() -scapy.automaton.select_objects = legacy_select_objects -assert received_data == sent_data - -import os -os.unlink(filename) diff --git a/test/tls/tests_tls_netaccess.uts b/test/tls/tests_tls_netaccess.uts deleted file mode 100644 index 834cfbf131a..00000000000 --- a/test/tls/tests_tls_netaccess.uts +++ /dev/null @@ -1,378 +0,0 @@ -% TLS session establishment tests - -~ crypto needs_root - -# More information at http://www.secdev.org/projects/UTscapy/ - -############ -############ -+ TLS server automaton tests -~ server - -= Load server util functions -~ client - -from __future__ import print_function - -import sys, os, re, time, subprocess -from scapy.libs.six.moves.queue import Queue -import threading - -from ast import literal_eval -import os -import sys -from contextlib import contextmanager -from scapy.autorun import StringWriter - -from scapy.libs import six - -from scapy.config import conf -from scapy.layers.tls.automaton_srv import TLSServerAutomaton - -conf.verb = 4 -conf.debug_tls = True -conf.debug_dissector = 2 -load_layer("tls") - -@contextmanager -def captured_output(): - old_out, old_err = sys.stdout, sys.stderr - new_out, new_err = StringWriter(debug=old_out), StringWriter(debug=old_out) - try: - sys.stdout, sys.stderr = new_out, new_err - yield sys.stdout, sys.stderr - finally: - sys.stdout, sys.stderr = old_out, old_err - -def check_output_for_data(out, err, expected_data): - errored = err.s.strip() - if errored: - return (False, errored) - output = out.s.strip() - if expected_data: - expected_data = plain_str(expected_data) - print("Testing for output: '%s'" % expected_data) - p = re.compile(r"> Received: b?'([^']*)'") - for s in p.finditer(output): - if s: - data = s.group(1) - print("Found: %s" % data) - if expected_data in data: - return (True, data) - return (False, output) - else: - return (False, None) - - -def run_tls_test_server(expected_data, q, curve=None, cookie=False, client_auth=False, - psk=None, handle_session_ticket=False): - correct = False - print("Server started !") - with captured_output() as (out, err): - # Prepare automaton - mycert = scapy_path("/test/tls/pki/srv_cert.pem") - mykey = scapy_path("/test/tls/pki/srv_key.pem") - print(mykey) - print(mycert) - assert os.path.exists(mycert) - assert os.path.exists(mykey) - kwargs = dict() - if psk: - kwargs["psk"] = psk - kwargs["psk_mode"] = "psk_dhe_ke" - t = TLSServerAutomaton(mycert=mycert, - mykey=mykey, - curve=curve, - cookie=cookie, - client_auth=client_auth, - handle_session_ticket=handle_session_ticket, - debug=5, - **kwargs) - # Sync threads - q.put(True) - # Run server automaton - t.run() - # Return correct answer - res = check_output_for_data(out, err, expected_data) - # Return data - q.put(res) - -def run_openssl_client(msg, suite="", version="", tls13=False, client_auth=False, - psk=None, sess_out=None): - # Run client - CA_f = scapy_path("/test/tls/pki/ca_cert.pem") - mycert = scapy_path("/test/tls/pki/cli_cert.pem") - mykey = scapy_path("/test/tls/pki/cli_key.pem") - args = [ - "openssl", "s_client", - "-connect", "127.0.0.1:4433", "-debug", - "-ciphersuites" if tls13 else "-cipher", suite, - version, - "-CAfile", CA_f - ] - if client_auth: - args.extend(["-cert", mycert, "-key", mykey]) - if psk: - args.extend(["-psk", str(psk)]) - if sess_out: - args.extend(["-sess_out", sess_out]) - p = subprocess.Popen( - " ".join(args), - shell=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) - msg += b"\nstop_server\n" - out = p.communicate(input=msg)[0] - print(plain_str(out)) - if p.returncode != 0: - raise RuntimeError("OpenSSL returned with error code %s" % p.returncode) - else: - p = re.compile(br'verify return:(\d+)') - _failed = False - _one_success = False - for match in p.finditer(out): - if match.group(1).strip() != b"1": - _failed = True - break - else: - _one_success = True - break - if _failed or not _one_success: - raise RuntimeError("OpenSSL returned unexpected values") - -def test_tls_server(suite="", version="", tls13=False, client_auth=False, psk=None): - msg = ("TestS_%s_data" % suite).encode() - # Run server - q_ = Queue() - th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), - kwargs={"curve": None, "cookie": False, "client_auth": client_auth, "psk": psk}, - name="test_tls_server %s %s" % (suite, version)) - th_.setDaemon(True) - th_.start() - # Synchronise threads - q_.get() - time.sleep(1) - # Run openssl client - run_openssl_client(msg, suite=suite, version=version, tls13=tls13, client_auth=client_auth, psk=psk) - # Wait for server - th_.join(5) - if th_.is_alive(): - raise RuntimeError("Test timed out") - # Analyse values - if q_.empty(): - raise RuntimeError("Missing return values") - ret = q_.get(timeout=5) - print(ret) - assert ret[0] - - -= Testing TLS server with TLS 1.0 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA -~ open_ssl_client - -test_tls_server("ECDHE-RSA-AES128-SHA", "-tls1") - -= Testing TLS server with TLS 1.1 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA -~ open_ssl_client - -test_tls_server("ECDHE-RSA-AES128-SHA", "-tls1_1") - -= Testing TLS server with TLS 1.2 and TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 -~ open_ssl_client - -test_tls_server("DHE-RSA-AES128-SHA256", "-tls1_2") - -= Testing TLS server with TLS 1.2 and TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 -~ open_ssl_client - -test_tls_server("ECDHE-RSA-AES256-GCM-SHA384", "-tls1_2") - -= Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 -~ open_ssl_client - -test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True) - -= Testing TLS server with TLS 1.3 and TLS_AES_256_GCM_SHA384 and client auth -~ open_ssl_client - -test_tls_server("TLS_AES_256_GCM_SHA384", "-tls1_3", tls13=True, client_auth=True) - -= Testing TLS server with TLS 1.3 and ECDHE-PSK-AES256-CBC-SHA384 and PSK -~ open_ssl_client - -test_tls_server("ECDHE-PSK-AES256-CBC-SHA384", "-tls1_3", tls13=False, psk="1a2b3c4d") - -+ TLS client automaton tests -~ client - -= Load client utils functions - -import sys, os, time, threading - -from scapy.layers.tls.automaton_cli import TLSClientAutomaton -from scapy.layers.tls.handshake import TLSClientHello, TLS13ClientHello - -from scapy.libs.six.moves.queue import Queue - -send_data = cipher_suite_code = version = None - -def run_tls_test_client(send_data=None, cipher_suite_code=None, version=None, - client_auth=False, key_update=False, stop_server=True, - session_ticket_file_out=None, session_ticket_file_in=None): - print("Loading client...") - mycert = scapy_path("/test/tls/pki/cli_cert.pem") if client_auth else None - mykey = scapy_path("/test/tls/pki/cli_key.pem") if client_auth else None - commands = [send_data] - if key_update: - commands.append(b"key_update") - if stop_server: - commands.append(b"stop_server") - if session_ticket_file_out: - commands.append(b"wait") - commands.append(b"quit") - if version == "0002": - t = TLSClientAutomaton(data=commands, version="sslv2", debug=5, mycert=mycert, mykey=mykey, - session_ticket_file_in=session_ticket_file_in, - session_ticket_file_out=session_ticket_file_out) - elif version == "0304": - ch = TLS13ClientHello(ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=commands, version="tls13", debug=5, mycert=mycert, mykey=mykey, - session_ticket_file_in=session_ticket_file_in, - session_ticket_file_out=session_ticket_file_out) - else: - ch = TLSClientHello(version=int(version, 16), ciphers=int(cipher_suite_code, 16)) - t = TLSClientAutomaton(client_hello=ch, data=commands, debug=5, mycert=mycert, mykey=mykey, - session_ticket_file_in=session_ticket_file_in, - session_ticket_file_out=session_ticket_file_out) - print("Running client...") - t.run() - -def test_tls_client(suite, version, curve=None, cookie=False, client_auth=False, - key_update=False, sess_in_out=False): - msg = ("TestC_%s_data" % suite).encode() - # Run server - q_ = Queue() - print("Starting server...") - th_ = threading.Thread(target=run_tls_test_server, args=(msg, q_), - kwargs={"curve": None, "cookie": False, "client_auth": client_auth, - "handle_session_ticket": sess_in_out}, - name="test_tls_client %s %s" % (suite, version)) - th_.setDaemon(True) - th_.start() - # Synchronise threads - print("Syncrhonising...") - assert q_.get(timeout=5) is True - time.sleep(1) - print("Thread synchronised") - # Run client - if sess_in_out: - file_sess = scapy_path("/test/session") - run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_out=file_sess, - stop_server=False) - run_tls_test_client(msg, suite, version, client_auth, key_update, session_ticket_file_in=file_sess, - stop_server=True) - else: - run_tls_test_client(msg, suite, version, client_auth, key_update) - # Wait for server - print("Client running, waiting...") - th_.join(5) - if th_.is_alive(): - raise RuntimeError("Test timed out") - # Return values - if q_.empty(): - raise RuntimeError("Missing return value") - ret = q_.get(timeout=5) - print(ret) - assert ret[0] - -= Testing TLS server and client with SSLv2 and SSL_CK_DES_192_EDE3_CBC_WITH_MD5 - -test_tls_client("0700c0", "0002") - -= Testing TLS client with SSLv3 and TLS_RSA_EXPORT_WITH_RC4_40_MD5 - -test_tls_client("0003", "0300") - -= Testing TLS client with TLS 1.0 and TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA - -test_tls_client("0088", "0301") - -= Testing TLS client with TLS 1.1 and TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA - -test_tls_client("c013", "0302") - -= Testing TLS client with TLS 1.2 and TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 - -test_tls_client("009e", "0303") - -= Testing TLS client with TLS 1.2 and TLS_ECDH_anon_WITH_RC4_128_SHA - -test_tls_client("c016", "0303") - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_GCM_SHA256 - -test_tls_client("1301", "0304") - -= Testing TLS server and client with TLS 1.3 and TLS_CHACHA20_POLY1305_SHA256 -~ crypto_advanced - -test_tls_client("1303", "0304") - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 -~ crypto_advanced - -test_tls_client("1305", "0304") - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and x448 -~ crypto_advanced - -test_tls_client("1305", "0304", curve="x448") - -= Testing TLS server and client with TLS 1.3 and a retry -~ crypto_advanced - -test_tls_client("1302", "0304", curve="secp256r1", cookie=True) - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and client auth -~ crypto_advanced - -test_tls_client("1305", "0304", client_auth=True) - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and key update -~ crypto_advanced - -test_tls_client("1305", "0304", key_update=True) - -= Testing TLS server and client with TLS 1.3 and TLS_AES_128_CCM_8_SHA256 and session resumption -~ crypto_advanced not_pypy - -test_tls_client("1305", "0304", client_auth=True, sess_in_out=True) - -= Clear session file - -file_sess = scapy_path("/test/session") -try: - os.remove(file_sess) -except: - pass - -# Automaton as Socket tests - -+ TLSAutomatonClient socket tests -~ netaccess - -= Connect to google.com - -load_layer("tls") -load_layer("http") - -def _test_connection(): - a = TLSClientAutomaton.tlslink(HTTP, server="www.google.com", dport=443, - server_name="www.google.com", debug=4) - pkt = a.sr1(HTTP()/HTTPRequest(Host="www.google.com"), - session=TCPSession(app=True), timeout=2, retry=3) - a.close() - assert pkt - assert HTTPResponse in pkt - assert b"" in pkt[HTTPResponse].load - -retry_test(_test_connection) diff --git a/test/tools/isotpscanner.uts b/test/tools/isotpscanner.uts index 830c0515f41..aefaac72d42 100644 --- a/test/tools/isotpscanner.uts +++ b/test/tools/isotpscanner.uts @@ -11,7 +11,7 @@ with open(scapy_path("test/contrib/automotive/interface_mockup.py")) as f: ISOTPSocket = ISOTPSoftSocket -from mock import patch +from unittest.mock import patch + Usage tests @@ -40,19 +40,6 @@ assert result.wait() == 0 assert expected_output in plain_str(std_out) -= Test wrong socket for Python2 or Windows - -if six.PY2: - version = subprocess.Popen(["python2", "--version"], stdout=subprocess.PIPE) - if 0 == version.wait(): - print(version.communicate()) - result = subprocess.Popen([sys.executable, "scapy/tools/automotive/isotpscanner.py", "-c", iface0, "-s", "0x600", "-e", "0x600"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - expected_output = plain_str(b'Please provide all required arguments.') - std_out, std_err = result.communicate() - assert result.wait() == 1 - assert expected_output in plain_str(std_err) - - = Test Python2 call result = subprocess.Popen([sys.executable, "scapy/tools/automotive/isotpscanner.py", "-i", "socketcan", "-c", iface0, "-s", "0x600", "-e", "0x600", "-v", "-n", "0", "-t", "0"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -120,7 +107,6 @@ for out in expected_output: = Test extended scan -~ python3_only def isotp_scan(sock, # type: SuperSocket scan_range=range(0x7ff + 1), # type: Iterable[int] @@ -153,7 +139,6 @@ with patch.object(sys, "argv", testargs), patch.object(scapy.contrib.isotp, "iso = Test scan with piso flag -~ python3_only def isotp_scan(sock, # type: SuperSocket scan_range=range(0x7ff + 1), # type: Iterable[int] diff --git a/test/tools/obdscanner.uts b/test/tools/obdscanner.uts index 190855616e0..1fe7ab31d26 100644 --- a/test/tools/obdscanner.uts +++ b/test/tools/obdscanner.uts @@ -30,34 +30,6 @@ std_out, std_err = result.communicate() expected_output = plain_str(b'Scan for all possible obd service classes and their subfunctions.') assert expected_output in plain_str(std_out) - -= Test wrong socket for Python2 or Windows -if six.PY2: - version = subprocess.Popen(["python2", "--version"], stdout=subprocess.PIPE) - result = subprocess.Popen([sys.executable, "scapy/tools/automotive/obdscanner.py", "-c", "vcan0", "-s", "0x600", "-d", "0x601", "-t", "0.001"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - assert result.wait() == 1 - expected_output = plain_str(b'Please provide all required arguments.') - std_out, std_err = result.communicate() - assert expected_output in plain_str(std_err) - -= Test Python2 call -if six.PY2: - result = subprocess.Popen([sys.executable, "scapy/tools/automotive/obdscanner.py", "-i", "socketcan", "-c", "vcan0", "-s", "0x600", "-d", "0x601", "-v", "-t", "0.001"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - returncode = result.wait() - std_out, std_err = result.communicate() - assert returncode == 0 - expected_output = plain_str(b'Starting OBD-Scan...') - assert expected_output in plain_str(std_out) - -= Test Python2 call with python-can args -if six.PY2: - result = subprocess.Popen([sys.executable, "scapy/tools/automotive/obdscanner.py", "-i", "socketcan", "-c", "vcan0", "-s", "0x600", "-d", "0x601", "-v", "-a", "bitrate=250000", "-t", "0.001"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - returncode = result.wait() - std_out, std_err = result.communicate() - assert returncode == 0 - expected_output = plain_str(b'Starting OBD-Scan...') - assert expected_output in plain_str(std_out) - + Scan tests = Load contribution layer diff --git a/test/tuntap.uts b/test/tuntap.uts index d97fc65e448..1ba470ea175 100644 --- a/test/tuntap.uts +++ b/test/tuntap.uts @@ -23,6 +23,26 @@ assert p.type == 0x86dd assert isinstance(p.payload, IPv6) +####### ++ Test Darwin-specific protocol headers for utun +~ osx utun not_libpcap + += Darwin-specific protocol headers + +p = DarwinUtunPacketInfo()/IP() +assert p.addr_family == 2 + +p = DarwinUtunPacketInfo(raw(p)) +assert p.addr_family == 2 +assert isinstance(p.payload, IP) + +p = DarwinUtunPacketInfo()/IPv6() +assert p.addr_family == 30 + +p = DarwinUtunPacketInfo(raw(p)) +assert p.addr_family == 30 +assert isinstance(p.payload, IPv6) + ####### + Test tun device @@ -80,8 +100,7 @@ if not LINUX: import subprocess -iface = resolve_iface("tun0") # test TunTapInterface on NetworkInterface -tun0 = TunTapInterface(iface, strip_packet_info=False) +tun0 = TunTapInterface("tun0", strip_packet_info=False) assert subprocess.check_call(["ip", "link", "set", "tun0", "up"]) == 0 assert subprocess.check_call([ @@ -97,7 +116,7 @@ conf.route6.resync() def cb(): send(IP(dst="192.0.2.2")/ICMP(seq=(1,3))) -t = AsyncSniffer(opened_socket=tun0, lfilter=lambda x: IP in x, started_callback=cb, count=3) +t = AsyncSniffer(opened_socket=tun0, lfilter=lambda x: ICMP in x, started_callback=cb, count=3) t.start() t.join(timeout=3) diff --git a/test/windows.uts b/test/windows.uts index eb7cedb5f93..394376be0c2 100644 --- a/test/windows.uts +++ b/test/windows.uts @@ -6,7 +6,7 @@ = Imports -import mock +from unittest import mock ############ ############ @@ -40,37 +40,49 @@ from scapy.config import conf assert dev_from_networkname(conf.iface.network_name).guid == conf.iface.guid = test pcap_service_status +~ npcap_service from scapy.arch.windows import pcap_service_status status = pcap_service_status() assert status += test get_if_list + +from scapy.interfaces import get_if_list + +print(get_if_list()) +assert all(x.startswith(r"\Device\NPF_") for x in get_if_list()) + = test pcap_service_stop -~ appveyor_only require_gui +~ ci_only require_gui npcap_service + +from scapy.arch.windows import pcap_service_stop pcap_service_stop() -assert pcap_service_status()[2] == False +assert pcap_service_status() == False = test pcap_service_start -~ appveyor_only require_gui +~ ci_only require_gui npcap_service + +from scapy.arch.windows import pcap_service_start pcap_service_start() -assert pcap_service_status()[2] == True +assert pcap_service_status() == True = Test auto-pcap start UI @mock.patch("scapy.arch.windows.get_windows_if_list") def _test_autostart_ui(mocked_getiflist): mocked_getiflist.side_effect = lambda: [] - IFACES.reload() - assert all(x.index < 0 for x in IFACES.data.values()) + conf.ifaces.reload() + assert all(x.index < 0 for x in conf.ifaces.data.values()) try: - old_ifaces = IFACES.data.copy() + old_ifaces = conf.ifaces.data.copy() _test_autostart_ui() finally: - IFACES.data = old_ifaces + conf.ifaces.data = old_ifaces ######### Native mode ########### @@ -79,39 +91,23 @@ finally: = Set up native mode conf.use_pcap = False +conf.route.resync() +conf.ifaces.reload() assert conf.use_pcap == False -= Prepare ping: open firewall & get current seq number -~ netaccess needs_root - -from scapy.arch.windows.native import open_icmp_firewall, get_current_icmp_seq - -# Note: this method is complicated, but allow us to perform a real test -# it is discouraged otherwise. Npcap/Winpcap does NOT require such mechanics - -# output of this may vary, but it doesn't matter: -# if it fails the teat below won't work -open_icmp_firewall("www.google.com") - -seq = get_current_icmp_seq() -assert seq > 0 - -True - = Ping -~ netaccess needs_root +~ netaccess needs_root icmp_firewall def _test(): with conf.L3socket() as a: - answer = a.sr1(IP(dst="www.google.com", ttl=128)/ICMP(id=1, seq=seq)/"abcdefghijklmnopqrstuvwabcdefghi", timeout=2) + answer = a.sr1(IP(dst="1.1.1.1", ttl=128)/ICMP()/"abcdefghijklmnopqrstuvwabcdefghi", timeout=2) answer.show() assert ICMP in answer retry_test(_test) = DNS lookup -~ netaccess needs_root require_gui -% XXX currently disabled +~ netaccess needs_root def _test(): answer = sr1(IP(dst="8.8.8.8")/UDP()/DNS(rd=1, qd=DNSQR(qname="www.google.com")), timeout=2) @@ -121,7 +117,30 @@ def _test(): retry_test(_test) += Test L3WinSocket close() with partial initialization +~ windows + +from scapy.arch.windows.native import L3WinSocket +import socket + +# Create partially initialized L3WinSocket +ws = object.__new__(L3WinSocket) +ws.closed = False +ws.promisc = True +# Note: ws.ins is intentionally not set + +# This should not raise AttributeError +try: + ws.close() + test_passed = True +except AttributeError: + test_passed = False + +assert test_passed, "L3WinSocket.close() raised AttributeError on partially initialized object" + = Leave native mode conf.use_pcap = True +conf.route.resync() +conf.ifaces.reload() assert conf.use_pcap == True diff --git a/tox.ini b/tox.ini index 7bdc3a49786..c3a06ac726a 100644 --- a/tox.ini +++ b/tox.ini @@ -2,53 +2,69 @@ # Copyright (C) 2020 Guillaume Valadon +# Tox environments: +# py{version}-{os}-{non_root,root} +# In our testing, version can be 37 to 313 or py39 for pypy39 + [tox] -envlist = py{27,34,35,36,37,38,39,310,py27,py39}-{linux,bsd}_{non_root,root}, - py{27,34,35,36,37,38,39,310,py27,py39}-windows, +# minversion = 4.0 skip_missing_interpreters = true -minversion = 2.9 +# envlist = default when doing 'tox' +envlist = py{37,38,39,310,311,312,313}-{linux,bsd,windows}-{non_root,root} # Main tests [testenv] description = "Scapy unit tests" -whitelist_externals = sudo +allowlist_externals = sudo parallel_show_output = true -passenv = PATH PWD PROGRAMFILES WINDIR SYSTEMROOT OPENSSL_CONF - # Used by scapy - SCAPY_USE_LIBPCAP -deps = mock - # cryptography requirements - setuptools>=18.5 +package = wheel +passenv = + PATH + PWD + PROGRAMFILES + WINDIR + SYSTEMROOT + OPENSSL_CONF + # Used by scapy + SCAPY_USE_LIBPCAP +deps = ipython cryptography - coverage + coverage[toml] python-can # disabled on windows because they require c++ dependencies - brotli ; sys_platform != 'win32' + # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 + brotli < 1.1.0 ; sys_platform != 'win32' zstandard ; sys_platform != 'win32' platform = - linux_non_root,linux_root: linux - bsd_non_root,bsd_root: darwin|freebsd|openbsd|netbsd + linux: linux + bsd: (darwin|freebsd|openbsd|netbsd).* windows: win32 commands = - linux_non_root: {envpython} {env:SCAPY_PY_OPTS:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc -N {posargs} - linux_root: sudo -E {envpython} {env:SCAPY_PY_OPTS:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc {posargs} - bsd_non_root: {envpython} {env:SCAPY_PY_OPTS:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark -N {posargs} - bsd_root: sudo -E {envpython} {env:SCAPY_PY_OPTS:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K manufdb -K tshark {posargs} - windows: {envpython} {env:SCAPY_PY_OPTS:-m coverage run} -m scapy.tools.UTscapy -c test/configs/windows.utsc {posargs} - coverage combine + linux-non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc -N {posargs} + linux-root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c ./test/configs/linux.utsc {posargs} + bsd-non_root: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K tshark -N {posargs} + bsd-root: sudo -E {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/bsd.utsc -K tshark {posargs} + windows: {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy -c test/configs/windows.utsc {posargs} + {env:DISABLE_COVERAGE:coverage combine} + {env:DISABLE_COVERAGE:coverage xml -i} # Variants of the main tests [testenv:py38-isotp_kernel_module] description = "Scapy unit tests - ISOTP Linux kernel module" -whitelist_externals = sudo +allowlist_externals = sudo git bash lsmod modprobe -passenv = PATH PWD PROGRAMFILES WINDIR SYSTEMROOT +passenv = + PATH + PWD + PROGRAMFILES + WINDIR + SYSTEMROOT deps = {[testenv]deps} commands = sudo apt-get -qy install build-essential linux-headers-$(uname -r) linux-modules-extra-$(uname -r) @@ -61,6 +77,7 @@ commands = lsmod sudo -E {envpython} -m coverage run -m scapy.tools.UTscapy -c ./test/configs/linux.utsc {posargs} coverage combine + coverage xml -i # Test used by upstream pyca/cryptography [testenv:cryptography] @@ -71,41 +88,33 @@ commands = python -c "import cryptography; print('DEBUG: cryptography %s' % cryptography.__version__)" python -m scapy.tools.UTscapy -c ./test/configs/cryptography.utsc -# Specific functions or tests - -[testenv:codecov] -description = "Upload coverage results to codecov" -passenv = TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* -deps = codecov -commands = codecov -e TOXENV - - # The files listed past the first argument of the sphinx-apidoc command are ignored [testenv:apitree] description = "Regenerates the API reference doc tree" skip_install = true -changedir = doc/scapy +changedir = {toxinidir}/doc/scapy deps = sphinx + cryptography commands = - sphinx-apidoc -f --no-toc -d 1 --separate --module-first --templatedir=_templates --output-dir api ../../scapy ../../scapy/modules/ ../../scapy/libs/ ../../scapy/tools/ ../../scapy/arch/ ../../scapy/contrib/scada/* ../../scapy/all.py ../../scapy/layers/all.py ../../scapy/compat.py + sphinx-apidoc -f --no-toc -d 1 --separate --module-first --templatedir=_templates --output-dir api ../../scapy ../../scapy/modules/voip.py ../../scapy/modules/krack/ ../../scapy/libs/winpcapy.py ../../scapy/libs/ethertypes.py ../../scapy/libs/bluetoothids.py ../../scapy/libs/m*.py ../../scapy/libs/structures.py ../../scapy/libs/test_pyx.py ../../scapy/tools/ ../../scapy/arch/ ../../scapy/contrib/scada/* ../../scapy/layers/msrpce/raw/ ../../scapy/layers/msrpce/all.py ../../scapy/all.py ../../scapy/layers/all.py ../../scapy/compat.py [testenv:mypy] description = "Check Scapy compliance against static typing" skip_install = true -deps = mypy==0.931 +deps = mypy==1.7.0 typing - types-mock commands = python .config/mypy/mypy_check.py linux python .config/mypy/mypy_check.py win32 [testenv:docs] description = "Build the docs" -skip_install = true -changedir = doc/scapy -deps = sphinx>=2.4.2 +deps = cryptography + sphinx sphinx_rtd_theme +extras = doc +changedir = {toxinidir}/doc/scapy commands = sphinx-build -W --keep-going -b html . _build/html @@ -113,8 +122,8 @@ commands = # Debug mode [testenv:docs2] description = "Build the docs without rebuilding the API tree" -skip_install = true -changedir = doc/scapy +extras = doc +changedir = {toxinidir}/doc/scapy deps = {[testenv:docs]deps} setenv = SCAPY_APITREE = 0 @@ -127,29 +136,41 @@ description = "Check code for Grammar mistakes" skip_install = true deps = codespell # inet6, dhcp6 and the ipynb files contains french: ignore them -commands = codespell --ignore-words=.config/codespell_ignore.txt --skip="*.pyc,*.png,*.jpg,*.ods,*.raw,*.pdf,*.pcap,*.js,*.html,*.der,*_build*,*inet6.py,*dhcp6.py,*.ipynb,*.svg,*.gif,*.obs,*.gz" scapy/ doc/ test/ .github/ +commands = codespell --ignore-words=.config/codespell_ignore.txt --skip="*.pyc,*.png,*.jpg,*.ods,*.raw,*.pdf,*.pcap,*.js,*.html,*.der,*_build*,*inet6.py,*dhcp6.py,*manuf.py,*tcpros.py,*bluetoothids.py,*.ipynb,*.svg,*.gif,*.obs,*.gz" scapy/ doc/ test/ .github/ [testenv:twine] description = "Check Scapy code distribution" skip_install = true deps = twine - setuptools>=38.6.0 cmarkgfm -commands = python setup.py --quiet sdist - twine check dist/* + build +setenv = SCAPY_VERSION=3.0.0 +commands = python -m build + twine check --strict dist/* + + +[testenv:gitarchive] +description = "Check Scapy git archive" +skip_install = true +allowlist_externals = git +commands = git version + git archive HEAD -o {envtmpdir}/scapy.tar + python -m pip install {envtmpdir}/scapy.tar + # Below: remove current folder from path to force use of installed Scapy + python -c "import sys; sys.path.remove(''); import scapy; print(scapy._version_from_git_archive())" [testenv:flake8] description = "Check Scapy code style & quality" skip_install = true -deps = flake8 +deps = flake8<6.0.0 commands = flake8 scapy/ # flake8 configuration [flake8] -ignore = E731, W504 +ignore = E203, E731, W504, W503 max-line-length = 88 per-file-ignores = scapy/all.py:F403,F401 @@ -167,6 +188,7 @@ per-file-ignores = scapy/layers/tls/crypto/all.py:F403 scapy/layers/tls/crypto/md4.py:E741 scapy/libs/winpcapy.py:F405,F403,E501 + scapy/libs/manuf.py:E501 scapy/tools/UTscapy.py:E501 -exclude = scapy/libs/six.py, - scapy/libs/ethertypes.py +exclude = scapy/libs/ethertypes.py, + scapy/layers/msrpce/raw/*