diff --git a/.gitignore b/.gitignore index f8233a9..e37d53a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,3 @@ *.pyc -__init__.py __pycache__ - -# Ignore built files -/.dshellrc -/bin/decode -/dshell -/dshell-decode +Dshell.egg-info diff --git a/Dshell-Training-Pack-0.1.tar.gz b/Dshell-Training-Pack-0.1.tar.gz new file mode 100644 index 0000000..5841d34 Binary files /dev/null and b/Dshell-Training-Pack-0.1.tar.gz differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c10e25f --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +© (2020) United States Government, as represented by the Secretary of the Army. All rights reserved. + +ICF Incorporated, L.L.C. contributed to the development of Dshell (Python 3). + +Because the project utilizes code licensed from contributors and other third parties, it therefore is licensed under the MIT License. http://opensource.org/licenses/mit-license.php. Under that license, permission is 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 conditions that any appropriate copyright notices and this permission notice are 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. + diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index b398bc9..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,5 +0,0 @@ -This project constitutes a work of the United States Government and is not subject to domestic copyright protection under 17 USC § 105. - -However, because the project utilizes code licensed from contributors and other third parties, it therefore is licensed under the MIT License. http://opensource.org/licenses/mit-license.php. Under that license, permission is 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 conditions that any appropriate copyright notices and this permission notice are 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. diff --git a/Makefile b/Makefile deleted file mode 100755 index 7dba82f..0000000 --- a/Makefile +++ /dev/null @@ -1,41 +0,0 @@ -default: all - -all: rc dshell - -dshell: rc initpy pydoc - -rc: - # Generating .dshellrc and dshell files - python $(PWD)/bin/generate-dshellrc.py $(PWD) - chmod 755 $(PWD)/dshell - chmod 755 $(PWD)/dshell-decode - chmod 755 $(PWD)/bin/decode.py - ln -s $(PWD)/bin/decode.py $(PWD)/bin/decode - -initpy: - find $(PWD)/decoders -type d -not -path \*.svn\* -print -exec touch {}/__init__.py \; - -pydoc: - (cd $(PWD)/doc && ./generate-doc.sh $(PWD) ) - -clean: clean_pyc clean_ln - -distclean: clean clean_py clean_pydoc clean_rc - -clean_rc: - rm -fv $(PWD)/dshell - rm -fv $(PWD)/dshell-decode - rm -fv $(PWD)/.dshellrc - -clean_ln: - rm -fv $(PWD)/bin/decode - -clean_py: - find $(PWD)/decoders -name '__init__.py' -exec rm -v {} \; - -clean_pyc: - find $(PWD)/decoders -name '*.pyc' -exec rm -v {} \; - find $(PWD)/lib -name '*.pyc' -exec rm -v {} \; - -clean_pydoc: - find $(PWD)/doc -name '*.htm*' -exec rm -v {} \; diff --git a/README b/README new file mode 100644 index 0000000..3c1feea --- /dev/null +++ b/README @@ -0,0 +1,207 @@ +# Dshell +An extensible network forensic analysis framework. Enables rapid development of plugins to support the dissection of network packet captures. + +Key features: +* Deep packet analysis using specialized plugins +* Robust stream reassembly +* IPv4 and IPv6 support +* Custom output handlers +* Chainable plugins + +## Requirements +* Linux (developed on Red Hat Enterprise Linux 6.7) +* Python 3 (developed with Python 3.5.1) +* [pypacker](https://github.com/mike01/pypacker) +* [pcapy](http://www.coresecurity.com/corelabs-research/open-source-tools/pcapy) +* [geoip2](https://github.com/maxmind/GeoIP2-python) + * [MaxMind GeoIP2 datasets](https://dev.maxmind.com/geoip/geoip2/geolite2/) + +## Optional +* [oui.txt](http://standards-oui.ieee.org/oui.txt) + * used by some plugins that handle MAC addresses + * place in <dshell>/data/ +* [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html) + * used in the elasticout output module + * only necessary if planning to use elasticsearch to store output + +## Major Changes Since Previous Release +* This is a major framework update to Dshell. Plugins written for the previous version are not compatible with this version, and vice versa. +* Uses Python 3 + * Rewritten in Python 3 from the ground up. Python 2 language deprecated on [1 JAN 2020](https://www.python.org/doc/sunset-python-2/) + * By extension, dpkt and pypcap have been replaced with Python3-friendly pypacker and pcapy (respectively). +* Is a Python package + * Converted into a single package, removing the need for the shell to set several environment variables. + * Allows easier use of Dshell plugins in other Python scripts +* Changed "decoders" to "plugins" + * Primarily a word-swap, to clarify that "decoders" can do more than simply decode traffic, and to put Dshell more in line with the terminology of other frameworks. +* Significant reduction in camelCase functions, replaced with more Pythonic snake\_case functions. + * Notable examples include blobHandler->blob\_handler, rawHandler->raw\_handler, connectionInitHandler->connection\_init\_handler, etc. +* All plugins are now chainable + * To accommodate this, handler functions in plugins must now use return statements indicating whether a packet, connection, or similar will continue to the next plugin. The type of object(s) to return depends on the type of handler, but will generally match the types of the handler's input. Dshell will display a warning if it's not the right type. +* Plugins can now use all output modules\* available to the command line switch, -O + * That does not mean every output module will be _useful_ to every plugin (e.g. using netflow output for a plugin that looks at individual packets), but they are available. + * alert(), write(), and dump() are now the same function: write() + * Output modules can be listed with a new flag in decode.py, --list-output or --lo + * Arguments for output modules are now passed with the --oargs command-line argument + * \* pcapout is (currently) the exception to this rule. A method has yet to arise that allows it to work with connection-based plugins +* No more dObj declaration + * decode.py just looks for the class named DshellPlugin and creates an instance of that +* Improved error handling + * Dshell handles more of the most common exceptions during everyday use +* Enables development of external plugin packs, allowing the sharing and installation of new, externally-developed plugins without overlapping the core Dshell libraries. + +## Installation + +1. Install Dshell with pip + * `sudo python3 -m pip install Dshell/` OR `sudo python3 -m pip install ` +2. Configure geoip2 by moving the MaxMind data files (GeoLite2-ASN.mmdb, GeoLite2-City.mmdb, GeoLite2-Country.mmdb) to <install-location>/data/GeoIP/ +3. Run `dshell`. This should drop you into a `Dshell> ` prompt. + +## Basic Usage + +* `decode -l` + * This will list all available plugins, alongside basic information about them +* `decode -h` + * Show generic command-line flags available to most plugins +* `decode -p ` + * Display information about a plugin, including available command line flags +* `decode -p ` + * Run the selected plugin on a pcap file +* `decode -p + ` + * Chain two (or more) plugins together and run them on a pcap file +* `decode -p -i ` + * Run the selected plugin live on an interface (may require superuser privileges) + +## Usage Examples +Showing DNS lookups in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) + +``` +Dshell> decode -p dns ~/pcap/dns.cap |sort +[DNS] 2005-03-30 03:47:46 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 4146, TXT? google.com., TXT: b'\x0fv=spf1 ptr ?all' ** +[DNS] 2005-03-30 03:47:50 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 63343, MX? google.com., MX: b'\x00(\x05smtp4\xc0\x0c', MX: b'\x00\n\x05smtp5\xc0\x0c', MX: b'\x00\n\x05smtp6\xc0\x0c', MX: b'\x00\n\x05smtp1\xc0\x0c', MX: b'\x00\n\x05smtp2\xc0\x0c', MX: b'\x00(\x05smtp3\xc0\x0c' ** +[DNS] 2005-03-30 03:47:59 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 18849, LOC? google.com. ** +[DNS] 2005-03-30 03:48:07 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 39867, PTR? 104.9.192.66.in-addr.arpa., PTR: 66-192-9-104.gen.twtelecom.net. ** +[DNS] 2005-03-30 03:49:18 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 30144, A? www.netbsd.org., A: 204.152.190.12 (ttl 82159s) ** +[DNS] 2005-03-30 03:49:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 61652, AAAA? www.netbsd.org., AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86400s) ** +[DNS] 2005-03-30 03:50:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 32569, AAAA? www.netbsd.org., AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86340s) ** +[DNS] 2005-03-30 03:50:44 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 36275, AAAA? www.google.com., CNAME: 'www.l.google.com.' ** +[DNS] 2005-03-30 03:50:54 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 56482, AAAA? www.l.google.com. ** +[DNS] 2005-03-30 03:51:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 48159, AAAA? www.example.com. ** +[DNS] 2005-03-30 03:51:46 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 9837, AAAA? www.example.notginh., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 65251, AAAA: 2001:4f8:0:2::d (ttl 600s), A: 204.152.184.88 (ttl 600s) ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32796 -- 192.168.170.20:53 ** ID: 23123, PTR? 1.0.0.127.in-addr.arpa., PTR: localhost. ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32797 -- 192.168.170.20:53 ** ID: 8330, NS: b'\x06ns-ext\x04nrt1\xc0\x0c', NS: b'\x06ns-ext\x04sth1\xc0\x0c', NS: b'\x06ns-ext\xc0\x0c', NS: b'\x06ns-ext\x04lga1\xc0\x0c' ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1707 -- 217.13.4.24:53 ** ID: 12910, SRV? _ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1708 -- 217.13.4.24:53 ** ID: 61793, SRV? _ldap._tcp.dc._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1709 -- 217.13.4.24:53 ** ID: 33633, SRV? _ldap._tcp.05b5292b-34b8-4fb7-85a3-8beef5fd2069.domains._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1710 -- 217.13.4.24:53 ** ID: 53344, A? GRIMM.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:25 192.168.170.56:1711 -- 217.13.4.24:53 ** ID: 30307, A? GRIMM.utelsystems.local., NXDOMAIN ** +``` + +Following and reassembling a stream in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) + +``` +Dshell> decode -p followstream ~/pcap/v6-http.cap +Connection 1 (TCP) +Start: 2007-08-05 15:16:44.189851 +End: 2007-08-05 15:16:44.219460 +2001:6f8:102d:0:2d0:9ff:fee3:e8de: 59201 -> 2001:6f8:900:7c0::2: 80 (300 bytes) +2001:6f8:900:7c0::2: 80 -> 2001:6f8:102d:0:2d0:9ff:fee3:e8de: 59201 (2379 bytes) + +GET / HTTP/1.0 +Host: cl-1985.ham-01.de.sixxs.net +Accept: text/html, text/plain, text/css, text/sgml, */*;q=0.01 +Accept-Encoding: gzip, bzip2 +Accept-Language: en +User-Agent: Lynx/2.8.6rel.2 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8b + + + +HTTP/1.1 200 OK +Date: Sun, 05 Aug 2007 19:16:44 GMT +Server: Apache +Content-Length: 2121 +Connection: close +Content-Type: text/html + + + + + Index of / + + +

Index of /

+
Icon  Name                    Last modified      Size  Description
[DIR] 202-vorbereitung/ 06-Jul-2007 14:31 - +[   ] Efficient_Video_on_d..> 19-Dec-2006 03:17 291K +[   ] Welcome Stranger!!! 28-Dec-2006 03:46 0 +[TXT] barschel.htm 31-Jul-2007 02:21 44K +[DIR] bnd/ 30-Dec-2006 08:59 - +[DIR] cia/ 28-Jun-2007 00:04 - +[   ] cisco_ccna_640-801_c..> 28-Dec-2006 03:48 236K +[DIR] doc/ 19-Sep-2006 01:43 - +[DIR] freenetproto/ 06-Dec-2006 09:00 - +[DIR] korrupt/ 03-Jul-2007 11:57 - +[DIR] mp3_technosets/ 04-Jul-2007 08:56 - +[TXT] neues_von_rainald_go..> 21-Mar-2007 23:27 31K +[TXT] neues_von_rainald_go..> 21-Mar-2007 23:29 36K +[   ] pruef.pdf 28-Dec-2006 07:48 88K +
+ +``` + +Chaining plugins to view flow data for a specific country code in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) (note: TCP handshakes are not included in the packet count) + +``` +Dshell> decode -p country+netflow --country_code=JP ~/pcap/SkypeIRC.cap +2006-08-25 15:32:20.766761 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33438 1 0 64 0 0.0000s +2006-08-25 15:32:20.634046 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33435 1 0 64 0 0.0000s +2006-08-25 15:32:20.747503 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33437 1 0 64 0 0.0000s +2006-08-25 15:32:20.651501 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33436 1 0 64 0 0.0000s +``` + +Collecting DNS traffic from several files and storing it in a new pcap file. + +``` +Dshell> decode -p dns+pcapwriter --pcapwriter_outfile=test.pcap ~/pcap/*.cap >/dev/null +Dshell> tcpdump -nnr test.pcap |head +reading from file test.pcap, link-type EN10MB (Ethernet) +15:36:08.670569 IP 192.168.1.2.2131 > 192.168.1.1.53: 40209+ A? ui.skype.com. (30) +15:36:08.670687 IP 192.168.1.2.2131 > 192.168.1.1.53: 40210+ AAAA? ui.skype.com. (30) +15:36:08.674022 IP 192.168.1.1.53 > 192.168.1.2.2131: 40209- 1/0/0 A 212.72.49.131 (46) +15:36:09.011208 IP 192.168.1.1.53 > 192.168.1.2.2131: 40210 0/1/0 (94) +15:36:10.171350 IP 192.168.1.2.2131 > 192.168.1.1.53: 40210+ AAAA? ui.skype.com. (30) +15:36:10.961350 IP 192.168.1.1.53 > 192.168.1.2.2131: 40210* 0/1/0 (85) +15:36:10.961608 IP 192.168.1.2.2131 > 192.168.1.1.53: 40211+ AAAA? ui.skype.com. (30) +15:36:11.294333 IP 192.168.1.1.53 > 192.168.1.2.2131: 40211 0/1/0 (94) +15:32:21.664798 IP 192.168.1.2.2130 > 192.168.1.1.53: 39862+ A? ui.skype.com. (30) +15:32:21.664913 IP 192.168.1.2.2130 > 192.168.1.1.53: 39863+ AAAA? ui.skype.com. (30) +``` + +Collecting TFTP data and converting alerts to JSON format using [sample traffic](https://wiki.wireshark.org/SampleCaptures#TFTP) + +``` +Dshell> decode -p tftp -O jsonout ~/pcap/tftp_*.pcap +{"dport": 3445, "dip": "192.168.0.10", "data": "read rfc1350.txt (24599 bytes) ", "sport": 50618, "readwrite": "read", "sip": "192.168.0.253", "plugin": "tftp", "ts": 1367411051.972852, "filename": "rfc1350.txt"} +{"dport": 2087, "dip": "192.168.0.13", "data": "write rfc1350.txt (24599 bytes) ", "sport": 57509, "readwrite": "write", "sip": "192.168.0.1", "plugin": "tftp", "ts": 1367053679.45274, "filename": "rfc1350.txt"} +``` + +Running a plugin within a separate Python script using [sample traffic](https://wiki.wireshark.org/SampleCaptures#TFTP) + +``` +# Import required Dshell libraries +import dshell.decode as decode +import dshell.plugins.tftp.tftp as tftp + +# Instantiate plugin +plugin = tftp.DshellPlugin() +# Define plugin-specific arguments, if needed +dargs = {plugin: {"outdir": "/tmp/"}} +# Add plugin(s) to plugin chain +decode.plugin_chain = [plugin] +# Run decode main function with all other arguments +decode.main( + debug=True, + files=["/home/user/pcap/tftp_rrq.pcap", "/home/user/pcap/tftp_wrq.pcap"], + plugin_args=dargs +) +``` diff --git a/README.md b/README.md index d7014eb..3c1feea 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,112 @@ # Dshell -**_A new version of Dshell for Python 3 is coming in September 2020, Dshell 3. See ‘News’ section for additional information._** - An extensible network forensic analysis framework. Enables rapid development of plugins to support the dissection of network packet captures. Key features: - - +* Deep packet analysis using specialized plugins * Robust stream reassembly * IPv4 and IPv6 support * Custom output handlers -* Chainable decoders - -## Prerequisites - -* Linux (developed on Ubuntu 12.04) -* Python 2.7 -* [geoip2](https://github.com/maxmind/GeoIP2-python), Apache License, Version 2.0 - * [MaxMind GeoIP datasets](https://dev.maxmind.com/geoip/geoip2/geolite2/) -* [PyCrypto](https://pypi.python.org/pypi/pycrypto), custom license -* [dpkt](https://code.google.com/p/dpkt/), New BSD License -* [IPy](https://github.com/haypo/python-ipy), BSD 2-Clause License -* [pypcap](https://code.google.com/p/pypcap/), New BSD License -* [elasticsearch-py](https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html), Apache License, Version 2.0 - optional, used only with Dshell's elasticout output module +* Chainable plugins + +## Requirements +* Linux (developed on Red Hat Enterprise Linux 6.7) +* Python 3 (developed with Python 3.5.1) +* [pypacker](https://github.com/mike01/pypacker) +* [pcapy](http://www.coresecurity.com/corelabs-research/open-source-tools/pcapy) +* [geoip2](https://github.com/maxmind/GeoIP2-python) + * [MaxMind GeoIP2 datasets](https://dev.maxmind.com/geoip/geoip2/geolite2/) + +## Optional +* [oui.txt](http://standards-oui.ieee.org/oui.txt) + * used by some plugins that handle MAC addresses + * place in <dshell>/data/ +* [elasticsearch](https://www.elastic.co/guide/en/elasticsearch/client/python-api/current/index.html) + * used in the elasticout output module + * only necessary if planning to use elasticsearch to store output + +## Major Changes Since Previous Release +* This is a major framework update to Dshell. Plugins written for the previous version are not compatible with this version, and vice versa. +* Uses Python 3 + * Rewritten in Python 3 from the ground up. Python 2 language deprecated on [1 JAN 2020](https://www.python.org/doc/sunset-python-2/) + * By extension, dpkt and pypcap have been replaced with Python3-friendly pypacker and pcapy (respectively). +* Is a Python package + * Converted into a single package, removing the need for the shell to set several environment variables. + * Allows easier use of Dshell plugins in other Python scripts +* Changed "decoders" to "plugins" + * Primarily a word-swap, to clarify that "decoders" can do more than simply decode traffic, and to put Dshell more in line with the terminology of other frameworks. +* Significant reduction in camelCase functions, replaced with more Pythonic snake\_case functions. + * Notable examples include blobHandler->blob\_handler, rawHandler->raw\_handler, connectionInitHandler->connection\_init\_handler, etc. +* All plugins are now chainable + * To accommodate this, handler functions in plugins must now use return statements indicating whether a packet, connection, or similar will continue to the next plugin. The type of object(s) to return depends on the type of handler, but will generally match the types of the handler's input. Dshell will display a warning if it's not the right type. +* Plugins can now use all output modules\* available to the command line switch, -O + * That does not mean every output module will be _useful_ to every plugin (e.g. using netflow output for a plugin that looks at individual packets), but they are available. + * alert(), write(), and dump() are now the same function: write() + * Output modules can be listed with a new flag in decode.py, --list-output or --lo + * Arguments for output modules are now passed with the --oargs command-line argument + * \* pcapout is (currently) the exception to this rule. A method has yet to arise that allows it to work with connection-based plugins +* No more dObj declaration + * decode.py just looks for the class named DshellPlugin and creates an instance of that +* Improved error handling + * Dshell handles more of the most common exceptions during everyday use +* Enables development of external plugin packs, allowing the sharing and installation of new, externally-developed plugins without overlapping the core Dshell libraries. ## Installation -1. Install all of the necessary Python modules listed above. Many of them are available via pip and/or apt-get. - - * `sudo pip install geoip2 pycrypto dpkt IPy pypcap` - -2. Configure GeoIP by moving the MaxMind data files (GeoLite2-Country.mmdb, GeoLite2-ASN.mmdb) to <install-location>/share/GeoIP/ +1. Install Dshell with pip + * `sudo python3 -m pip install Dshell/` OR `sudo python3 -m pip install ` +2. Configure geoip2 by moving the MaxMind data files (GeoLite2-ASN.mmdb, GeoLite2-City.mmdb, GeoLite2-Country.mmdb) to <install-location>/data/GeoIP/ +3. Run `dshell`. This should drop you into a `Dshell> ` prompt. -2. Run `make`. This will build Dshell. - -3. Run `./dshell`. This is Dshell. If you get a Dshell> prompt, you're good to go! - -## Basic usage +## Basic Usage * `decode -l` - * This will list all available decoders alongside basic information about them + * This will list all available plugins, alongside basic information about them * `decode -h` - * Show generic command-line flags available to most decoders -* `decode -d ` - * Display information about a decoder, including available command-line flags -* `decode -d ` - * Run the selected decoder on a pcap file - -## Development -* [Using Dshell With PyCharm](doc/UsingDshellWithPyCharm.md) - -## News - -* Sep 2020 - A new version of Dshell for Python 3 is coming, Dshell 3. - * This is a major framework update to Dshell. Plugins written for the previous version are not compatible with this version, and vice versa. - * Uses Python 3 - * Rewritten in Python 3 from the ground up. Python 2 language deprecated on [1 JAN 2020](https://www.python.org/doc/sunset-python-2/) - * By extension, dpkt and pypcap have been replaced with Python 3-friendly pypacker and pcapy (respectively). - * Is a Python package - * All plugins are chainable - * Plugins can use all output modules - * Improved error handling - * Enables development of external plugin packs, allowing the sharing and installation of new, externally-developed plugins without overlapping the core Dshell libraries. -* Sep 2020 - This Python 2 version of Dshell will be deprecated and tagged with its current version number after Dshell 3 is released. It will still be available via this repository. Issues and Pull requests for the previous version will be closed when the new version is released. -* Feb 2019 - Removed deprecated pygeoip dependency, and replaced it with geoip2. This requires the use of new GeoIP data files, listed in the Prerequisites and Installation sections above. - -## Partners - -Below are repositories from partners Dshell has worked together with. - -* [DeKrych/Dshell-plugins](https://github.com/DeKrych/Dshell-plugins) -* [terry-wen/Network-Visualization-Project](https://github.com/terry-wen/Network-Visualization-Project) + * Show generic command-line flags available to most plugins +* `decode -p ` + * Display information about a plugin, including available command line flags +* `decode -p ` + * Run the selected plugin on a pcap file +* `decode -p + ` + * Chain two (or more) plugins together and run them on a pcap file +* `decode -p -i ` + * Run the selected plugin live on an interface (may require superuser privileges) ## Usage Examples - Showing DNS lookups in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) ``` -Dshell> decode -d dns ~/pcap/dns.cap -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 39867 PTR? 66.192.9.104 / PTR: 66-192-9-104.gen.twtelecom.net ** -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 30144 A? www.netbsd.org / A: 204.152.190.12 (ttl 82159s) ** -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 61652 AAAA? www.netbsd.org / AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86400s) ** -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 32569 AAAA? www.netbsd.org / AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86340s) ** -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 36275 AAAA? www.google.com / CNAME: www.l.google.com ** -dns 2005-03-30 03:47:46 192.168.170.8:32795 -> 192.168.170.20:53 ** 9837 AAAA? www.example.notginh / NXDOMAIN ** -dns 2005-03-30 03:52:17 192.168.170.8:32796 <- 192.168.170.20:53 ** 23123 PTR? 127.0.0.1 / PTR: localhost ** -dns 2005-03-30 03:52:25 192.168.170.56:1711 <- 217.13.4.24:53 ** 30307 A? GRIMM.utelsystems.local / NXDOMAIN ** -dns 2005-03-30 03:52:17 192.168.170.56:1710 <- 217.13.4.24:53 ** 53344 A? GRIMM.utelsystems.local / NXDOMAIN ** +Dshell> decode -p dns ~/pcap/dns.cap |sort +[DNS] 2005-03-30 03:47:46 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 4146, TXT? google.com., TXT: b'\x0fv=spf1 ptr ?all' ** +[DNS] 2005-03-30 03:47:50 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 63343, MX? google.com., MX: b'\x00(\x05smtp4\xc0\x0c', MX: b'\x00\n\x05smtp5\xc0\x0c', MX: b'\x00\n\x05smtp6\xc0\x0c', MX: b'\x00\n\x05smtp1\xc0\x0c', MX: b'\x00\n\x05smtp2\xc0\x0c', MX: b'\x00(\x05smtp3\xc0\x0c' ** +[DNS] 2005-03-30 03:47:59 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 18849, LOC? google.com. ** +[DNS] 2005-03-30 03:48:07 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 39867, PTR? 104.9.192.66.in-addr.arpa., PTR: 66-192-9-104.gen.twtelecom.net. ** +[DNS] 2005-03-30 03:49:18 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 30144, A? www.netbsd.org., A: 204.152.190.12 (ttl 82159s) ** +[DNS] 2005-03-30 03:49:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 61652, AAAA? www.netbsd.org., AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86400s) ** +[DNS] 2005-03-30 03:50:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 32569, AAAA? www.netbsd.org., AAAA: 2001:4f8:4:7:2e0:81ff:fe52:9a6b (ttl 86340s) ** +[DNS] 2005-03-30 03:50:44 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 36275, AAAA? www.google.com., CNAME: 'www.l.google.com.' ** +[DNS] 2005-03-30 03:50:54 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 56482, AAAA? www.l.google.com. ** +[DNS] 2005-03-30 03:51:35 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 48159, AAAA? www.example.com. ** +[DNS] 2005-03-30 03:51:46 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 9837, AAAA? www.example.notginh., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32795 -- 192.168.170.20:53 ** ID: 65251, AAAA: 2001:4f8:0:2::d (ttl 600s), A: 204.152.184.88 (ttl 600s) ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32796 -- 192.168.170.20:53 ** ID: 23123, PTR? 1.0.0.127.in-addr.arpa., PTR: localhost. ** +[DNS] 2005-03-30 03:52:17 192.168.170.8:32797 -- 192.168.170.20:53 ** ID: 8330, NS: b'\x06ns-ext\x04nrt1\xc0\x0c', NS: b'\x06ns-ext\x04sth1\xc0\x0c', NS: b'\x06ns-ext\xc0\x0c', NS: b'\x06ns-ext\x04lga1\xc0\x0c' ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1707 -- 217.13.4.24:53 ** ID: 12910, SRV? _ldap._tcp.Default-First-Site-Name._sites.dc._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1708 -- 217.13.4.24:53 ** ID: 61793, SRV? _ldap._tcp.dc._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1709 -- 217.13.4.24:53 ** ID: 33633, SRV? _ldap._tcp.05b5292b-34b8-4fb7-85a3-8beef5fd2069.domains._msdcs.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:17 192.168.170.56:1710 -- 217.13.4.24:53 ** ID: 53344, A? GRIMM.utelsystems.local., NXDOMAIN ** +[DNS] 2005-03-30 03:52:25 192.168.170.56:1711 -- 217.13.4.24:53 ** ID: 30307, A? GRIMM.utelsystems.local., NXDOMAIN ** ``` Following and reassembling a stream in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) ``` -Dshell> decode -d followstream ~/pcap/v6-http.cap +Dshell> decode -p followstream ~/pcap/v6-http.cap Connection 1 (TCP) -Start: 2007-08-05 19:16:44.189852 UTC - End: 2007-08-05 19:16:44.204687 UTC -2001:6f8:102d:0:2d0:9ff:fee3:e8de:59201 -> 2001:6f8:900:7c0::2:80 (240 bytes) -2001:6f8:900:7c0::2:80 -> 2001:6f8:102d:0:2d0:9ff:fee3:e8de:59201 (2259 bytes) +Start: 2007-08-05 15:16:44.189851 +End: 2007-08-05 15:16:44.219460 +2001:6f8:102d:0:2d0:9ff:fee3:e8de: 59201 -> 2001:6f8:900:7c0::2: 80 (300 bytes) +2001:6f8:900:7c0::2: 80 -> 2001:6f8:102d:0:2d0:9ff:fee3:e8de: 59201 (2379 bytes) GET / HTTP/1.0 Host: cl-1985.ham-01.de.sixxs.net @@ -105,6 +115,8 @@ Accept-Encoding: gzip, bzip2 Accept-Language: en User-Agent: Lynx/2.8.6rel.2 libwww-FM/2.14 SSL-MM/1.4.1 OpenSSL/0.9.8b + + HTTP/1.1 200 OK Date: Sun, 05 Aug 2007 19:16:44 GMT Server: Apache @@ -137,31 +149,59 @@ Content-Type: text/html ``` -Chaining decoders to view flow data for a specific country code in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) (note: TCP handshakes are not included in the packet count) +Chaining plugins to view flow data for a specific country code in [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) (note: TCP handshakes are not included in the packet count) + +``` +Dshell> decode -p country+netflow --country_code=JP ~/pcap/SkypeIRC.cap +2006-08-25 15:32:20.766761 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33438 1 0 64 0 0.0000s +2006-08-25 15:32:20.634046 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33435 1 0 64 0 0.0000s +2006-08-25 15:32:20.747503 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33437 1 0 64 0 0.0000s +2006-08-25 15:32:20.651501 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33436 1 0 64 0 0.0000s +``` + +Collecting DNS traffic from several files and storing it in a new pcap file. + +``` +Dshell> decode -p dns+pcapwriter --pcapwriter_outfile=test.pcap ~/pcap/*.cap >/dev/null +Dshell> tcpdump -nnr test.pcap |head +reading from file test.pcap, link-type EN10MB (Ethernet) +15:36:08.670569 IP 192.168.1.2.2131 > 192.168.1.1.53: 40209+ A? ui.skype.com. (30) +15:36:08.670687 IP 192.168.1.2.2131 > 192.168.1.1.53: 40210+ AAAA? ui.skype.com. (30) +15:36:08.674022 IP 192.168.1.1.53 > 192.168.1.2.2131: 40209- 1/0/0 A 212.72.49.131 (46) +15:36:09.011208 IP 192.168.1.1.53 > 192.168.1.2.2131: 40210 0/1/0 (94) +15:36:10.171350 IP 192.168.1.2.2131 > 192.168.1.1.53: 40210+ AAAA? ui.skype.com. (30) +15:36:10.961350 IP 192.168.1.1.53 > 192.168.1.2.2131: 40210* 0/1/0 (85) +15:36:10.961608 IP 192.168.1.2.2131 > 192.168.1.1.53: 40211+ AAAA? ui.skype.com. (30) +15:36:11.294333 IP 192.168.1.1.53 > 192.168.1.2.2131: 40211 0/1/0 (94) +15:32:21.664798 IP 192.168.1.2.2130 > 192.168.1.1.53: 39862+ A? ui.skype.com. (30) +15:32:21.664913 IP 192.168.1.2.2130 > 192.168.1.1.53: 39863+ AAAA? ui.skype.com. (30) +``` + +Collecting TFTP data and converting alerts to JSON format using [sample traffic](https://wiki.wireshark.org/SampleCaptures#TFTP) ``` -Dshell> decode -d country+netflow --country_code=JP ~/pcap/SkypeIRC.cap -2006-08-25 19:32:20.651502 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33436 1 0 36 0 0.0000s -2006-08-25 19:32:20.766761 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33438 1 0 36 0 0.0000s -2006-08-25 19:32:20.634046 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33435 1 0 36 0 0.0000s -2006-08-25 19:32:20.747503 192.168.1.2 -> 202.232.205.123 (-- -> JP) UDP 60583 33437 1 0 36 0 0.0000s +Dshell> decode -p tftp -O jsonout ~/pcap/tftp_*.pcap +{"dport": 3445, "dip": "192.168.0.10", "data": "read rfc1350.txt (24599 bytes) ", "sport": 50618, "readwrite": "read", "sip": "192.168.0.253", "plugin": "tftp", "ts": 1367411051.972852, "filename": "rfc1350.txt"} +{"dport": 2087, "dip": "192.168.0.13", "data": "write rfc1350.txt (24599 bytes) ", "sport": 57509, "readwrite": "write", "sip": "192.168.0.1", "plugin": "tftp", "ts": 1367053679.45274, "filename": "rfc1350.txt"} ``` -Collecting netflow data for [sample traffic](http://wiki.wireshark.org/SampleCaptures#General_.2F_Unsorted) with vlan headers, then tracking the connection to a specific IP address +Running a plugin within a separate Python script using [sample traffic](https://wiki.wireshark.org/SampleCaptures#TFTP) ``` -Dshell> decode -d netflow ~/pcap/vlan.cap -1999-11-05 18:20:43.170500 131.151.20.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:42.063074 131.151.32.71 -> 131.151.32.255 (US -> US) UDP 138 138 1 0 201 0 0.0000s -1999-11-05 18:20:43.096540 131.151.1.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.079765 131.151.5.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:41.521798 131.151.104.96 -> 131.151.107.255 (US -> US) UDP 137 137 3 0 150 0 1.5020s -1999-11-05 18:20:43.087010 131.151.6.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.368210 131.151.111.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.250410 131.151.32.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.115330 131.151.10.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.375145 131.151.115.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:43.363348 131.151.107.254 -> 255.255.255.255 (US -> --) UDP 520 520 1 0 24 0 0.0000s -1999-11-05 18:20:40.112031 131.151.5.55 -> 131.151.5.255 (US -> US) UDP 138 138 1 0 201 0 0.0000s -1999-11-05 18:20:43.183825 131.151.32.79 -> 131.151.32.255 (US -> US) UDP 138 138 1 0 201 0 0.0000s +# Import required Dshell libraries +import dshell.decode as decode +import dshell.plugins.tftp.tftp as tftp + +# Instantiate plugin +plugin = tftp.DshellPlugin() +# Define plugin-specific arguments, if needed +dargs = {plugin: {"outdir": "/tmp/"}} +# Add plugin(s) to plugin chain +decode.plugin_chain = [plugin] +# Run decode main function with all other arguments +decode.main( + debug=True, + files=["/home/user/pcap/tftp_rrq.pcap", "/home/user/pcap/tftp_wrq.pcap"], + plugin_args=dargs +) ``` diff --git a/bin/decode.py b/bin/decode.py deleted file mode 100755 index 86d26c3..0000000 --- a/bin/decode.py +++ /dev/null @@ -1,964 +0,0 @@ -#!/usr/bin/env python - -# pylint: disable-msg=C0103,E501 - -import copy -import dshell -import glob -import gzip -import logging -import optparse -import os -import output -import sys -import tempfile -import traceback -import util -import zipfile -try: - import pcap -except ImportError: - pcap = None - print 'pcap not available: decoders requiring pcap are not usable' - - -def import_module(name=None, silent=False, search=None): - if search is None: - search = {} - try: - # we will first check search[name] for the module - # else split foo.bar:baz to get from foo.bar import baz - # else split dotted path to perform a 'from foo import bar' operation - try: - module = search[name] # a -> from search[a] import a - except KeyError: - # a.b.c from a.b import c - module, name = name.split('.')[:-1], name.split('.')[-1] - if module: - module = '.'.join(module) - else: - module = name - path = None - if os.path.sep in module: # was a full path to a decoder given? - path, module = os.path.dirname(module), os.path.basename(module) - # print module,name - if path: - sys.path.append(path) - obj = __import__(module, fromlist=[name]) - if path: - sys.path.remove(path) - if 'dObj' in dir(obj) or 'obj' in dir(obj): - return obj - elif name in dir(obj): - obj = getattr(obj, name) - if 'dObj' in dir(obj) or 'obj' in dir(obj): - return obj - except Exception as err: - if not silent: - sys.stderr.write( - "Error '%s' loading module %s\n" % (str(err), module)) - return False - - -def setDecoderPath(decoder_path): - '''set the base decoder path, - add it to sys.path for importing, - and walk it to return all subpaths''' - paths = [] - paths.append(decoder_path) # append base path first - # walk decoder directories an add to sys.path - for root, dirs, files in os.walk(decoder_path): - # skip hidden dirs like .svn - [dirs.remove(d) for d in dirs if d.startswith('.')] - for d in sorted(dirs): - paths.append(os.path.join(root, d)) - return paths # return the paths we found - - -def getDecoders(decoder_paths): - ''' find all decoders and map decoder to import.path.decoder - expect common prefix to start with basepath''' - import_base = os.path.commonprefix(decoder_paths).split( - os.path.sep)[:-1] # keep last part as base - decoders = {} - for path in decoder_paths: - # split path and trim off part before base - import_path = path.split(os.path.sep)[len(import_base):] - for f in glob.iglob("%s/*.py" % path): - name = os.path.splitext(os.path.basename(f))[0] - if name != '__init__': # skip package stubs - # build topdir.path...module name from topdir/dir.../file - decoders[name] = '.'.join(import_path + [name]) - return decoders - - -def printDecoders(decoder_map, silent=True): - '''Print list of decoders with additional info''' - dList = [] - FS = ' %-40s %-30s %-10s %s %1s %s' - for name, module in sorted(decoder_map.iteritems()): - try: - try: - decoder = import_module(module, silent).dObj - except Exception as exc: - print "Exception loading module '%s': %s" % (module, exc) - continue - # get the type of decoder it is - dtype = 'RAW' - if 'IP' in dir(decoder): - dtype = 'IP ' - if 'UDP' in dir(decoder): - dtype = 'UDP' - if 'TCP' in dir(decoder): - dtype = 'TCP' - dList.append(FS % ( - module, decoder.name, - decoder.author, - dtype, '+' if decoder.chainable else '', - decoder.description)) - except: # :-( - pass - - print FS % ('module', 'name', 'author', ' ', ' ', 'desc') - print FS % ('-' * 40, '-' * 30, '-' * 10, '---', '-', '-' * 50) - for d in sorted(dList): - print d - - -def readInFilter(fname): - '''Read in a BPF filter provided by a command line argument''' - filter = '' - tmpfd = open(fname, 'r') - for line in tmpfd: - if '#' in line: - # keep \n for visual output sanity - line = line.split('#')[0] + '\n' - filter += line - tmpfd.close() - - return filter - - -def decode_live(out, options, decoder, decoder_args, decoder_options): - # set decoder options - initDecoderOptions(decoder, out, options, decoder_args, decoder_options) - - if 'preModule' in dir(decoder): - decoder.preModule() - - # give the interface name to the decoder - decoder.input_file = options.interface - stats = None - if options.verbose: - log('Attempting to listen on %s' % options.interface) - try: - - if not pcap: - raise NotImplementedError("raw capture support not implemented") - decoder.capture = pcap.pcap(options.interface, 65535, True) - if decoder.filter: - decoder.capture.setfilter(decoder.filter) - while not options.count or decoder.count < options.count: - # use dispatch so we can handle signals - decoder.capture.dispatch(1, decoder.decode) - except KeyboardInterrupt: - pass - except Exception as exc: - log(str(exc), level=logging.ERROR) - - if 'cleanConnectionStore' in dir(decoder): - decoder.cleanConnectionStore() - - if 'postModule' in dir(decoder): - decoder.postModule() - - -def expandCompressedFile(fname, verbose, tmpdir): - ''' Expand a file compressed with gzip, bzip2, or zip. - Only handles zip files with 1 file. Need to add handling - for zip files containing multiple pcap files.''' - try: - # print fname - ext = os.path.splitext(fname)[1] - if verbose: - log('+Attempting to process %s compressed file' % (ext)) - if ext == '.gz': - f = gzip.open(fname, 'rb') - elif ext == '.bz2': - f = bz2.BZ2File(fname, 'r') - elif ext == '.zip': - print "Enter password for .zip file [default:none]:", - pswd = raw_input() - z = zipfile.ZipFile(fname) - f = z.open(z.namelist()[0], 'r', pswd) - else: - log('+Error decompressing %s' % (fname), level=logging.ERROR) - return - - h = tempfile.NamedTemporaryFile(dir=tmpdir, delete=False) - if verbose: - log('+Temp directory: %s' % (tempfile.gettempdir())) - log('+Expanding to tempfile %s' % (h.name)) - - for line in f.readlines(): - h.write(line) - h.close() - f.close() - return h.name - except: - return None - - -# This is a support function for the code in main() that supports -# recursive directory crawling when looking for pcap files to parse -# This function recurses through a directory structure, applying -# the wildcard (if any) from the command line. If no wildcard -# is specified, then all files are included. -def addFilesFromDirectory(inputs, curDir, wildcard='*'): - # STEP 1: Add files matching wildcard from current directory... - - # concatenate into path - full_path = os.path.join(curDir, wildcard) - inputs.extend(glob.glob(full_path)) - - # STEP 2: Recurse into child directories - for path in os.listdir(curDir): - fullDir = os.path.join(curDir, path) - - if os.path.isdir(fullDir): - addFilesFromDirectory(inputs, fullDir, wildcard) - - -# The default OptionParser will raise an error when it encounters an option that -# it doesn't understand. By creating a new option parser, dshellOptionParser, -# we can ignore the unknown options (read: Module specific options) -class dshellOptionParser(optparse.OptionParser): - - def error(self, msg): - pass - - # create options for all loaded decoders - def add_decoder_options(self, d): - if d.subDecoder: - # if we have a subdecoder, recurse down until we don't - self.add_decoder_options(d.subDecoder) - try: - if d.optiondict: - group = optparse.OptionGroup( - self, "%s decoder options" % d.name) - for argname, optargs in d.optiondict.iteritems(): - optname = "%s_%s" % (d.name, argname) - group.add_option("--" + optname, dest=optname, **optargs) - self.add_option_group(group) - except: - raise # :-( - - # pass thru to parse_args, but add in kwargs - def parse_args(self, args, **kwargs): - try: - options, args = optparse.OptionParser.parse_args(self, args) - options.__dict__.update(kwargs) - except UnboundLocalError: - # probably missing a value for an argument, e.g. 'decode -d' - # without a decoder - self.print_help() - return None, None - return options, args - - # Fix for handling unknown options (e.g. decoder-specific options) - # reference: - # http://stackoverflow.com/questions/1885161/how-can-i-get-optparses-optionparser-to-ignore-invalid-arguments - def _process_args(self, largs, rargs, values): - while rargs: - try: - optparse.OptionParser._process_args(self, largs, rargs, values) - except (optparse.BadOptionError, optparse.AmbiguousOptionError) as exc: - largs.append(exc.opt_str) - - -def printDecoderBriefs(decoders): - """Prints a brief overview of a decoder when using --help with a decoder""" - print - for d in decoders.values(): - print 'Module name:', d.name - print '=' * 20 - if d.longdescription: - print d.longdescription - else: - print d.description - print 'Default filter: %s' % (d.filter) - return - - -def initDecoderOptions(decoder, out, options, decoder_args, decoder_options): - """ - pass global config to decoder - """ - - # recurse from the bottom of the chain to the top - if decoder.subDecoder: - initDecoderOptions( - decoder.subDecoder, out, options, decoder_args, decoder_options) - - # give the decoder the output object if the decoder doesn't pick one - # or if an output object is specified via command line options - if not decoder.out or options.output != 'output': - decoder.out = out - else: - # initialize the decoder's custom output using the channels from the - # global - # provide global output module under alternate name - decoder.globalout = out - try: - # If the decoder's default output doesn't have a filehandle set, - # use the user provided one - if decoder.out.fh == sys.stdout: - decoder.out.fh = out.fh - except AttributeError: - # A filehandle doesn't always exist, such as with QueueOutput - pass - if not decoder.out.sessionwriter: - decoder.out.sessionwriter = out.sessionwriter - if not decoder.out.pcapwriter: - decoder.out.pcapwriter = out.pcapwriter - # set the logger - decoder.out.logger = logging.getLogger(decoder.name) - - # perform any output module setup before processing data - decoder.out.setup() - - # set output format string, or reset to default - # do not override --oformat specified string - if decoder.format and not options.oformat: - decoder.out.setformat(decoder.format) - - # set verbosity - decoder.verbose = options.verbose - if options.debug: - # debug() is already taken, and _DEBUG might already be set - decoder._DEBUG = options.debug - - # override decoder BPF - if options.bpf != None: - decoder.filter = options.bpf - - # override decoder filterfn - if options.nofilterfn: - decoder.filterfn = lambda addr: True - - # read BPF from file - if options.filefilter != None: - try: - tmpbpf = readInFilter(options.filefilter) - except: - log("Invalid tcpdump filter file: %s" % - (options.filefilter), level=logging.ERROR) - return - - decoder.filter = tmpbpf - - # extend bpf filter if necessary - if options.ebpf != None: - ebpf = options.ebpf - if not decoder.filter: - decoder.filter = ebpf - elif ebpf.startswith('or '): - decoder.filter = decoder.filter + ' ' + ebpf - else: - decoder.filter = decoder.filter + ' and ' + ebpf - - # do we change the layer-2 decoder for raw capture - if options.layer2: - import dpkt - decoder.l2decoder = eval('dpkt.' + options.layer2) - - # strip extra layers? - if options.striplayers: - decoder.striplayers = int(options.striplayers) - - if not options.novlan and not(decoder.filter.startswith('vlan')): - if decoder.filter: - decoder.filter = '( ' + decoder.filter + \ - ' ) or ( vlan and ( ' + decoder.filter + ' ) )' - else: - decoder.filter = '' # fix for null filter case - - # pass args and config file to decoder - decoder.parseArgs(decoder_args, decoder_options) - - log('Using module ' + repr(decoder)) - - -def main(*largs, **kwargs): - global log - bin_path = os.environ['BINPATH'] - sys.path.insert(0, bin_path) - # get map of name to module import path - decoder_map = getDecoders(setDecoderPath(os.environ['DECODERPATH'])) - - # The main argument parser. It will have every command line option - # available and should be used when actually parsing - parser = dshellOptionParser( - usage="usage: %prog [options] [decoder options] file1 file2 ... filen [-- [decoder args]+]", - version="%prog " + str(dshell.__version__), add_help_option=False) - # A short argument parser, meant to only hold the shorter list of - # arguments for when a decoder is called without a pcap file. DO - # NOT USE for any serious argument parsing. - parser_short = dshellOptionParser( - usage="usage: %prog [options] [decoder options] file1 file2 ... filen [-- [decoder args]+]", - version="%prog " + str(dshell.__version__), add_help_option=False) - parser.add_option('-h', '-?', '--help', dest='help', - help="Print common command-line flags and exit", action='store_true', - default=False) - parser_short.add_option('-h', '-?', '--help', dest='help', - help="Print common command-line flags and exit", action='store_true', - default=False) - parser.add_option('-d', '--decoder', dest="decoder", - action='append', help="Use a specific decoder module") - parser.add_option('-l', '--ls', '--list', action="store_true", - help='List all available decoders', dest='list') - parser.add_option( - '-C', '--config', dest='config', help='specify config.ini file') - parser.add_option('--tmpdir', dest='tmpdir', type='string', default=tempfile.gettempdir(), - help='alternate temp directory (for use when processing compressed pcap files)') - parser.add_option('-r', '--recursive', dest='recursive', action='store_true', - help='recursively process all PCAP files under input directory') - - group = optparse.OptionGroup(parser, "Multiprocessing options") - group.add_option('-p', '--parallel', dest='parallel', - action='store_true', help='process multiple files in parallel') - group.add_option('-t', '--threaded', dest='threaded', - action='store_true', help='run multiple decoders in parallel') - group.add_option('-n', '--nprocs', dest='numprocs', type='int', - default=4, help='number of simultaneous processes') - parser.add_option_group(group) - - # decode-pcap specific options - group = optparse.OptionGroup(parser, "Input options") - group.add_option('-i', '--interface', dest='interface', - default=None, help='listen live on INTERFACE') - group.add_option('-c', '--count', dest='count', type='int', - help='number of packets to process', default=0) - group.add_option('-f', '--bpf', dest='bpf', - help='replace default decoder filter (use carefully)') - group.add_option('--nofilterfn', dest='nofilterfn', - action="store_true", help='Set filterfn to pass-thru') - group.add_option('-F', dest='filefilter', - help='Use filefilter as input for the filter expression. An additional expression given on the command line is ignored.') - group.add_option( - '--ebpf', dest='ebpf', help='BPF filter to exclude traffic, extends other filters') - group.add_option('--no-vlan', dest='novlan', action="store_true", - help='do not examine traffic which has VLAN headers present') - group.add_option('--layer2', dest='layer2', default='ethernet.Ethernet', - help='select the layer-2 protocol module') - group.add_option('--strip', dest='striplayers', default=0, - help='extra data-link layers to strip') - parser.add_option_group(group) - - group = optparse.OptionGroup(parser_short, "Input options") - group.add_option('-i', '--interface', dest='interface', - default=None, help='listen live on INTERFACE') - group.add_option('-c', '--count', dest='count', type='int', - help='number of packets to process', default=0) - group.add_option('-f', '--bpf', dest='bpf', - help='replace default decoder filter (use carefully)') - group.add_option('--nofilterfn', dest='nofilterfn', - action="store_true", help='Set filterfn to pass-thru') - group.add_option('-F', dest='filefilter', - help='Use filefilter as input for the filter expression. An additional expression given on the command line is ignored.') - group.add_option( - '--ebpf', dest='ebpf', help='BPF filter to exclude traffic, extends other filters') - group.add_option('--no-vlan', dest='novlan', action="store_true", - help='do not examine traffic which has VLAN headers present') - group.add_option('--layer2', dest='layer2', default='ethernet.Ethernet', - help='select the layer-2 protocol module') - group.add_option('--strip', dest='striplayers', default=0, - help='extra data-link layers to strip') - parser_short.add_option_group(group) - - group = optparse.OptionGroup(parser, "Output options") - group.add_option('-o', '--outfile', dest='outfile', help='write output to the file OUTFILE. Additional output can be set with KEYWORD=VALUE,...\n' + - '\tmode= -1: - (path, wildcard) = os.path.split(file_path) - - # If just file is specified (no path) - if len(path) == 0: - inputs.extend(glob.glob(wildcard)) - - # If there is a path, but recursion not specified, - # then just add matching files from specified dir - elif not len(path) == 0 and not options.recursive: - inputs.extend(glob.glob(file_path)) - - # Otherwise, recursion specified and there is a directory. - # Recurse directory and add files - else: - addFilesFromDirectory(inputs, path, wildcard) - - # Just a normal file, append to list of inputs - else: - inputs.append(file_path) - - if options.parallel or options.threaded: - import multiprocessing - procs = [] - q = multiprocessing.Queue() - kwargs = options.__dict__.copy() # put parsed base options in kwargs - kwargs.update(config=None, outfile=None, queue=q) # pass the q, - # do not pass the config file or outfile because we handled that here - for d in decoder_options: # put pre-parsed decoder options in kwargs - for k, v in decoder_options[d].items(): - kwargs[d + '_' + k] = v - - # check here to see if we are running in parallel-file mode - if options.parallel and len(inputs) > 1: - for f in inputs: - # create a child process for each input file - procs.append( - multiprocessing.Process(target=main, kwargs=kwargs, args=[f])) - runChildProcs(procs, q, out, numprocs=options.numprocs) - - # check here to see if we are running decoders multithreaded - elif options.threaded and len(options.decoder) > 1: - for d in options.decoder: - # create a child for each decoder - kwargs.update(decoder=d) - procs.append( - multiprocessing.Process(target=main, kwargs=kwargs, args=inputs)) - runChildProcs(procs, q, out, numprocs=options.numprocs) - - # fall through to here (single threaded or child process) - else: - # - # Here is where we use the decoder(s) to process the pcap - # - - temporaryFiles = [] # used when uncompressing files - - for module in decoders.keys(): - decoder = decoders[module] - initDecoderOptions( - decoder, out, options, decoder_args, decoder_options) - - # If the decoder has a preModule function, will execute it now - decoder.preModule() - - for input_file in inputs: - # Decoder-specific options may be seen as input files - # Skip anything starts with "--" - if input_file[:2] == '--': - continue - - # Recursive directory processing is handled elsewhere, - # so we should only be dealing with files at this point. - if os.path.isdir(input_file): - continue - - log('+Processing file %s' % input_file) - - # assume the input_file is not compressed - # Allows the processing of .pcap files that are compressed with - # gzip, bzip2, or zip. Writes uncompressed file to a - # NamedTemporaryFile and unlinks the file once it is no longer - # needed. Might consider using mkstemp() since this implementation - # requires Python >= 2.6. - try: - exts = ['.gz', '.bz2', '.zip'] - if os.path.splitext(input_file)[1] not in exts: - pcapfile = input_file - - else: - # we have a compressed file - tmpfile = expandCompressedFile( - input_file, options.verbose, options.tmpdir) - temporaryFiles.append(tmpfile) - pcapfile = tmpfile - except: - if options.verbose: - sys.stderr.write( - '+Error processing file %s' % (input_file)) - continue - - # give the decoder access to the input filename - # motivation: run a decoder against a large number of pcap - # files and have the decoder print the filename - # so you can go straight to the pcap file for - # further analysis - decoder.input_file = input_file - - # Check to see if the decoder has a preFile function - # This will be called before the decoder processes each - # input file - decoder.preFile() - - try: - if not pcap: - raise NotImplementedError( - "pcap support not implemented") - decoder.capture = pcap.pcap(pcapfile) - if decoder.filter: - decoder.capture.setfilter(decoder.filter) - while not options.count or decoder.count < options.count: - try: - # read next packet and break on EOF - ts, pkt = decoder.capture.next() - except: - break # no data - decoder.decode(ts, pkt) - except KeyboardInterrupt: - raise - except: - traceback.print_exc() - - if options.verbose: - log('+Done processing %s' % (input_file)) - - # call that decoder's processFile() - decoder.postFile() - - # check to see if the decoder is using the Messages class - # if so, we need to clean up the connection store to - # purge any unfinished connections - if 'cleanConnectionStore' in dir(decoder): - decoder.cleanConnectionStore() - - # Check to see if the decoder has a postModule function - # A postModule function will be called when the module - # has finished running against all of the input files - if 'postModule' in dir(decoder): - decoder.postModule() - - # remove any temporary files that were created during execution - for tmpfile in temporaryFiles: - if options.verbose: - log('+Unlinking %s' % (tmpfile)) - os.unlink(tmpfile) - - # close output - out.close() - return - - -def runChildProcs(procs, q, out, numprocs=4): - import Queue - running = [] - # while we still have processes to spawn or running - while procs or running: - if procs and len(running) < numprocs: - proc = procs.pop(0) - proc.start() - out.log('started %d' % proc.pid, level=logging.INFO) - running.append(proc) - for proc in running: - if not proc.is_alive(): # see if it finished - out.log('%d exited (%d)' % - (proc.pid, proc.exitcode), level=logging.INFO) - running.remove(proc) - try: # get from the output queue until empty - while True: - m, args, kw = q.get(True, 1) # method, listargs, kwargs - out.dispatch(m, *args, **kw) # dispatch to method - except Queue.Empty: - pass # q empty - - -if __name__ == '__main__': - try: - main(*sys.argv[1:]) - except KeyboardInterrupt: - sys.exit(0) diff --git a/bin/generate-dshellrc.py b/bin/generate-dshellrc.py deleted file mode 100755 index 2de6bd3..0000000 --- a/bin/generate-dshellrc.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/python - -import os -import sys - -if __name__ == '__main__': - cwd = sys.argv[1] - - # environment variables used by shell and modules - envvars = { - 'DSHELL': '%s' % (cwd), - 'DECODERPATH': '%s/decoders' % (cwd), - 'BINPATH': '%s/bin' % (cwd), - 'LIBPATH': '%s/lib' % (cwd), - 'DATAPATH': '%s/share' % (cwd), - } - # further shell environment setup - envsetup = { - 'LD_LIBRARY_PATH': '$LIBPATH:$LD_LIBRARY_PATH', - 'PATH': '$BINPATH:$PATH', - 'PYTHONPATH': '$DSHELL:$LIBPATH:$LIBPATH/output:' + os.path.join('$LIBPATH', 'python' + '.'.join(sys.version.split('.', 3)[:2]).split(' ')[0], 'site-packages') + ':$PYTHONPATH'} - - try: - os.mkdir(os.path.join( - cwd, 'lib', 'python' + '.'.join(sys.version.split('.', 3)[:2]).split(' ')[0])) - os.mkdir(os.path.join(cwd, 'lib', 'python' + - '.'.join(sys.version.split('.', 3)[:2]).split(' ')[0], 'site-packages')) - except Exception, e: - print e - - envdict = {} - envdict.update(envvars) - envdict.update(envsetup) - - #.dshellrc text - env = ['export PS1="`whoami`@`hostname`:\w Dshell> "'] + ['export %s=%s' % - (k, v) for k, v in envvars.items()] + ['export %s=%s' % (k, v) for k, v in envsetup.items()] - outfd = open('.dshellrc', 'w') - outfd.write("\n".join(env)) - if len(sys.argv) > 2 and sys.argv[2] == 'with_bash_completion': - outfd.write(''' - - -if [ `echo $BASH_VERSION | cut -d'.' -f1` -ge '4' ]; then -if [ -f ~/.bash_aliases ]; then -. ~/.bash_aliases -fi - -if [ -f /etc/bash_completion ]; then -. /etc/bash_completion -fi - -find_decoder() -{ -local IFS="+" -for (( i=0; i<${#COMP_WORDS[@]}; i++ )); -do - if [ "${COMP_WORDS[$i]}" == '-d' ] ; then - decoders=(${COMP_WORDS[$i+1]}) - fi -done -} - -get_decoders() -{ - decoders=$(for x in `find $DECODERPATH -iname '*.py' | grep -v '__init__'`; do basename ${x} .py; done) -} - -_decode() -{ -local dashdashcommands=' --ebpf --output --outfile --logfile' - -local cur prev xspec decoders -COMPREPLY=() -cur=`_get_cword` -_expand || return 0 -prev="${COMP_WORDS[COMP_CWORD-1]}" - -case "${cur}" in ---*) - find_decoder - local options="" -# if [ -n "$decoders" ]; then -# for decoder in "${decoders[@]}" -# do -# options+=`/usr/bin/python $BINPATH/gen_decoder_options.py $decoder` -# options+=" " -# done -# fi - - options+=$dashdashcommands - COMPREPLY=( $(compgen -W "${options}" -- ${cur}) ) - return 0 - ;; - -*+*) - get_decoders - firstdecoder=${cur%+*}"+" - COMPREPLY=( $(compgen -W "${decoders}" -P $firstdecoder -- ${cur//*+}) ) - return 0 - ;; - -esac - -xspec="*.@(cap|pcap)" -xspec="!"$xspec -case "${prev}" in --d) - get_decoders - COMPREPLY=( $(compgen -W "${decoders[0]}" -- ${cur}) ) - return 0 - ;; - ---output) - local outputs=$(for x in `find $DSHELL/lib/output -iname '*.py' | grep -v 'output.py'`; do basename ${x} .py; done) - - COMPREPLY=( $(compgen -W "${outputs}" -- ${cur}) ) - return 0 - ;; - --F | -o | --outfile | -L | --logfile) - xspec= - ;; - -esac - -COMPREPLY=( $( compgen -f -X "$xspec" -- "$cur" ) \ -$( compgen -d -- "$cur" ) ) -} -complete -F _decode -o filenames decode -complete -F _decode -o filenames decode.py -fi -''') - outfd.close() - - # dshell text - outfd = open('dshell', 'w') - outfd.write('#!/bin/bash\n') - outfd.write('/bin/bash --rcfile %s/.dshellrc\n' % (cwd)) - outfd.close() - - # dshell-decode text - outfd = open('dshell-decode', 'w') - outfd.write('#!/bin/bash\n') - outfd.write('source %s/.dshellrc\n' % (cwd)) - outfd.write('decode "$@"') - outfd.close() diff --git a/bin/pcapanon.py b/bin/pcapanon.py deleted file mode 100755 index 1a61478..0000000 --- a/bin/pcapanon.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/env python -''' -Created on Feb 6, 2012 - -@author: tparker -''' - -import sys -import dpkt -import struct -import pcap -import socket -import time -from Crypto.Random import random -from Crypto.Hash import SHA -from output import PCAPWriter -from util import getopts - - -def hashaddr(addr, *extra): - # hash key+address plus any extra data (ports if flow) - global key, ip_range, ip_mask - sha = SHA.new(key + addr) - for e in extra: - sha.update(str(extra)) - # take len(addr) octets of digest as address, to int, mask, or with range, - # back to octets - return inttoip((iptoint(sha.digest()[0:len(addr)]) & ip_mask) | ip_range) - - -def mangleMAC(addr): - global zero_mac - if zero_mac: - return "\x00\x00\x00\x00\x00\x00" - if addr in emap: - return emap[addr] - haddr = None - if addr == "\x00\x00\x00\x00\x00\x00": - haddr = addr # return null MAC - if ord(addr[0]) & 0x01: - haddr = addr # mac&0x800000000000 == broadcast addr, do not touch - if not haddr: - haddr = hashaddr(addr) - # return hash bytes with first byte set to xxxxxx10 (LAA unicast) - haddr = chr(ord(haddr[0]) & 0xfc | 0x2) + haddr[1:6] - emap[addr] = haddr - return haddr - - -def mangleIP(addr, *ports): # addr,extra=our_port,other_port - global exclude, exclude_port, anon_all, by_flow - haddr = None - intip = iptoint(addr) - if len(addr) == 4 and intip >= 0xE0000000: - haddr = addr # pass multicast 224.x.x.x and higher - ip = iptoa(addr) - # pass 127.x.x.x, IANA reserved, and autoconfig ranges - if not anon_all and (ip.startswith('127.') - or ip.startswith('10.') - or ip.startswith('172.16.') - or ip.startswith('192.168.') - or ip.startswith('169.254.')): - haddr = addr - # pass ips matching exclude - for x in exclude: - if ip.startswith(x): - haddr = addr - if ports and ports[0] in exclude_port: - haddr = addr # if our port is exclude - if not haddr: - if by_flow: - # use ports if by flow, else just use ip - haddr = hashaddr(addr, *ports) - else: - haddr = hashaddr(addr) - return haddr - - -def mangleIPs(src, dst, sport, dport): - if by_flow: # if by flow, hash addresses with s/d ports - if (src, sport, dst, dport) in ipmap: - src, dst = ipmap[(src, sport, dst, dport)] - elif (dst, dport, src, sport) in ipmap: - # make sure reverse flow maps same - dst, src = ipmap[(dst, dport, src, sport)] - else: - src, dst = ipmap.setdefault( - (src, sport, dst, dport), (mangleIP(src, sport, dport), mangleIP(dst, dport, sport))) - else: - if src in ipmap: - src = ipmap[src] - else: - src = ipmap.setdefault(src, mangleIP(src, sport)) - if dst in ipmap: - dst = ipmap[dst] - else: - dst = ipmap.setdefault(dst, mangleIP(dst, dport)) - return src, dst - - -def mactoa(addr): - return ':'.join(['%02x' % b for b in struct.unpack('6B', addr)]) - - -def iptoa(addr): - if len(addr) is 16: - return socket.inet_ntop(socket.AF_INET6, addr) - else: - return socket.inet_ntop(socket.AF_INET, addr) - - -def iptoint(addr): - if len(addr) is 16: # ipv6 to long - ip = struct.unpack('!IIII', addr) - return ip[0] << 96 | ip[1] << 64 | ip[2] << 32 | ip[3] - else: - return struct.unpack('!I', addr)[0] # ip to int - - -def inttoip(l): - if l > 0xffffffff: # ipv6 - return struct.pack('!IIII', l >> 96, l >> 64 & 0xffffffff, l >> 32 & 0xffffffff, l & 0xffffffff) - else: - return struct.pack('!I', l) - - -def pcap_handler(ts, pktdata): - global init_ts, start_ts, replace_ts, by_flow, anon_mac, zero_mac - if not init_ts: - init_ts = ts - if replace_ts: - ts = start_ts + (ts - init_ts) # replace timestamps - try: - pkt = dpkt.ethernet.Ethernet(pktdata) - if anon_mac or zero_mac: - pkt.src = mangleMAC(pkt.src) - pkt.dst = mangleMAC(pkt.dst) - if pkt.type == dpkt.ethernet.ETH_TYPE_IP: - try: - # TCP or UDP? - sport, dport = pkt.data.data.sport, pkt.data.data.dport - except: - sport = dport = None # nope - pkt.data.src, pkt.data.dst = mangleIPs( - pkt.data.src, pkt.data.dst, sport, dport) - pktdata = str(pkt) - except Exception, e: - print e - out.write(len(pktdata), pktdata, ts) - -if __name__ == '__main__': - - global key, init_ts, start_ts, replace_ts, by_flow, anon_mac, zero_mac, exclude, exclude_port, anon_all, ip_range, ip_mask - opts, args = getopts(sys.argv[1:], 'i:aezftx:p:rk:', [ - 'ip=', 'all', 'ether', 'zero', 'flow', 'ts', 'exclude=', 'random', 'key=', 'port='], ['-x', '--exclude', '-p', '--port']) - - if '-r' in opts or '--random' in opts: - key = random.long_to_bytes(random.getrandbits(64), 8) - else: - key = '' - key = opts.get('-k', opts.get('--key', key)) - - ip_range = opts.get('-i', opts.get('--ip', '0.0.0.0')) - ip_mask = 0 # bitmask for hashed address - ipr = '' - for o in map(int, ip_range.split('.')): - ipr += chr(o) - ip_mask <<= 8 # shift by 8 bits - if not o: - ip_mask |= 0xff # set octet mask to 0xff if ip_range octet is zero - ip_range = iptoint(ipr) # convert to int value for hash&mask|ip_range - - replace_ts = '-t' in opts or '--ts' in opts - by_flow = '-f' in opts or '--flow' in opts - anon_mac = '-e' in opts or '--ether' in opts - zero_mac = '-z' in opts or '--zero' in opts - anon_all = '-a' in opts or '--all' in opts - - start_ts = time.time() - init_ts = None - - exclude = opts.get('-x', []) - exclude.extend(opts.get('--exclude', [])) - - exclude_port = map(int, opts.get('-p', [])) - exclude_port.extend(map(int, opts.get('--port', []))) - - emap = {} - ipmap = {} - - if len(args) < 2: - print "usage: pcapanon.py [options] > mapping.csv\nOptions:\n\t[-i/--ip range]\n\t[-r/--random | -k/--key 'salt' ]\n\t[-a/--all] [-t/--ts] [-f/--flow]\n\t[-e/--ether | -z/--zero]\n\t[-x/--exclude pattern...]\n\t[-p/--port list...]" - print "Will anonymize all non-reserved IPs to be in range specified by -i/--ip option," - print "\tnonzero range octets are copied to anonymized address,\n\t(default range is 0.0.0.0 for fully random IPs)" - print "CSV output maps original to anonymized addresses" - print "By default anonymization will use a straight SHA1 hash of the address" - print "\t***this is crackable as mapping is always the same***".upper() - print "Use -r/--random to generate a random salt (cannot easily reverse without knowing map)" - print "\tor use -k/--key 'salt' (will generate same mapping given same salt)," - print "-f/--flows will anonymize by flow (per source:port<->dest:port tuples)" - print "-a/--all will also anonymize reserved IPs" - print "-x/--exclude will leave IPs starting with pattern unchanged" - print "-p/--port port will leave IP unchanged if port is in list" - print "-t/--ts will replace timestamp of first packet with time pcapanon was run,\n\tsubsequent packets will preserve delta from initial ts" - print "-e/--ether will also anonymize non-broadcast MAC addresses" - print "-z/--zero will zero all MAC addresses" - sys.exit(0) - - out = PCAPWriter(args[-1]) - print '#file, packets' - for f in args[0:-1]: - p = 0 - cap = pcap.pcap(f) - while cap.dispatch(1, pcap_handler): - p += 1 # process whole file - del cap - print '%s,%s' % (f, p) - out.close() - - print "#type,is-anonymized, original, anonymized" - for ia, oa in sorted(emap.items()): - print 'ether,%d, %s, %s' % (int(not ia == oa), mactoa(ia), mactoa(oa)) - for ia, oa in sorted(ipmap.items()): - if by_flow: - sip, sp, dip, dp = ia - osip, odip = oa - print "flow,%d, %s:%s,%s:%s, %s:%s,%s:%s" % (int(sip != osip or dip != odip), iptoa(sip), sp, iptoa(dip), dp, iptoa(osip), sp, iptoa(odip), dp) - else: - print 'ip,%d, %s, %s' % (int(ia != oa), iptoa(ia), iptoa(oa)) diff --git a/bin/pcapslice.py b/bin/pcapslice.py deleted file mode 100755 index 9e7998b..0000000 --- a/bin/pcapslice.py +++ /dev/null @@ -1,249 +0,0 @@ -#!/usr/bin/env python -''' -split pcap files by ip src/dst pair or tcp/udp stream - -Originally created February 2013 -Updated from pylibpcap to pypcap, November 2015 - -@author: amm -''' - -import sys -import os -import pcap -import dpkt -import signal -import socket -import output -from optparse import OptionParser - -IPprotocols = { - 0: 'IP', 1: 'ICMP', 2: 'IGMP', 3: 'GGP', 4: 'IP-ENCAP', 133: 'FC', 6: 'TCP', 8: 'EGP', 137: 'MPLS-IN-IP', 138: 'MANET', 139: 'HIP', 12: 'PUP', 17: 'UDP', 20: 'HMP', 22: 'XNS-IDP', 132: 'SCTP', 27: 'RDP', 29: 'ISO-TP4', 5: 'ST', 36: 'XTP', 37: 'DDP', 38: 'IDPR-CMTP', 41: 'IPV6', 43: 'IPV6-ROUTE', 44: 'IPV6-FRAG', - 45: 'IDRP', 46: 'RSVP', 47: 'GRE', 136: 'UDPLITE', 50: 'IPSEC-ESP', 51: 'IPSEC-AH', 9: 'IGP', 57: 'SKIP', 58: 'IPV6-ICMP', 59: 'IPV6-NONXT', 60: 'IPV6-OPTS', 73: 'RSPF', 81: 'VMTP', 88: 'EIGRP', 89: 'OSPFIGP', 93: 'AX.25', 94: 'IPIP', 97: 'ETHERIP', 98: 'ENCAP', 103: 'PIM', 108: 'IPCOMP', 112: 'VRRP', 115: 'L2TP', 124: 'ISIS'} -flowtimeout = 1800 # seconds -ctrl_c_Received = False - -''' -main -''' - - -def main(): - global options, ctrl_c_Received - - flows = flowstore() - - parser = OptionParser( - usage="usage: %prog [options] file", version="%prog: PCAP Slicer") - parser.add_option('-f', '--bpf', dest='bpf', help='BPF input filter') - parser.add_option('-o', '--outdir', dest='outdir', default='.', - help='directory to write output files (Default: current directory)') - parser.add_option('--no-vlan', dest='novlan', action="store_true", - help='do not examine traffic which has VLAN headers present') - parser.add_option('--debug', action='store_true', dest='debug') - (options, args) = parser.parse_args(sys.argv[1:]) - - if not args: - parser.print_version() - parser.print_help() - sys.exit() - - filter = '' - if options.bpf != None: - filter = options.bpf - if not options.novlan and not(filter.startswith('vlan')): - if filter: - filter = '( ' + filter + ' ) or ( vlan and ( ' + filter + ' ) )' - else: - filter = '' # fix for null filter case - - pcount = 0 - for f in args: - pcapreader = pcap.pcap(f) - if options.bpf: - pcapreader.setfilter(filter) - while True: - # Pick a packet - try: - ts, spkt = pcapreader.next() - except: - break # EOF - # Parse IP/Port/Proto Information - try: - pkt = dpkt.ethernet.Ethernet(spkt) - # Only handle IP4/6 - if type(pkt.data) == dpkt.ip.IP: - proto = pkt.data.p - elif type(pkt.data) == dpkt.ip6.IP6: - proto = pkt.data.nxt - else: - continue - # Populate addr tuple - # (proto, sip, sport, dip, dport) - if proto == dpkt.ip.IP_PROTO_TCP or proto == dpkt.ip.IP_PROTO_UDP: - addr = ( - proto, pkt.data.src, pkt.data.data.sport, pkt.data.dst, pkt.data.data.dport) - else: - addr = (proto, pkt.data.src, None, pkt.data.dst, None) - except: - continue # Skip Packet if unable to parse - pcount += 1 - # - # Look for existing open flow or start new one - # - thisflow = flows.find(addr) - if thisflow == None: - thisflow = flow(addr) - flows.add(thisflow) - warn("New flow to file: %s" % str(thisflow)) - # - # Write this packet to correct flow - # - thisflow.write(len(spkt), spkt, ts) - # - # Check for TCP reset or fin - # - try: - if pkt.data.data.flags & (dpkt.tcp.TH_RST | dpkt.tcp.TH_FIN): - thisflow.done() - except: - pass # probably not a TCP packet - # - # Cleanup Routine - # - if pcount % 1000 == 0: - flows.cleanup(ts) - # - # Clean exit - # - if ctrl_c_Received: - sys.stderr.write("Exiting on interrupt signal.\n") - sys.exit(0) -''' -flow class - instantiated for each bi-directional flow of data - maintains pcapwriter for each open session -''' - - -class flow: - - def __init__(self, addr): - self.addr = addr - self.outfilename = localfilename(addr) - self.pcapwriter = output.PCAPWriter(self.outfilename) - self.state = 1 - self.lastptime = 0 - - def write(self, l, spkt, ts): - self.pcapwriter.write(l, spkt, ts) - self.lastptime = ts - - # Mark flow as done (RST/FIN received) - # but don't close the pcap file yet - def done(self): - self.state = 0 - - def __del__(self): - warn("Closing file: %s" % self.outfilename) - - def __str__(self): - return self.outfilename - - def __repr__(self): - return self.outfilename - -''' -flowstore class -''' - - -class flowstore: - global flowtimeout - - def __init__(self): - self.data = {} # indexed by addr tuple (proto, sip, sport, dip, dport) - - def find(self, addr): - # Fwd Search - if addr in self.data: - return self.data[addr] - # Rev Search - (proto, sip, sport, dip, dport) = addr - if (proto, dip, dport, sip, sport) in self.data: - return self.data[(proto, dip, dport, sip, sport)] - return None - - def add(self, newflow): - self.data[newflow.addr] = newflow - - def cleanup(self, currentPtime): - for k in self.data.keys(): - if self.data[k].state > 0: - continue - # Check timeout - if currentPtime - self.data[k].lastptime > flowtimeout: - del self.data[k] - - -def warn(text): - sys.stdout.write("WARN: " + str(text) + "\n") - - -def normalizedIP(packed): - if len(packed) == 16: - return socket.inet_ntop(socket.AF_INET6, packed) - else: - ip = socket.inet_ntoa(packed) - if '.' in ip: - parts = ip.split('.') - return '.'.join(['%03d' % int(p) for p in parts]) - return ip - - -def localfilename(addr): - global IPprotocols, options - (proto, sip, sport, dip, dport) = addr - # Convert Numeric Protocol to Text - if proto in IPprotocols: - proto = IPprotocols[proto] - else: - proto = '%05d' % int(proto) - # Convert packed IPs to Text - nameparts = [proto, normalizedIP(sip), normalizedIP(dip)] - try: - nameparts.append('%05d' % int(sport)) - except: - pass - try: - nameparts.append('%05d' % int(dport)) - except: - pass - # Filename - fname = '_'.join(nameparts) - inc = 0 - while True: - fullname = os.path.join(options.outdir, '%s_%03d.pcap' % (fname, inc)) - if not os.path.exists(fullname): - return fullname - inc += 1 - - -''' -handle interupt events -''' - - -def ctrlchandler(signum, frame): - global ctrl_c_Received - ctrl_c_Received = True - sys.stderr.write("Interrupt received. Will exit at next clean break.\n") - -if __name__ == '__main__': - signal.signal(signal.SIGINT, ctrlchandler) - # sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', 0) # reopen STDOUT - # unbuffered - try: - main() - except KeyboardInterrupt: - sys.exit(0) diff --git a/decoders/dhcp/dhcp.py b/decoders/dhcp/dhcp.py deleted file mode 100644 index 7073bf5..0000000 --- a/decoders/dhcp/dhcp.py +++ /dev/null @@ -1,89 +0,0 @@ -import dpkt -import dshell -import util -from struct import unpack -import binascii - -class DshellDecoder(dshell.UDPDecoder): - - def __init__(self): - dshell.UDPDecoder.__init__(self, - name='dhcp', - description='Extract client information from DHCP messages', - longdescription=""" -The dhcp decoder will extract the Transaction ID, Client Hostname, and -Client MAC address from every UDP DHCP packet found in the given pcap -using port 67. DHCP uses BOOTP as its transport protocol. -BOOTP traffic generally uses ports 67 and 68 for outgoing and incoming traffic. -This filter pulls DHCP Inform packets. - -Examples: - - General usage: - - decode -d dhcp - - This will display the connection info including the timestamp, - the source IP : source port, destination IP : destination port, - Transaction ID, Client Hostname, and the Client MAC address - in a tabular format. - - - Malware Traffic Analysis Exercise Traffic from 2015-03-03 where a user was hit with an Angler exploit kit: - - We want to find out more about the infected machine, and some of this information can be pulled from DHCP traffic - - decode -d dhcp /2015-03-03-traffic-analysis-exercise.pcap - - OUTPUT: - dhcp 2015-03-03 14:05:10 172.16.101.196:68 -- 172.16.101.1:67 ** Transaction ID: 0xba5a2cfe Client Hostname: Gregory-PC Client MAC: 38:2c:4a:3d:ef:01 ** - dhcp 2015-03-03 14:08:40 172.16.101.196:68 -- 255.255.255.255:67 ** Transaction ID: 0x6a482406 Client Hostname: Gregory-PC Client MAC: 38:2c:4a:3d:ef:01 ** - dhcp 2015-03-03 14:10:11 172.16.101.196:68 -- 172.16.101.1:67 ** Transaction ID: 0xe74b17fe Client Hostname: Gregory-PC Client MAC: 38:2c:4a:3d:ef:01 ** - dhcp 2015-03-03 14:12:50 172.16.101.196:68 -- 255.255.255.255:67 ** Transaction ID: 0xd62614a0 Client Hostname: Gregory-PC Client MAC: 38:2c:4a:3d:ef:01 ** -""", - filter='(udp and port 67)', - author='dek', - ) - self.mac_address = None - self.client_hostname = None - self.xid = None - - - # A packetHandler is used to ensure that every DHCP packet in the traffic is parsed - def packetHandler(self, udp, data): - try: - dhcp_packet = dpkt.dhcp.DHCP(data) - except dpkt.NeedData as e: - self.warn('{} dpkt could not parse session data (DHCP packet not found)'.format(str(e))) - return - - # Pull the transaction ID from the packet - self.xid = hex(dhcp_packet.xid) - - # if we have a DHCP INFORM PACKET - if dhcp_packet.op == dpkt.dhcp.DHCP_OP_REQUEST: - self.debug(dhcp_packet.op) - for option_code, msg_value in dhcp_packet.opts: - - # if opt is CLIENT_ID (61) - # unpack the msg_value and reformat the MAC address - if option_code == dpkt.dhcp.DHCP_OPT_CLIENT_ID: - hardware_type, mac = unpack('B6s', msg_value) - mac = binascii.hexlify(mac) - self.mac_address = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)]) - - # if opt is HOSTNAME (12) - elif option_code == dpkt.dhcp.DHCP_OPT_HOSTNAME: - self.client_hostname = msg_value - - - if self.xid and self.client_hostname and self.mac_address: - self.alert('Transaction ID: {0:<12} Client Hostname: {1:<15} Client MAC: {2:<20}'.format( - self.xid, self.client_hostname, self.mac_address), **udp.info()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/dns/dns-asn.py b/decoders/dns/dns-asn.py deleted file mode 100644 index bb97401..0000000 --- a/decoders/dns/dns-asn.py +++ /dev/null @@ -1,76 +0,0 @@ -import dshell -import dpkt -import socket -from dnsdecoder import DNSDecoder - - -class DshellDecoder(DNSDecoder): - - def __init__(self): - DNSDecoder.__init__(self, - name='dns-asn', - description='identify AS of DNS A/AAAA record responses', - filter='(port 53)', - author='bg', - cleanupinterval=10, - maxblobs=2, - ) - - def decode_q(self, dns): - queried = "" - if dns.qd[0].type == dpkt.dns.DNS_A: - queried = queried + "A? %s" % (dns.qd[0].name) - if dns.qd[0].type == dpkt.dns.DNS_AAAA: - queried = queried + "AAAA? %s" % (dns.qd[0].name) - return queried - - def DNSHandler(self, conn, request, response, **kwargs): - anstext = '' - queried = '' - id = None - for dns in request, response: - if dns is None: - continue - id = dns.id - # DNS Question, update connection info with query - if dns.qr == dpkt.dns.DNS_Q: - conn.info(query=self.decode_q(dns)) - - # DNS Answer with data and no errors - elif (dns.qr == dpkt.dns.DNS_A and dns.rcode == dpkt.dns.DNS_RCODE_NOERR and len(dns.an) > 0): - - queried = self.decode_q(dns) - - answers = [] - for an in dns.an: - if an.type == dpkt.dns.DNS_A: - try: - cc = self.getASN(socket.inet_ntoa(an.ip)) - answers.append( - 'A: %s (%s) (ttl %s)' % (socket.inet_ntoa(an.ip), cc, an.ttl)) - except: - continue - elif an.type == dpkt.dns.DNS_AAAA: - try: - cc = self.getASN( - socket.inet_ntop(socket.AF_INET6, an.ip6)) - answers.append('AAAA: %s (%s) (ttl %s)' % ( - socket.inet_ntop(socket.AF_INET6, an.ip6), cc, an.ttl)) - except: - continue - else: - # un-handled type - continue - if queried != '': - anstext = ", ".join(answers) - - if anstext: # did we get an answer? - self.alert( - str(id) + ' ' + queried + ' / ' + anstext, **conn.info(response=anstext)) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/dns/dns-cc.py b/decoders/dns/dns-cc.py deleted file mode 100644 index 99fbe51..0000000 --- a/decoders/dns/dns-cc.py +++ /dev/null @@ -1,86 +0,0 @@ -import dshell -import dpkt -import socket -from dnsdecoder import DNSDecoder - - -class DshellDecoder(DNSDecoder): - - def __init__(self): - DNSDecoder.__init__(self, - name='dns-cc', - description='identify country code of DNS A/AAAA record responses', - filter='(port 53)', - author='bg', - cleanupinterval=10, - maxblobs=2, - optiondict={'foreign': {'action': 'store_true', 'help': 'report responses in foreign countries'}, - 'code': {'type': 'string', 'help': 'filter on a specific country code (ex. US)'}} - ) - - def decode_q(self, dns): - queried = "" - if dns.qd[0].type == dpkt.dns.DNS_A: - queried = queried + "A? %s" % (dns.qd[0].name) - if dns.qd[0].type == dpkt.dns.DNS_AAAA: - queried = queried + "AAAA? %s" % (dns.qd[0].name) - return queried - - def DNSHandler(self, conn, request, response, **kwargs): - anstext = '' - queried = '' - id = None - for dns in request, response: - if dns is None: - continue - id = dns.id - # DNS Question, update connection info with query - if dns.qr == dpkt.dns.DNS_Q: - conn.info(query=self.decode_q(dns)) - - # DNS Answer with data and no errors - elif (dns.qr == dpkt.dns.DNS_A and dns.rcode == dpkt.dns.DNS_RCODE_NOERR and len(dns.an) > 0): - - queried = self.decode_q(dns) - - answers = [] - for an in dns.an: - if an.type == dpkt.dns.DNS_A: - try: - cc = self.getGeoIP(socket.inet_ntoa(an.ip)) - if self.foreign and (cc == 'US' or cc == '--'): - continue - elif self.code != None and cc != self.code: - continue - answers.append( - 'A: %s (%s) (ttl %ss)' % (socket.inet_ntoa(an.ip), cc, an.ttl)) - except: - continue - elif an.type == dpkt.dns.DNS_AAAA: - try: - cc = self.getGeoIP( - socket.inet_ntop(socket.AF_INET6, an.ip6)) - if self.foreign and (cc == 'US' or cc == '--'): - continue - elif self.code != None and cc != self.code: - continue - answers.append('AAAA: %s (%s) (ttl %ss)' % ( - socket.inet_ntop(socket.AF_INET6, an.ip6), cc, an.ttl)) - except: - continue - else: - # un-handled type - continue - if queried != '': - anstext = ", ".join(answers) - - if anstext: # did we get an answer? - self.alert( - str(id) + ' ' + queried + ' / ' + anstext, **conn.info(response=anstext)) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/dns/dns.py b/decoders/dns/dns.py deleted file mode 100755 index 1971c7d..0000000 --- a/decoders/dns/dns.py +++ /dev/null @@ -1,135 +0,0 @@ -import dpkt -import socket -from dnsdecoder import DNSDecoder - - -class DshellDecoder(DNSDecoder): - - def __init__(self): - DNSDecoder.__init__(self, - name='dns', - description='extract and summarize DNS queries/responses (defaults: A,AAAA,CNAME,PTR records)', - filter='(udp and port 53)', - author='bg/twp', - optiondict={'show_noanswer': {'action': 'store_true', 'help': 'report unanswered queries alongside other queries'}, - 'show_norequest': {'action': 'store_true', 'help': 'report unsolicited responses alongside other responses'}, - 'only_noanswer': {'action': 'store_true', 'help': 'report only unanswered queries'}, - 'only_norequest': {'action': 'store_true', 'help': 'report only unsolicited responses'}, - 'showall': {'action': 'store_true', 'help': 'show all answered queries/responses'}} - ) - - def decode_q(self, dns): - queried = "" - if dns.qd[0].type == dpkt.dns.DNS_A: - queried = queried + "A? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_CNAME: - queried = queried + "CNAME? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_AAAA: - queried = queried + "AAAA? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_SOA: - queried = queried + "SOA? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_PTR: - if dns.qd[0].name.endswith('.in-addr.arpa'): - query_name = '.'.join( - reversed(dns.qd[0].name.split('.in-addr.arpa')[0].split('.'))) - else: - query_name = dns.qd[0].name - queried = queried + "PTR? %s" % (query_name) - - if not self.showall: - return queried - - if dns.qd[0].type == dpkt.dns.DNS_NS: - queried = queried + "NS? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_MX: - queried = queried + "MX? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_TXT: - queried = queried + "TXT? %s" % (dns.qd[0].name) - elif dns.qd[0].type == dpkt.dns.DNS_SRV: - queried = queried + "SRV? %s" % (dns.qd[0].name) - - return queried - - def DNSHandler(self, conn, request, response, **kwargs): - if self.only_norequest and request is not None: - return - if not self.show_norequest and request is None: - return - anstext = '' - queried = '' - id = None - for dns in request, response: - if dns is None: - continue - id = dns.id - # DNS Question, update connection info with query - if dns.qr == dpkt.dns.DNS_Q: - conn.info(query=self.decode_q(dns)) - - # DNS Answer with data and no errors - elif (dns.rcode == dpkt.dns.DNS_RCODE_NOERR and len(dns.an) > 0): - - queried = self.decode_q(dns) - - answers = [] - for an in dns.an: - if an.type == dpkt.dns.DNS_A: - try: - answers.append( - 'A: %s (ttl %ss)' % (socket.inet_ntoa(an.ip), str(an.ttl))) - except: - continue - elif an.type == dpkt.dns.DNS_AAAA: - try: - answers.append('AAAA: %s (ttl %ss)' % ( - socket.inet_ntop(socket.AF_INET6, an.ip6), str(an.ttl))) - except: - continue - elif an.type == dpkt.dns.DNS_CNAME: - answers.append('CNAME: ' + an.cname) - elif an.type == dpkt.dns.DNS_PTR: - answers.append('PTR: ' + an.ptrname) - elif an.type == dpkt.dns.DNS_NS: - answers.append('NS: ' + an.nsname) - elif an.type == dpkt.dns.DNS_MX: - answers.append('MX: ' + an.mxname) - elif an.type == dpkt.dns.DNS_TXT: - answers.append('TXT: ' + ' '.join(an.text)) - elif an.type == dpkt.dns.DNS_SRV: - answers.append('SRV: ' + an.srvname) - else: - # un-handled type - continue - if queried != '': - anstext = ", ".join(answers) - - #NXDOMAIN in response - elif dns.qr == dpkt.dns.DNS_A and dns.rcode == dpkt.dns.DNS_RCODE_NXDOMAIN: - queried = self.decode_q(dns) # decode query part - - if queried != '': - anstext = 'NXDOMAIN' - - #SOA response - elif dns.qd[0].type == dpkt.dns.DNS_SOA and len(dns.ns): - queried = self.decode_q(dns) - answers = [] - for ns in dns.ns: - if ns.type == dpkt.dns.DNS_SOA: - answers.append('SOA: '+ ns.mname) - anstext = ", ".join(answers) - - - # did we get an answer? - if anstext and not self.only_noanswer and not self.only_norequest: - self.alert( - str(id) + ' ' + queried + ' / ' + anstext, **conn.info(response=anstext)) - elif not anstext and (self.show_noanswer or self.only_noanswer): - self.alert( - str(id) + ' ' + conn.query + ' / (no answer)', **conn.info()) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/dns/innuendo-dns.py b/decoders/dns/innuendo-dns.py deleted file mode 100644 index 0799c18..0000000 --- a/decoders/dns/innuendo-dns.py +++ /dev/null @@ -1,93 +0,0 @@ -import dpkt -from dnsdecoder import DNSDecoder -import base64 - - -class DshellDecoder(DNSDecoder): - - """ - Proof-of-concept Dshell decoder to detect INNUENDO DNS Channel - - Based on the short marketing video [http://vimeo.com/115206626] the INNUENDO - DNS Channel relies on DNS to communicate with an authoritative name server. - The name server will respond with a base64 encoded TXT answer. This decoder - will analyze DNS TXT queries and responses to determine if it matches the - network traffic described in the video. There are multiple assumptions (*very - poor*) in this detection plugin but serves as a proof-of-concept detector. This - detector has not been tested against authentic INNUENDO DNS Channel traffic. - - Usage: decode -d innuendo-dns *.pcap - - """ - - def __init__(self): - DNSDecoder.__init__(self, - name='innuendo-dns', - description='proof-of-concept detector for INNUENDO DNS channel', - filter='(port 53)', - author='primalsec', - ) - self.whitelist = [] # probably be necessary to whitelist A/V domains - - def in_whitelist(self, domain): - # add logic - return False - - def decrypt_payload(payload): pass - - def DNSHandler(self, conn, request, response, **kwargs): - query = '' - answers = [] - - for dns in request, response: - - if dns is None: - continue - - id = dns.id - - # DNS Question, extract query name if it is a TXT record request - if dns.qr == dpkt.dns.DNS_Q and dns.qd[0].type == dpkt.dns.DNS_TXT: - query = dns.qd[0].name - - # DNS Answer with data and no errors - elif (dns.qr == dpkt.dns.DNS_A and dns.rcode == dpkt.dns.DNS_RCODE_NOERR and len(dns.an) > 0): - - for an in dns.an: - if an.type == dpkt.dns.DNS_TXT: - answers.append(an.text[0]) - - if query != '' and len(answers) > 0: - # add check here to see if the second level domain and top level - # domain are not in a white list - if self.in_whitelist(query): - return - - # assumption: INNUENDO will use the lowest level domain for C2 - # example: AAAABBBBCCCC.foo.bar.com -> AAAABBBBCCCC is the INNUENDO - # data - subdomain = query.split('.')[0] - - # weak test based on video observation *very poor assumption* - if subdomain.isupper(): - # check each answer in the TXT response - for answer in answers: - try: - # INNUENDO DNS channel base64 encodes the response, check to see if - # it contains a valid base64 string *poor assumption* - dummy = base64.b64decode(answer) - - self.alert( - 'INNUENDO DNS Channel', query, '/', answer, **conn.info()) - - # here would be a good place to decrypt the payload (if you have the keys) - # decrypt_payload( answer ) - except: - pass - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/dns/reservedips.py b/decoders/dns/reservedips.py deleted file mode 100644 index 8478dce..0000000 --- a/decoders/dns/reservedips.py +++ /dev/null @@ -1,151 +0,0 @@ -import dshell -import dpkt -import socket -from dnsdecoder import DNSDecoder -import IPy - - -class DshellDecoder(DNSDecoder): - - def __init__(self): - DNSDecoder.__init__(self, - name='reservedips', - description='identify DNS resolutions that fall into reserved ip space', - filter='(port 53)', - author='bg', - cleanupinterval=10, - maxblobs=2, - ) - - # source: https://en.wikipedia.org/wiki/Reserved_IP_addresses - nets = ['0.0.0.0/8', # Used for broadcast messages to the current ("this") network as specified by RFC 1700, page 4. - # Used for local communications within a private network as - # specified by RFC 1918. - '10.0.0.0/8', - # Used for communications between a service provider and its - # subscribers when using a Carrier-grade NAT, as specified by - # RFC 6598. - '100.64.0.0/10', - # Used for loopback addresses to the local host, as specified - # by RFC 990. - '127.0.0.0/8', - # Used for autoconfiguration between two hosts on a single - # link when no IP address is otherwise specified - '169.254.0.0/16', - # Used for local communications within a private network as - # specified by RFC 1918 - '172.16.0.0/12', - # Used for the DS-Lite transition mechanism as specified by - # RFC 6333 - '192.0.0.0/29', - # Assigned as "TEST-NET" in RFC 5737 for use solely in - # documentation and example source code and should not be used - # publicly - '192.0.2.0/24', - # Used by 6to4 anycast relays as specified by RFC 3068 - '192.88.99.0/24', - # Used for local communications within a private network as - # specified by RFC 1918 - '192.168.0.0/16', - # Used for testing of inter-network communications between two - # separate subnets as specified in RFC 2544 - '198.18.0.0/15', - # Assigned as "TEST-NET-2" in RFC 5737 for use solely in - # documentation and example source code and should not be used - # publicly - '198.51.100.0/24', - # Assigned as "TEST-NET-3" in RFC 5737 for use solely in - # documentation and example source code and should not be used - # publicly - '203.0.113.0/24', - # Reserved for multicast assignments as specified in RFC 5771 - '224.0.0.0/4', - # Reserved for future use, as specified by RFC 6890 - '240.0.0.0/4', - # Reserved for the "limited broadcast" destination address, as - # specified by RFC 6890 - '255.255.255.255/32', - - '::/128', # Unspecified address - '::1/128', # loopback address to the local host. - '::ffff:0:0/96', # IPv4 mapped addresses - '100::/64', # Discard Prefix RFC 6666 - '64:ff9b::/96', # IPv4/IPv6 translation (RFC 6052) - '2001::/32', # Teredo tunneling - # Overlay Routable Cryptographic Hash Identifiers (ORCHID) - '2001:10::/28', - '2001:db8::/32', # Addresses used in documentation - '2002::/16', # 6to4 - 'fc00::/7', # Unique local address - 'fe80::/10', # Link-local address - 'ff00::/8', # Multicast - ] - - self.reservednets = [] - for net in nets: - self.reservednets.append(IPy.IP(net)) - self.domains = [] # list for known domains - - def inReservedSpace(self, ipaddress): - for net in self.reservednets: - if ipaddress in net: - return True - return False - - def decode_q(self, dns): - queried = "" - if dns.qd[0].type == dpkt.dns.DNS_A: - queried = queried + "A? %s" % (dns.qd[0].name) - if dns.qd[0].type == dpkt.dns.DNS_AAAA: - queried = queried + "AAAA? %s" % (dns.qd[0].name) - return queried - - def DNSHandler(self, conn, request, response, **kwargs): - anstext = '' - queried = '' - id = None - for dns in request, response: - if dns is None: - continue - id = dns.id - # DNS Question, update connection info with query - if dns.qr == dpkt.dns.DNS_Q: - conn.info(query=self.decode_q(dns)) - - # DNS Answer with data and no errors - elif (dns.qr == dpkt.dns.DNS_A and dns.rcode == dpkt.dns.DNS_RCODE_NOERR and len(dns.an) > 0): - - queried = self.decode_q(dns) - - answers = [] - for an in dns.an: - if an.type == dpkt.dns.DNS_A: - try: - if self.inReservedSpace(socket.inet_ntoa(an.ip)): - answers.append( - 'A: ' + socket.inet_ntoa(an.ip) + ' (ttl ' + str(an.ttl) + 's)') - except: - continue - elif an.type == dpkt.dns.DNS_AAAA: - try: - if self.inReservedSpace(socket.inet_ntop(socket.AF_INET6, an.ip6)): - answers.append( - 'AAAA: ' + socket.inet_ntop(socket.AF_INET6, an.ip6) + ' (ttl ' + str(an.ttl) + 's)') - except: - continue - else: - # un-handled type - continue - if queried != '': - anstext = ", ".join(answers) - - if anstext: # did we get an answer? - self.alert( - str(id) + ' ' + queried + ' / ' + anstext, **conn.info(response=anstext)) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/filter/asn-filter.py b/decoders/filter/asn-filter.py deleted file mode 100644 index 6abf064..0000000 --- a/decoders/filter/asn-filter.py +++ /dev/null @@ -1,100 +0,0 @@ -import dshell -import util -import netflowout - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self, **kwargs): - self.sessions = {} - self.alerts = False - self.file = None - dshell.TCPDecoder.__init__(self, - name='asn-filter', - description='filter connections on autonomous system number (ASN)', - longdescription=""" -This decoder filters connections by autonomous system numbers/names (ASN). - -Chainable decoder used to filter TCP/UDP streams by ASNs. If no -downstream (+) decoder is used the netflow data will be printed to -the screen (when using --asn-filter_alerts). If used without specifying -a asn string, the asn-filter will filter nothing out and pass -everything onto the next decoder or print it. - -Examples: - - decode -d asn-filter --asn-filter_asn AS8075 --asn-filter_alerts - - This will print the connection info for all connections where - AS8075 is the ASN for either the server of client. - - decode -d asn-filter --asn-filter_asn Google --asn-filter_alerts - - This will print the connection info for all connections where - "Google" appeared in the ASN information. - - decode -d asn-filter+followstream --asn-filter_asn AS8075 - - This will filter the streams by ASN and feed them into the - followstream decoder. -""", - filter="ip or ip6", - author='twp/nl', - optiondict={ - 'asn': {'type': 'string', 'help': 'asn for client or server'}, - 'alerts': {'action': 'store_true'}}) - '''instantiate an decoder that will call back to us once the IP decoding is done''' - self.__decoder = dshell.IPDecoder() - self.out = netflowout.NetflowOutput() - self.chainable = True - - def decode(self, *args): - if len(args) is 3: - pktlen, pktdata, ts = args # orig_len,packet,ts format (pylibpcap) - else: # ts,pktdata (pypcap) - ts, pktdata = args - pktlen = len(pktdata) - '''do normal decoder stack to track session ''' - dshell.TCPDecoder.decode(self, pktlen, pktdata, ts) - '''our hook to decode the ip/ip6 addrs, then dump the addrs and raw packet to our callback''' - self.__decoder.IPHandler = self.__callback # set private decoder to our callback - self.__decoder.decode(pktlen, pktdata, ts, raw=pktdata) - - def __callback(self, addr, pkt, ts, raw=None, **kw): - '''substitute IPhandler for forwarding packets to subdecoders''' - if addr in self.sessions or (addr[1], addr[0]) in self.sessions: # if we are not passing this session, drop the packet - if self.subDecoder: - # make it look like a capture - self.subDecoder.decode(len(raw), str(raw), ts) - else: - self.dump(raw, ts) - - def connectionInitHandler(self, conn): - '''see if we have an ASN match and if so, flag this session for forwarding or dumping''' - m = self.__asnTest(conn) - if m: - self.sessions[conn.addr] = m - - def __asnTest(self, conn): - # If no ASN specified, pass all traffic through - if not self.asn: - return True - # check criteria - if self.asn.lower() in conn.clientasn.lower(): - return u'client {0}'.format(conn.clientasn) - if self.asn.lower() in conn.serverasn.lower(): - return u'server {0}'.format(conn.serverasn) - # no match - return None - - def connectionHandler(self, conn): - if conn.addr in self.sessions and self.alerts: - self.alert(self.sessions[conn.addr], **conn.info()) - - def connectionCloseHandler(self, conn): - if conn.addr in self.sessions: - del self.sessions[conn.addr] - -dObj = DshellDecoder() -if __name__ == "__main__": - print dObj diff --git a/decoders/filter/country.py b/decoders/filter/country.py deleted file mode 100644 index 921b168..0000000 --- a/decoders/filter/country.py +++ /dev/null @@ -1,125 +0,0 @@ -''' -@author: tparker -''' - -import dshell -import netflowout - - -class DshellDecoder(dshell.TCPDecoder): - - '''activity tracker ''' - - def __init__(self, **kwargs): - ''' - Constructor - ''' - self.sessions = {} - self.alerts = False - self.file = None - dshell.TCPDecoder.__init__(self, - name='country', - description='filter connections on geolocation (country code)', - longdescription=""" -country: filter connections on geolocation (country code) - -Chainable decoder to filter TCP/UDP streams on geolocation data. If no -downstream (+) decoders are specified, netflow data will be printed to -the screen. - -Mandatory option: - - --country_code: specify (2 character) country code to filter on - -Default behavior: - - If either the client or server IP address matches the specified country, - the stream will be included. - -Modifier options: - - --country_neither: Include only streams where neither the client nor the - server IP address matches the specified country. - - --country_both: Include only streams where both the client AND the server - IP addresses match the specified country. - - --country_notboth: Include streams where the specified country is NOT BOTH - the client and server IP. Streams where it is one or - the other may be included. - - -Example: - - decode -d country traffic.pcap -W USonly.pcap --country_code US - decode -d country+followstream traffic.pcap --country_code US --country_notboth -""", - filter="ip or ip6", - author='twp', - optiondict={ - 'code': {'type': 'string', 'help': 'two-char country code'}, - 'neither': {'action': 'store_true', 'help': 'neither (client/server) is in specified country'}, - 'both': {'action': 'store_true', 'help': 'both (client/server) ARE in specified country'}, - 'notboth': {'action': 'store_true', 'help': 'specified country is not both client and server'}, - 'alerts': {'action': 'store_true'} - } - ) - # instantiate a decoder that will call back to us once the IP decoding is done - self.__decoder = dshell.IPDecoder() - self.out = netflowout.NetflowOutput() - self.chainable = True - - def decode(self, *args): - if len(args) is 3: - pktlen, pktdata, ts = args - else: - ts, pktdata = args - pktlen = len(pktdata) - # do normal decoder stack to track session - dshell.TCPDecoder.decode(self, pktlen, pktdata, ts) - # our hook to decode the ip/ip6 addrs, then dump the addrs and raw packet to our callback - self.__decoder.IPHandler = self.__callback # set private decoder to our callback - self.__decoder.decode(pktlen, pktdata, ts, raw=pktdata) - - def __callback(self, addr, pkt, ts, raw=None, **kw): - '''substitute IPhandler for forwarding packets to subdecoders''' - if addr in self.sessions or (addr[1], addr[0]) in self.sessions: # if we are not passing this session, drop the packet - if self.subDecoder: - # make it look like a capture - self.subDecoder.decode(len(raw), str(raw), ts) - else: - self.dump(raw, ts) - - def connectionInitHandler(self, conn): - '''see if we have a country match and if so, flag this session for forwarding or dumping''' - m = self.__countryTest(conn) - if m: - self.sessions[conn.addr] = m - - def __countryTest(self, conn): - # If no country code specified, pass all traffic through - if not self.code: - return True - # check criteria - if self.neither and conn.clientcountrycode != self.code and conn.servercountrycode != self.code: - return 'neither ' + self.code - if self.both and conn.clientcountrycode == self.code and conn.servercountrycode == self.code: - return 'both ' + self.code - if self.notboth and ((conn.clientcountrycode == self.code) ^ (conn.servercountrycode == self.code)): - return 'not both ' + self.code - if not self.both and conn.clientcountrycode == self.code: - return 'client ' + self.code - if not self.both and conn.servercountrycode == self.code: - return 'server ' + self.code - # no match - return None - - def connectionHandler(self, conn): - if conn.addr in self.sessions and self.alerts: - self.alert(self.sessions[conn.addr], **conn.info()) - - def connectionCloseHandler(self, conn): - if conn.addr in self.sessions: - del self.sessions[conn.addr] - -dObj = DshellDecoder() diff --git a/decoders/filter/snort.py b/decoders/filter/snort.py deleted file mode 100644 index 8a79340..0000000 --- a/decoders/filter/snort.py +++ /dev/null @@ -1,209 +0,0 @@ -import dshell - - -class DshellDecoder(dshell.IPDecoder): - - def __init__(self): - dshell.IPDecoder.__init__(self, - name='snort', - description='filter packets by snort rule', - longdescription="""Chainable decoder to filter TCP/UDP streams by snort rule -rule is parsed by dshell, a limited number of options are supported: - currently supported rule options: - content - nocase - depth - offset - within - distance - -Mandatory option: - ---snort_rule: snort rule to filter by - -or - --snort_conf: snort.conf formatted file to read for multiple rules - -Modifier options: - ---snort_all: Pass only if all rules pass ---snort_none: Pass only if no rules pass ---snort_alert: Alert if rule matches? - -Example: -decode -d snort+followstream traffic.pcap --snort_rule 'alert tcp any any -> any any (content:"....."; nocase; depth .... )' - -""", - filter='ip or ip6', - author='twp', - optiondict={'rule': {'type': 'string', 'help': 'snort rule to filter packets'}, - 'conf': {'type': 'string', 'help': 'snort.conf file to read'}, - 'alerts': {'action': 'store_true', 'help': 'alert if rule matched'}, - 'none': {'action': 'store_true', 'help': 'pass if NO rules matched'}, - 'all': {'action': 'store_true', 'help': 'all rules must match to pass'} - } - ) - self.chainable = True - - def preModule(self): - rules = [] - if self.conf: - fh = file(self.conf) - rules = [r for r in (r.strip() for r in fh.readlines()) if len(r)] - fh.close() - else: - if not self.rule or not len(self.rule): - self.warn("No rule specified (--%s_rule)" % self.name) - else: - rules = [self.rule] - self.rules = [] - for r in rules: - try: - self.rules.append((self.parseSnortRule(r))) - except Exception, e: - self.error('bad snort rule "%s": %s' % (r, e)) - if self._DEBUG: - self._exc(e) - if self.subDecoder: - # we filter individual packets so session-based subdecoders will - # need this set - self.subDecoder.ignore_handshake = True - dshell.IPDecoder.preModule(self) - - def rawHandler(self, pktlen, pkt, ts, **kwargs): - kwargs['raw'] = pkt # put the raw frame in the kwargs - # continue decoding - return dshell.IPDecoder.rawHandler(self, pktlen, pkt, ts, **kwargs) - - def IPHandler(self, addr, pkt, ts, **kwargs): - '''check packets using filterfn here''' - raw = str( - kwargs['raw']) # get the raw frame for forwarding if we match - p = dshell.Packet(self, addr, pkt=str(pkt), ts=ts, **kwargs) - a = [] - match = None - for r, msg in self.rules: - if r(p): # if this rule matched - match = True - if msg: - a.append(msg) # append rule message to alerts - if self.none or not self.all: - break # unless matching all, one match does it - else: # last rule did not match - match = False - if self.all: - break # stop once no match if all - - # all rules processed, match = state of last rule match - if (match is not None) and ((match and not self.none) or (self.none and not match)): - self.decodedbytes += len(str(pkt)) - self.count += 1 - if self.alerts: - self.alert(*a, **p.info()) - if self.subDecoder: - # decode or dump packet - self.subDecoder.decode(len(raw), raw, ts) - else: - self.dump(len(raw), raw, ts) - - def parseSnortRule(self, ruletext): - '''returns a lambda function that can be used to filter traffic and the alert message - this function will expect a Packet() object and return True or False''' - KEYWORDS = ( - 'msg', 'content') # rule start, signal when we process all seen keywords - msg = '' - f = [] - rule = ruletext.split(' ', 7) - (a, proto, sip, sp, arrow, dip, dp) = rule[:7] - if len(rule) > 7: - rule = rule[7] - else: - rule = None - if a != 'alert': - raise Exception('Must be alert rule') - f.append('p.proto == "' + proto.upper() + '"') - if sip != 'any': - f.append('p.sip == "' + sip + '"') - if dip != 'any': - f.append('p.dip == "' + dip + '"') - if sp != 'any': - f.append('p.sport == ' + sp) - if dp != 'any': - f.append('p.dport == ' + dp) - f = ['(' + (' and '.join(f)) + ')'] # create header condition - if rule: - # split between () and split on ; - rule = rule.strip('()').split(';') - last = None # no last match - while rule: - try: - k, v = rule.pop(0).strip().split(':', 1) - except: - continue - if k.lower() == 'content': # reset content match - content = v.strip().strip('"') - # hex bytes? - if content.startswith('|') and content.endswith('|'): - content = ''.join( - '\\x' + c for c in content.strip('|').split()) - nocase = depth = offset = distance = within = None - while rule: - r = rule[0].strip() - if ':' in r: - k, v = r.split(':', 1) - else: - k, v = r, None - k = k.lower() - if k in KEYWORDS: - break # next rule part - elif k == 'nocase': - nocase = True - elif k == 'depth': - depth = int(v) - elif k == 'offset': - offset = int(v) - elif k == 'distance': - distance = int(v) - elif k == 'within': - within = int(v) - rule.pop(0) # remove this keyword:valuea - # add coerce to lower if nocase? - if nocase: - nocase = '.lower()' - else: - nocase = '' - # start,end offsets of find(), maybe number or result of - # another find() - st, end = offset, depth - # if we have a last content match, use the distance/within kws - if last: - # within means this match has to be within X from - # previous+distance, so use previous as offset and within - # as depth - if within: - # set to last match and X from last match - st, end = last, last + '+' + str(within) - # distance means the next match must be AT LEAST X from the - # last - if distance: - # set start to last match+distance - st = last + '+' + str(distance) - # else use the offset/depth values as given - last = 'p.pkt' + nocase + \ - '.find(' + "'" + content + "'" + nocase + ',' + \ - str(st) + ',' + str(end) + ') != -1' - if k.lower() == 'msg': - msg = v.strip().strip('"') # get alert message - if last: - f.append('(' + last + ')') - f = ' and '.join(f) - self.debug('%s\t%s\t"%s"' % (ruletext, f, msg)) - return eval('lambda(p): ' + f), msg # return fn and msg - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/filter/track.py b/decoders/filter/track.py deleted file mode 100644 index 3d83d7b..0000000 --- a/decoders/filter/track.py +++ /dev/null @@ -1,135 +0,0 @@ -''' -@author: tparker -''' - -import dshell -import util - - -class DshellDecoder(dshell.TCPDecoder): - - '''activity tracker ''' - - def __init__(self, **kwargs): - ''' - Constructor - ''' - self.sources = [] - self.targets = [] - self.sessions = {} - self.alerts = False - self.file = None - dshell.TCPDecoder.__init__(self, - name='track', - description='tracked activity recorder', - longdescription='''captures all traffic to/from target while a specific connection to the target is up - specify target(s) ip and/or port as --track_target=ip:port,ip... - --track_source=ip,ip.. can be used to limit to specified sources - --track_alerts will turn on alerts for session start/end''', - filter="ip", - author='twp', - optiondict={'target': {'action': 'append'}, - 'source': {'action': 'append'}, - 'alerts': {'action': 'store_true'}}) - self.chainable = True - - '''instantiate an IPDecoder and replace the IPHandler - to decode the ip/ip6 addr and then pass the packet - to _IPHandler, which will write the packet if in addr is in session''' - self.__decoder = dshell.IPDecoder() - - def preModule(self): - '''parse the source and target lists''' - if self.target: - for tstr in self.target: - targets = util.strtok(tstr, as_list=True)[0] - for t in targets: - try: - parts = t.split(':') - if len(parts) == 2: - ip, port = parts # IP:port - else: - ip, port = t, None # IPv6 addr - except: - ip, port = t, None # IP - if ip == '': - ip = None # :port - self.targets.append((ip, port)) - if self.source: - for sstr in self.source: - sources = util.strtok(sstr, as_list=True)[0] - for ip in sources: - self.sources.append(ip) - dshell.TCPDecoder.preModule(self) - - def decode(self, *args): - if len(args) is 3: - pktlen, pktdata, ts = args # orig_len,packet,ts format (pylibpcap) - else: # ts,pktdata (pypcap) - ts, pktdata = args - pktlen = len(pktdata) - '''do normal decoder stack to track session ''' - dshell.TCPDecoder.decode(self, pktlen, pktdata, ts) - '''our hook to decode the ip/ip6 addrs, then dump the addrs and raw packet - to our session check routine''' - self.__decoder.IPHandler = self.__callback # set private decoder to our callback - self.__decoder.decode(pktlen, pktdata, ts, raw=pktdata) - - def __callback(self, addr, pkt, ts, raw=None, **kw): - '''check to see if this packet is to/from an IP in a session, - if so write it. the packet will be passed in the 'raw' kwarg''' - if addr[0][0] in self.sessions: - ip = addr[0][0] # source ip - elif addr[1][0] in self.sessions: - ip = addr[1][0] # dest ip - else: - return # not tracked - for s in self.sessions[ip].values(): - s.sessionpackets += 1 - s.sessionbytes += len(raw) # actual captured data len - # dump the packet or sub-decode it - if self.subDecoder: - # make it look like a capture - self.subDecoder.decode(len(raw), str(raw), ts) - else: - self.dump(raw, ts) - - def connectionInitHandler(self, conn): - '''see if dest ip and/or port is in target list and (if a source list) - source ip is in source list - if so, put the connection in the tracked-session list by dest ip - if a new connection to the target comes in from an allowed source, - the existing connection will still be tracked''' - ((sip, sport), (dip, dport)) = conn.addr - sport, dport = str(sport), str(dport) - if ((dip, dport) in self.targets) or ((dip, None) in self.targets) or ((None, dport) in self.targets): - if not self.sources or (sip in self.sources): - s = self.sessions.setdefault(dip, {}) - s[conn.addr] = conn - if self.alerts: - self.alert('session started', **conn.info()) - conn.info(sessionpackets=0, sessionbytes=0) - - def connectionHandler(self, conn): - '''if a connection to a tracked-session host, alert and write if no subdecoder''' - if self.alerts: - if conn.serverip in self.sessions: - self.alert('inbound', **conn.info()) - if conn.clientip in self.sessions: - self.alert('outbound', **conn.info()) - if conn.serverip in self.sessions or conn.clientip in self.sessions: - if not self.subDecoder: - self.write(conn) - - def connectionCloseHandler(self, conn): - '''close the tracked session if the initiating connection is closing - make sure the conn in the session list matches, - as we may have had more incoming connections to the same ip during the session''' - if conn.serverip in self.sessions and conn.addr in self.sessions[conn.serverip]: - if self.alerts: - self.alert('session ended', **conn.info()) - del self.sessions[conn.serverip][conn.addr] - if not self.sessions[conn.serverip]: - del self.sessions[conn.serverip] - -dObj = DshellDecoder() diff --git a/decoders/flows/large-flows.py b/decoders/flows/large-flows.py deleted file mode 100644 index a954aaa..0000000 --- a/decoders/flows/large-flows.py +++ /dev/null @@ -1,36 +0,0 @@ -import dshell -import netflowout - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='large-flows', - description='display netflows that have at least 1MB transferred', - filter='tcp', - author='bg', - optiondict={'size': { - 'type': 'float', 'default': 1, 'help': 'number of megabytes transferred'}} - ) - self.out = netflowout.NetflowOutput() - self.min = 1048576 # 1MB - - def preModule(self): - if self.size <= 0: - self.warn( - "Cannot have a size that's less than or equal to zero. (size: %s)" % (self.size)) - self.size = 1 - self.min = 1048576 * self.size - self.debug("Input: %s, Final size: %s bytes" % (self.size, self.min)) - - def connectionHandler(self, conn): - if (conn.clientbytes + conn.serverbytes) >= self.min: - self.alert(**conn.info()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/flows/long-flows.py b/decoders/flows/long-flows.py deleted file mode 100644 index 6fefcb2..0000000 --- a/decoders/flows/long-flows.py +++ /dev/null @@ -1,29 +0,0 @@ -import dshell -import netflowout - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - self.len = 5 - dshell.TCPDecoder.__init__(self, - name='long-flows', - description='display netflows that have a duration of at least 5mins', - filter='(tcp or udp)', - author='bg', - optiondict={ - 'len': {'type': 'int', 'default': 5, 'help': 'set minimum connection time to alert on, in minutes [default: 5 mins]'}, - } - ) - self.out = netflowout.NetflowOutput() - - def connectionHandler(self, conn): - if (conn.endtime - conn.starttime) >= (60 * self.len): - self.alert(**conn.info()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/flows/netflow.py b/decoders/flows/netflow.py deleted file mode 100644 index a563ae2..0000000 --- a/decoders/flows/netflow.py +++ /dev/null @@ -1,37 +0,0 @@ -import dshell -import netflowout - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='netflow', - description='generate netflow information from pcap', - longdescription='generate netflow information from pcap', - filter='(tcp or udp)', - author='bg', - # grouping for output module - optiondict={'group': dict()} - ) - self.out = netflowout.NetflowOutput() - - def preModule(self): - # pass grouping to output module - if self.group: - self.out.group = self.group.split(',') - dshell.TCPDecoder.preModule(self) - - def connectionHandler(self, conn): - self.alert(**conn.info()) - - def postModule(self): - self.out.close() # write flow groups if grouping - dshell.TCPDecoder.postModule(self) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/flows/reverse-flow.py b/decoders/flows/reverse-flow.py deleted file mode 100644 index 84c2e45..0000000 --- a/decoders/flows/reverse-flow.py +++ /dev/null @@ -1,68 +0,0 @@ -import dshell - -class DshellDecoder(dshell.TCPDecoder): - - '''Reverse-Flow Decoder''' - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='reverse-flow', - description='Generate an alert if the client transmits more data than the server', - longdescription=""" - - Generate an alert when a client transmits more data than the server. - - Additionally, the user can specify a threshold. This means that an alert - will be generated if the client transmits more than three times as much data - as the server. - - The default threshold value is 3.0, meaning that any client transmits - more than three times as much data as the server will generate an alert. - - Examples: - 1) decode -d reverse-flow - Generates an alert for client transmissions that are three times - greater than the server transmission. - - 2) decode -d reverse-flow --reverse-flow_threshold 61 - Generates an alert for all client transmissions that are 61 times - greater than the server transmission - - 3) decode -d reverse-flow --reverse-flow_threshold 61 --reverse-flow_zero - Generates an alert for all client transmissions that are 61 times greater - than the server transmission. - - - - """, - filter="tcp or udp", - author='me', - optiondict={ - 'threshold':{'type':'float', 'default':3.0, - 'help':'Alerts if client transmits more than threshold times the data of the server'}, - 'minimum':{'type':'int', 'default':0, 'help':'alert on client transmissions larger than min bytes [default: 0]'}, - 'zero':{'action':'store_true', 'default':False, 'help':'alert if the server transmits zero bytes [default: false]'}, - } - ) - - def preModule(self): - if self.threshold < 0: - self.warn( - "Cannot have a negative threshold. (threshold: {0})".format(self.threshold)) - self.threshold = 3.0 - elif not self.threshold: - self.warn( - "Displaying all client-server transmissions (threshold: {0})".format(self.threshold)) - - def connectionHandler(self, conn): - if conn.clientbytes < self.minimum: - return - - if self.zero or (conn.serverbytes and float(conn.clientbytes)/conn.serverbytes > self.threshold): - self.alert('client sent {:>6.2f} more than the server'.format(conn.clientbytes/float(conn.serverbytes)), **conn.info()) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/ftp/ftp.py b/decoders/ftp/ftp.py deleted file mode 100644 index dbfb055..0000000 --- a/decoders/ftp/ftp.py +++ /dev/null @@ -1,296 +0,0 @@ -############################################## -# File Transfer Protocol (FTP) -# -# Goes through TCP connections and tries to find FTP control channels and -# associated data channels. Optionally, it will write out any file data it -# sees into a separate directory. -# -# More specifically, it sets up an initial BPF that looks for control channels. -# As it finds them, it updates the BPF to include the data connection along -# ephemeral ports. -# -# If a data connection is seen, it prints a message indicating the user, pass, -# and file requested. If the dump flag is set, it also dumps the file into the -# __OUTDIR directory. -############################################## - -import dshell -import os -import re - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='ftp', - description='ftp', - filter="tcp", - author='amm', - optiondict={ - 'port':{'type':'string', - 'default':'21', - 'help':'Port (or ports) to watch for control connections (Default: 21)'}, - 'dump':{'action':'store_true','help':'Dump files (Default: Off)'} - } - ) - - # Constants for channel type - __CTRLCONN = 0 - __DATACONN = 1 - __OUTDIR = 'ftpout' - - # Dynamically change the BPF filter - # to allow processing of data transfer channels - def __updatebpf(self): - dynfilters = [ '(host %s and host %s)' % self.conns[x]['tempippair'] for x in self.conns.keys() if 'tempippair' in self.conns[x] and self.conns[x]['tempippair'] != None ] - dynfilters.extend( [ '(host %s and port %d)' % (a,p) for a,p in self.datachan.keys() ] ) - self.filter = 'tcp and (%s%s)' % ( - ' or '.join(['port %d'%p for p in self.ctrlports]), - ' or '+' or '.join(dynfilters) if len(dynfilters) else '' - ) - self.debug("Setting BPF filter to: %s" % self.filter) - if 'capture' in dir(self): - self.capture.setfilter(self.filter,1) - - def preModule(self): - # Convert port specification from string to list of integers - self.ctrlports = [int(p) for p in self.port.split(',')] - self.datachan = {} # Dictionary of control channels indexed by data channel (host, port) tuples - self.conns = {} # Dictionary of information about connections - self.__updatebpf() # Set initial bpf - - # Attempt to create output directory - if self.dump: - self.__OUTDIR = self.__mkoutdir(self.__OUTDIR) - self.warn("Using output directory: %s" % self.__OUTDIR) - - def connectionInitHandler(self, conn): - # - # Setup conn info for New Data Channel - # - if conn.serverport in self.ctrlports: - self.conns[conn.addr] = { 'mode': self.__CTRLCONN, 'user':'', 'pass':'', 'path':[], 'lastcommand':'', 'filedata':None, 'file':('', '', '')} - elif self.dump and (conn.clientip, conn.clientport) in self.datachan: - self.conns[conn.addr] = { 'mode': self.__DATACONN, 'ctrlchan': self.datachan[(conn.clientip, conn.clientport)] } - elif self.dump and (conn.serverip, conn.serverport) in self.datachan: - self.conns[conn.addr] = { 'mode': self.__DATACONN, 'ctrlchan': self.datachan[(conn.serverip, conn.serverport)] } - elif self.dump: - # No match. Track as a DATACONN with unknown CTRLCHAN as it may be - # a passive mode transfer that we don't have port info on yet. - self.conns[conn.addr] = { 'mode': self.__DATACONN, 'ctrlchan': None } - - def connectionCloseHandler(self, conn): - info = self.conns[conn.addr] - ######################################################### - # Upon close of data channels, store file content in - # 'filedata' associated with the ctrlchan. - # ctrlchan will then write it out to disk after it knows - # for sure the file name - # - if self.dump and info['mode'] == self.__DATACONN: - # Associated Control Channel - if info['ctrlchan'] == None: - if (conn.clientip, conn.clientport) in self.datachan: - info['ctrlchan'] = self.datachan[(conn.clientip, conn.clientport)] - if (conn.serverip, conn.serverport) in self.datachan: - info['ctrlchan'] = self.datachan[(conn.serverip, conn.serverport)] - ctrlchan = self.conns[info['ctrlchan']] - # Add data to control channel - ctrlchan['filedata'] = conn.data() - # Update BPF and DataChan Knowledge - if (conn.serverip, conn.serverport) == ctrlchan['datachan'] or (conn.clientip, conn.clientport) == ctrlchan['datachan']: - del self.datachan[ctrlchan['datachan']] - ctrlchan['datachan'] = None - self.__updatebpf() - # Remove Data Channel from tracker - del self.conns[conn.addr] - - elif info['mode'] == self.__CTRLCONN: - if 'file' not in info or info['file'] == None: - del self.conns[conn.addr] - - def postModule(self): - for x in self.conns: - info = self.conns[x] - if self.dump and 'filedata' in info and info['filedata']: - origname = info['file'][0] + '_' + os.path.join(*info['file'][1:3]) - outname = self.__localfilename(self.__OUTDIR, origname) - fh = open(outname, 'w') - fh.write(info['filedata']) - fh.close() - numbytes = len(info['filedata']) - info['filedata'] = None - info['outfile'] = outname - #info.update(conn.info()) - msg = 'User: %s, Pass: %s, %s File: %s (Incomplete: %d bytes written to %s)' % (info['user'], info['pass'], info['file'][0], os.path.join(*info['file'][1:3]), numbytes, os.path.basename(outname)) - self.alert(msg, **info) - - - - def blobHandler(self, conn, blob): - - info = self.conns[conn.addr] - data = blob.data() - - # - # Data Channel - # - if info['mode'] == self.__DATACONN: - return - - - # - # Control Channel - # - - # Client Commands - if blob.direction == 'cs': - - try: - if ' ' not in data: (command, param) = (data.rstrip(), '') - else: (command, param) = data.rstrip().split(' ', 1) - command = command.upper() - info['lastcommand'] = command - info['request_time'] = blob.starttime - except: - return - - if command == 'USER': - info['user'] = param - elif command == 'PASS': - info['pass'] = param - elif command == 'CWD': - info['path'].append(param) - elif command == 'PASV' or command == 'EPSV': - if self.dump: - # Temporarily store the pair of IP addresses - # to open up the bpf filter until blobHandler processes - # the response with the full IP/Port information - # (Note: Due to the way blob processing works, we don't get this information - # until after the data channel is established) - info['tempippair'] = tuple(sorted((conn.clientip, conn.serverip))) - self.__updatebpf() - # - # For file transfers (including LIST), store tuple (Direction, Path, Filename) in info['file'] - # - elif command == 'LIST': - if param == '': - info['file'] = ( 'RETR', os.path.normpath(os.path.join(*info['path'])) if len(info['path']) else '', 'LIST' ) - else: - info['file'] = ( 'RETR', os.path.normpath(os.path.join(os.path.join(*info['path']), param)) if len(info['path']) else '', 'LIST' ) - elif command == 'RETR': - info['file'] = ( 'RETR', os.path.normpath(os.path.join(*info['path'])) if len(info['path']) else '', param ) - elif command == 'STOR': - info['file'] = ( 'STOR', os.path.normpath(os.path.join(*info['path'])) if len(info['path']) else '', param ) - - # Responses - else: - info['response_time'] = blob.endtime - # - # Rollback directory change unless 2xx response - # - if info['lastcommand'] == 'CWD' and data[0] != '2': info['path'].pop() - # - # Write out files upon resonse to transfer commands - # - if info['lastcommand'] in ('LIST', 'RETR', 'STOR'): - if self.dump and info['filedata']: - origname = info['file'][0] + '_' + os.path.join(*info['file'][1:3]) - outname = self.__localfilename(self.__OUTDIR, origname) - fh = open(outname, 'w') - fh.write(info['filedata']) - fh.close() - numbytes = len(info['filedata']) - info['filedata'] = None - info['outfile'] = outname - info.update(conn.info()) - msg = 'User: %s, Pass: %s, %s File: %s (%d bytes written to %s)' % (info['user'], info['pass'], info['file'][0], os.path.join(*info['file'][1:3]), numbytes, os.path.basename(outname)) - else: - info.update(conn.info()) - msg = 'User: %s, Pass: %s, %s File: %s' % (info['user'], info['pass'], info['file'][0], os.path.join(*info['file'][1:3])) - if data[0] not in ('1','2'): msg += ' (%s)' % data.rstrip() - info['ts'] = info['response_time'] - (info['Direction'], info['Path'], info['Filename']) = info['file'] - del info['file'] - self.alert(msg, **info) - info['file'] = None - # - # Handle EPSV mode port setting - # - if info['lastcommand'] == 'EPSV' and data[0] == '2': - ret = re.findall('\(\|\|\|\d+\|\)', data) - if ret: - tport = int(ret[0].split('|')[3]) - info['datachan'] = (conn.serverip, tport) - if self.dump: - self.datachan[(conn.serverip, tport)] = conn.addr - info['tempippair'] = None - self.__updatebpf() - - # - # Look for ip/port information, assuming PSV response - # - ret = re.findall('\d+,\d+,\d+,\d+,\d+\,\d+', data) - if len(ret)==1: - tip, tport = self.calculateTransfer(ret[0]) # transfer ip, transfer port - info['datachan'] = (tip, tport) # Update this control channel's knowledge of currently working data channel - if self.dump: - self.datachan[(tip,tport)] = conn.addr # Update decoder's global datachan knowledge - info['tempippair'] = None - self.__updatebpf() - - - def calculateTransfer(self,val): - # calculate passive FTP data port - tmp = val.split(',') - ip = '.'.join(tmp[:4]) - port = int(tmp[4])*256 + int(tmp[5]) - return ip, port - - # - # Create output directory. Returns full path to output directory. - # - def __mkoutdir(self, outdir): - path = os.path.realpath(outdir) - if os.path.exists(path): return path - try: - os.mkdir(path) - return path - except OSError: - pass # most likely a permission denied issue, continue and try system temp directory - except: - raise # other errors, abort - if os.path.exists('/tmp'): path = os.path.realpath(os.path.join('/tmp', outdir)) - if os.path.exists(path): return path - try: - os.mkdir(path) - return path - except: - raise - - # - # Generate a local (extracted) filename based on the original - # - def __localfilename(self, path, origname): - tmp = origname.replace("\\", "_") - tmp = tmp.replace("/", "_") - tmp = tmp.replace(":", "_") - localname = '' - for c in tmp: - if ord(c) > 32 and ord(c) < 127: - localname += c - else: - localname += "%%%02X" % ord(c) - localname = path + '/' + localname - postfix = '' - i = 0 - while os.path.exists(localname+postfix): - i += 1 - postfix = "_%02d" % i - return localname+postfix - -if __name__=='__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/flash-detect.py b/decoders/http/flash-detect.py deleted file mode 100644 index 59eb255..0000000 --- a/decoders/http/flash-detect.py +++ /dev/null @@ -1,236 +0,0 @@ -import util -import hashlib -import os -from httpdecoder import HTTPDecoder - - -class DshellDecoder(HTTPDecoder): - - # Constant for dump directory - __OUTDIR = 'flashout' - - # Constants for MD5sum option - NOMD5 = 0 - MD5 = 1 - MD5_EXPLICIT_FILENAME = 2 - - def __init__(self): - HTTPDecoder.__init__(self, - name='flash-detect', - description='Detects successful Flash file download.', - filter='tcp and (port 80 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 8000, 8080) or dp in (80, 8000, 8080), - optiondict={ - 'dump': {'action': 'store_true', 'help': '''\ -Dump the flash file to a file based off its name, md5sum (if specified), or -its URI. The file is dumped to the local directory "flashout". The file -extension is ".flash" to prevent accidental execution.''' - }, - 'md5sum': {'type': 'int', 'default': 0, 'help': '''\ -Calculate and print the md5sum of the file. There are three options: - 0: (default) No md5sum calculations or labeling - - 1: Calculate md5sum; Print out md5sum in alert; Name all dumped files by -their md5sum (must be used with 'dump' option) - - 2: Calculate md5sum; Print out md5sum in alert; If found, a file's explicitly -listed save name (found in 'content-disposition' HTTP header) will be used -for file dump name instead of md5sum. - -Any other numbers will be ignored and the default action will be used.''' - } - }, - longdescription='''\ -flash-detect identifies HTTP requests where the server response contains a Flash -file. Many exploit kits utilize Flash to deliver exploits to potentially vulnerable -browsers. If a flash file is successfully downloaded, an alert will occur stating -the full URL of the downloaded file, its content-type, and (optionally) its md5sum. - -Usage Examples: -=============== - Search all pcap files for Flash file downloads, and upon detection, calculate - and print alerts containing the md5sum to screen: - - decode -d flash-detect --flash-detect_md5sum=1 *.pcap - - If you wanted to save every detected Flash file to a local directory - "./flashout/" with its md5sum as the file name: - - decode -d flash-detect --flash-detect_md5sum=1 --flash-detect_dump *.pcap - The output directory can be changed by modifying the `__OUTDIR` variable. - - An example of a real pcap file, taken from - http://malware-traffic-analysis.net/2014/12/12/index.html: - decode -d flash-detect --flash-detect_md5sum=1 2014-12-12-Nuclear-EK-traffic.pcap - - The following text should be displayed in the output, and the md5sum - can be checked on a site like virustotal: -** yquesrerman.ga/AwoVG1ADAw4OUhlVDlRTBQoHRUJTXVYOUVYaAwtGXFRVVFxXVwBOVRtA (application/octet-stream) md5sum: 9b3ad66a2a61e8760602d98b537b7734 ** - -Implementation Logic -==================== - -1. Check if the HTTP response status is 200 OK - -2. Test the content-type of the HTTP response for the follwing strings: - 'application/x-shockwave-flash' - 'application/octet-stream' - 'application/vnd.adobe.flash-movie' - -3. Test filedownload following known Flash magic byte substrings: - 'CWS' - 'ZWS' - 'FWS' - -Note: Encoded or obfuscated flash files will *not* be detected. - -Chainable - -flash-detect is chainable. If a connection contains an HTTP response with a -successful Flash file download, then the entire connection (in the case of a -connectionHandler), and the request, response, requesttime, and responsetime -(in the case of an HTTPHandler) is/are passed to the subDecoders for additional -processing. Undetected or non-Flash files are dropped. -''', - author='ekilmer', - ) - self.chainable = True - - def preModule(self): - # Attempt to create output directory - if self.dump: - self.__OUTDIR = self.__mkoutdir(self.__OUTDIR) - self.log("Using output directory: {0}".format(self.__OUTDIR)) - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - if response and response.status != '200': - return - - content_type = util.getHeader(response, 'content-type') - if content_type not in ('application/x-shockwave-flash', - 'application/octet-stream', - 'application/vnd.adobe.flash-movie'): - return - - # Check for known flash file header characters - if not response.body.startswith(('CWS', 'ZWS', 'FWS')): - return - - host = util.getHeader(request, 'host') - # Grab file info as dict with keys: 'file_name', 'md5sum', 'uri' - file_info = self.get_file_info(request, response) - if self.md5sum == self.MD5 or self.md5sum == self.MD5_EXPLICIT_FILENAME: - # Print MD5 sum in the alert - self.alert('{0}{1} ({2}) md5sum: {3}'.format(host, request.uri, content_type, - file_info['md5sum']), **conn.info()) - else: - self.alert('{0}{1} ({2})'.format(host, request.uri, content_type), **conn.info()) - - # Dump the file if chosen - if self.dump: - # Name output files based on options - if self.md5sum == self.MD5: - origname = file_info['md5sum'] - # Check for explicitly listed filename - elif file_info['file_name']: - origname = file_info['file_name'] - # If explicit name not found, but still want MD5, give it MD5 - elif self.md5sum == self.MD5_EXPLICIT_FILENAME: - origname = file_info['md5sum'] - # Else name the file by its URI (which can be long) - else: - origname = file_info['uri'] - outname = self.__localfilename(self.__OUTDIR, origname) - with open('{0}.flash'.format(outname), 'wb') as outfile: - outfile.write(response.body) - - # Pass to subdecoder if necessary - if 'HTTPHandler' in dir(self.subDecoder): - self.subDecoder.HTTPHandler(conn, request, response, requesttime, responsetime) - elif 'connectionHandler' in dir(self.subDecoder): - self.subDecoder.connectionHandler(conn) - - def get_file_info(self, request, response): - """Checks for an explicitly listed file name. Returns a dictionary containing the - explicitly listed filename (if present), the URI, and an md5sum (if requested) - """ - file_info = {'file_name': '', 'uri': '', 'md5sum': ''} - - content = util.getHeader(response, 'content-disposition') - if content and 'filename' in content: - # RFC 1806: content contains a string with parameters separated by semi-colons - text = content.split(';') - for parm in text: - if parm.strip().startswith('filename='): - file_info['file_name'] = parm.split('filename=', 1)[1] - - # CAVEAT: When the URI is very long and it is used as a filename in a dump, - # then the file name may become unwieldy - file_info['uri'] = request.uri - - if self.md5sum == self.MD5 or self.md5sum == self.MD5_EXPLICIT_FILENAME: - file_info['md5sum'] = self.__body_md5(response) - - return file_info - - def __body_md5(self, response): - """Calculate the MD5sum(hex) of the body portion of the response.""" - if len(response.body) > 0: - return hashlib.md5(response.body.rstrip('\0')).hexdigest() - else: - self.warn("Nothing to hash") - return '' - - def __mkoutdir(self, outdir): - """Creates output directory. Returns full path to output directory.""" - path = os.path.realpath(outdir) - if os.path.exists(path): - return path - try: - os.mkdir(path) - return path - except OSError: - # most likely a permission denied issue, continue and try system temp directory - pass - except: - self.warn('Unable to create a directory for file dump') - # other errors, abort - raise - - # Trying temp directory - if os.path.exists('/tmp'): - path = os.path.realpath(os.path.join('/tmp', outdir)) - if os.path.exists(path): - return path - try: - os.mkdir(path) - return path - except: - self.warn('Unable to create a directory for file dump') - raise - - def __localfilename(self, path, origname): - """Generate a local (extracted) filename based on the original""" - tmp = origname.replace('\\', '_') - tmp = tmp.replace('/', '_') - tmp = tmp.replace(':', '_') - localname = '' - for c in tmp: - if ord(c) > 32 and ord(c) < 127: - localname += c - else: - localname += '%{0:02X}'.format(ord(c)) - localname = '{0}/{1}'.format(path, localname) - postfix = '' - i = 0 - while os.path.exists(localname+postfix): - i += 1 - postfix = '_{0:02d}'.format(i) - return localname+postfix - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/httpdump.py b/decoders/http/httpdump.py deleted file mode 100644 index 264cb42..0000000 --- a/decoders/http/httpdump.py +++ /dev/null @@ -1,153 +0,0 @@ -import dshell -import util -import hashlib -import urllib -import re -import colorout - -from httpdecoder import HTTPDecoder - - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='httpdump', - description='Dump useful information about HTTP sessions', - filter='tcp and (port 80 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 8000, 8080) or dp in (80, 8000, 8080), - author='amm', - optiondict={ - 'maxurilen': {'type': 'int', 'default': 30, 'help': 'Truncate URLs longer than max len. Set to 0 for no truncating. (default: 30)'}, - 'maxpost': {'type': 'int', 'default': 1000, 'help': 'Truncate POST body longer than max chars. Set to 0 for no truncating. (default: 1000)'}, - 'showcontent': {'action': 'store_true', 'help': 'Display response BODY.'}, - 'showhtml': {'action': 'store_true', 'help': 'Display response BODY only if HTML.'}, - 'urlfilter': {'type': 'string', 'default': None, 'help': 'Filter to URLs matching this regex'}, - }, - ) - self.out = colorout.ColorOutput() - # Disable auto-gunzip as we want to indicate content that was - # compressed in the output - self.gunzip = False - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - host = '' - loc = '' - uri = '' - lastmodified = '' - - #request_time, request, response = self.httpDict[conn.addr] - - # extract method,uri,host from response - host = util.getHeader(request, 'host') - if host == '': - host = conn.serverip - - try: - status = response.status - except: - status = '' - try: - reason = response.reason - except: - reason = '' - - if self.urlfilter: - if not re.search(self.urlfilter, host + request.uri): - return - - if '?' in request.uri: - [uri_location, uri_data] = request.uri.split('?', 1) - else: - uri_location = request.uri - uri_data = '' - - if self.maxurilen > 0 and len(uri_location) > self.maxurilen: - uri_location = uri_location[:self.maxurilen] + '[truncated]' - else: - uri_location = uri_location - - if response == None: - response_message = "%s (%s) %s%s" % ( - request.method, 'NO RESPONSE', host, uri_location) - else: - response_message = "%s (%s) %s%s (%s)" % ( - request.method, response.status, host, uri_location, util.getHeader(response, 'content-type')) - urlParams = util.URLDataToParameterDict(uri_data) - postParams = util.URLDataToParameterDict(request.body) - # If URLData parser only returns a single element with null value, it's probably an eroneous evaluation. Most likely base64 encoded payload ending in an '=' character. - if len(postParams)==1 and postParams[postParams.keys()[0]] == '\x00': - postParams = None - - clientCookies = self._parseCookies(util.getHeader(request, 'cookie')) - serverCookies = self._parseCookies( - util.getHeader(response, 'set-cookie')) - - self.alert(response_message, - urlParams=urlParams, postParams=postParams, clientCookies=clientCookies, serverCookies=serverCookies, - **conn.info() - ) - - referer = util.getHeader(request, 'referer') - if len(referer): - self.out.write(' Referer: %s\n' % referer) - - if clientCookies: - self.out.write(' Client Transmitted Cookies:\n', direction='cs') - for key in clientCookies: - self.out.write(' %s -> %s\n' % (util.printableUnicode(key), - util.printableUnicode(clientCookies[key])), direction='cs') - if serverCookies: - self.out.write(' Server Set Cookies:\n', direction='sc') - for key in serverCookies: - self.out.write(' %s -> %s\n' % (util.printableUnicode(key), - util.printableUnicode(serverCookies[key])), direction='sc') - - if urlParams: - self.out.write(' URLParameters:\n', direction='cs') - for key in urlParams: - self.out.write(' %s -> %s\n' % (util.printableUnicode(key), - util.printableUnicode(urlParams[key])), direction='cs') - if postParams: - self.out.write(' POSTParameters:\n', direction='cs') - for key in postParams: - self.out.write(' %s -> %s\n' % (util.printableUnicode(key), - util.printableUnicode(postParams[key])), direction='cs') - elif len(request.body): - self.out.write(' POST Body:\n', direction='cs') - if len(request.body) > self.maxpost and self.maxpost > 0: - self.out.write('%s[truncated]\n' % util.printableUnicode( - request.body[:self.maxpost]), direction='cs') - else: - self.out.write( - util.printableUnicode(request.body) + u"\n", direction='cs') - - if self.showcontent or self.showhtml: - - if self.showhtml and 'html' not in util.getHeader(response, 'content-type'): - return - - if 'gzip' in util.getHeader(response, 'content-encoding'): - content = self.decompressGzipContent(response.body) - if content == None: - content = '(gunzip failed)\n' + response.body - else: - content = '(gzip encoded)\n' + content - else: - content = response.body - - self.out.write("Body Content:\n", direction='sc') - self.out.write( - util.printableUnicode(content) + u"\n", direction='sc') - - def _parseCookies(self, data): - p, kwp = util.strtok(data, sep='; ') - return dict((urllib.unquote(k), urllib.unquote(kwp[k]))for k in kwp.keys()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/joomla-cve-2015-8562.py b/decoders/http/joomla-cve-2015-8562.py deleted file mode 100644 index 691d8d1..0000000 --- a/decoders/http/joomla-cve-2015-8562.py +++ /dev/null @@ -1,81 +0,0 @@ -import dshell -import util -from httpdecoder import HTTPDecoder - -import re - - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='joomla-cve-2015-8562', - description='detect and dissect malformed HTTP headers targeting Joomla', - filter='tcp and (port 80 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 8000, 8080) or dp in (80, 8000, 8080), - author='bg', - optiondict={ - 'raw_payload': {'action': 'store_true', 'help':'return the raw payload (do not attempt to decode chr encoding)'} - }, - longdescription=''' -Usage Examples: ---------------- - - decode -d joomla-cve-2015-8562 *.pcap -joomla-cve-2015-8562 2015-12-15 20:17:18 192.168.1.119:43865 <- 192.168.1.139:80 ** x-forwarded-for -> system('touch /tmp/2'); ** - - The module assumes the cmd payload is encoded using chr. To turn this off run: - - decode -d joomla-cve-2015-8562 --joomla-cve-2015-8562_no_eval *.pcap -oomla-cve-2015-8562 2015-12-15 20:17:18 192.168.1.119:43865 <- 192.168.1.139:80 ** x-forwarded-for -> "eval(chr(115).chr(121).chr(115).chr(116).chr(101).chr(109).chr(40).chr(39).chr(116).chr(111).chr(117).chr(99).chr(104).chr(32).chr(47).chr(116).chr(109).chr(112).chr(47).chr(50).chr(39).chr(41).chr(59)); ** -''' - ) - - self.ioc = 'JFactory::getConfig();exit' - - def attempt_decode(self, cmd): - ptext = '' - for c in re.findall('\d+', cmd): - ptext += chr(int(c)) - return ptext - - - def parse_cmd(self, data): - start = data.find('"feed_url";')+11 - end = data.find(self.ioc) - chunk = data[start:end] - - try: - cmd = chunk.split(':')[-1] - if self.raw_payload: - return cmd - - plaintext_cmd = self.attempt_decode(cmd) - return plaintext_cmd - except: - return None - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - if not request: - return - - if self.ioc not in str(request): - # indicator of (potential) compromise is not here - return - - # there is an attempt to exploit Joomla! - - # The Joomla exploit could be sent any HTTP header field - for hdr in request.headers: - if self.ioc in request.headers[hdr]: - cmd = self.parse_cmd(request.headers[hdr]) - if cmd: - self.alert('{} -> {}'.format(hdr, cmd), **conn.info()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/ms15-034.py b/decoders/http/ms15-034.py deleted file mode 100644 index f5bfed1..0000000 --- a/decoders/http/ms15-034.py +++ /dev/null @@ -1,74 +0,0 @@ -import dshell -import util -from httpdecoder import HTTPDecoder - -class DshellDecoder(HTTPDecoder): - ''' - 15 April 2015 - - Proof-of-concept code to detect attempts to enumerate MS15-034 vulnerable - IIS servers and/or cause a denial of service. Each event will generate an - alert that prints out the HTTP Request method and the range value contained - with the HTTP stream. - - Usage: - decode -d ms15-034 -q *.pcap - decode -d ms15-034 -i -q - - References: - https://technet.microsoft.com/library/security/ms15-034 - https://ma.ttias.be/remote-code-execution-via-http-request-in-iis-on-windows/ - ''' - def __init__(self): - HTTPDecoder.__init__(self, - name='ms15-034', - description='detect attempts to enumerate MS15-034 vulnerable IIS servers', - longdescription=''' -Proof-of-concept code to detect attempts to enumerate MS15-034 vulnerable -IIS servers and/or cause a denial of service. Each event will generate an -alert that prints out the HTTP Request method and the range value contained -with the HTTP stream. - -Usage: -decode -d ms15-034 -q *.pcap -decode -d ms15-034 -i -q -''', - filter='tcp and (port 80 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 8000, 8080) or dp in (80, 8000, 8080), - author='bg', - ) - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - if response == None: # Denial of Service (no server response) - try: - rangestr = util.getHeader(request,'range') - # check range value to reduce false positive rate - if not rangestr.endswith('18446744073709551615'): return - except: return - self.alert('MS15-034 DoS [Request Method: "%s" URI: "%s" Range: "%s"]' % \ - (request.method, request.uri, rangestr), conn.info()) - - else: # probing for vulnerable server - try: - rangestr = util.getHeader(request,'range') - # check range value to reduce false positive rate - if not rangestr.endswith('18446744073709551615'): return - except: return - - # indication of vulnerable server - if rangestr and (response.status == '416' or \ - response.reason == 'Requested Range Not Satisfiable'): - - self.alert('MS15-034 Vulnerable Server [Request Method: "%s" Range: "%s"]' % - (request.method,rangestr), conn.info()) - - if request.method != 'GET': # this could be interesting - pass # waiting on more details - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/peht.py b/decoders/http/peht.py deleted file mode 100644 index 9ce0e3d..0000000 --- a/decoders/http/peht.py +++ /dev/null @@ -1,264 +0,0 @@ -# -# Author: MM - https://github.com/1modm -# -# Most of the Penetration/Exploit/Hijacking Tools use the HTTP methods to try to inject -# or execute code into the attacked server, also this tools usually have a well known -# "hardcoded" User-Agent, URI or request content. -# -# So if the original scanner is not modified can be detected. This is a PoC in order to generate -# simple rules to detect and identified some of the most commons Penetration/Exploit/Hijacking Tools. -# -# Some of the most commons tools source and information: -# -# Nmap -# User-Agent header by default it is "Mozilla/5.0 (compatible; Nmap Scripting Engine; https://nmap.org/book/nse.html)". -# https://nmap.org/nsedoc/lib/http.html -# -# OpenVAS -# http://www.openvas.org/src-doc/openvas-libraries/nasl__http_8c_source.html -# User-Agent header by default: #define OPENVAS_USER_AGENT "Mozilla/5.0 [en] (X11, U; OpenVAS)" -# -# MASSCAN -# https://github.com/robertdavidgraham/masscan -# -# Morpheus -# https://github.com/r00t-3xp10it/morpheus -# https://latesthackingnews.com/2016/12/19/morpheus-automated-ettercap-tcpip-hijacking-tool/ -# -# DataCha0s Web Scanner -# http://eromang.zataz.com/2011/05/23/suc026-datacha0s-web-scannerrobot/ -# https://blogs.harvard.edu/zeroday/2006/06/12/data-cha0s-connect-back-backdoor/ -# -# HNAP (Home Network Administration Protocol) -# https://nmap.org/nsedoc/scripts/hnap-info.html -# -# ZmEu Scanner -# https://en.wikipedia.org/wiki/ZmEu_(vulnerability_scanner) -# http://linux.m2osw.com/zmeu-attack -# https://code.google.com/archive/p/caffsec-malware-analysis/wikis/ZmEu.wiki -# https://ensourced.wordpress.com/2011/02/25/zmeu-attacks-some-basic-forensic/ -# http://philriesch.com/computersecurity_zmeu.html -# -# Jorgee Scanner -# http://www.skepticism.us/2015/05/new-malware-user-agent-value-jorgee/ -# https://www.checkpoint.com/defense/advisories/public/2016/cpai-2016-0214.html -# https://blog.paranoidpenguin.net/2017/04/jorgee-goes-on-a-rampage/ - -import re -import util -import dshell -import datetime -import colorout -from httpdecoder import HTTPDecoder - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='peht', - description='Penetration/Exploit/Hijacking Tool detector', - longdescription=""" -The Penetration/Exploit/Hijacking Tool detector will identify the tool used to scan or exploit a server using the -User agent, URI or HTTP content. - -General usage: - decode -d peht - -Detailed usage: - decode -d peht --peht_showcontent - -Output: - - Request Timestamp (UTC): 2017-07-16 02:41:47.238549 - Penetration/Exploit/Hijacking Tool: Open Vulnerability Assessment System - User-Agent: Mozilla/5.0 [en] (X11, U; OpenVAS 8.0.9) - Request Method: GET - URI: /scripts/session/login.php - Source IP: 1.2.3.4 - Source port: 666 - MAC: 50:b4:02:39:24:56 - Host requested: example.com - - Response Timestamp (UTC): 2017-07-16 02:41:48.238549 - Response Reason: Not Found - Response Status: 404 - Destination IP: 192.168.1.1 - Destination port: 80 - MAC: a4:42:ab:56:b6:23 - - - Detailed Output: - - Request Timestamp (UTC): 2017-07-16 02:41:47.238549 - Penetration/Exploit/Hijacking Tool: Arbitrary Remote Code Execution/injection - User-Agent: Wget(linux) - Request Method: POST - URI: /command.php - Source IP: 1.2.3.4 - Source port: 666 - MAC: 50:b4:02:39:24:56 - Host requested: example.com - - cmd=%63%64%20%2F%76%61%72%2F%74%6D%70%20%26%26%20%65%63%68%6F%20%2D%6E%65%20%5C%5C%78%33%6B%65%72%20%3E%20%6B%65%72%2E%74%78%74%20%26%26%20%63%61%74%20%6B%65%72%2E%74%78%74 - - Response Timestamp (UTC): 2017-07-16 02:41:48.238549 - Response Reason: Found - Response Status: 302 - Destination IP: 192.168.1.1 - Destination port: 80 - MAC: a4:42:ab:56:b6:23 - - - - 302 Found - -

Found

-

The document has moved here.

- - -""", - filter='tcp and (port 80 or port 81 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 81, 8000, 8080) or dp in (80, 81, 8000, 8080), - author='mm', - optiondict={ - 'showcontent': {'action': 'store_true', 'default': False, 'help': 'Display the request and response body content.'} - } - ) - - self.out = colorout.ColorOutput() - self.direction = None - self.request_ioc = None - self.request_method = None - self.request_user_agent = None - self.request_host = None - self.request_rangestr = None - self.request_body = None - self.request_referer = None - self.response_content_type = None - self.response_body = None - self.response_contentencoding = None - self.response_status = None - self.response_contentlength = None - self.response_reason = None - - def preModule(self): - if 'setColorMode' in dir(self.out): - self.out.setColorMode() - - def check_payload(self, payloadheader, payloaduri, requestbody): - - ET_identified = None - - r = re.compile(r'\bbash\b | \bcmd\b | \bsh\b | \bwget\b', flags=re.I | re.X) - if r.findall(requestbody): - ET_identified = 'Arbitrary Remote Code Execution/injection' - - if payloadheader.has_key('content-type'): - struts_ioc = ['cmd', 'ProcessBuilder', 'struts'] - #Will return empty if all words from struts_ioc are in payloadheader['content-type'] - struts_check = list(filter(lambda x: x not in payloadheader['content-type'], struts_ioc)) - if not struts_check: - ET_identified = 'Apache Struts Content-Type arbitrary command execution' - - if payloadheader.has_key('user-agent'): - if 'Jorgee' in payloadheader['user-agent']: - ET_identified = 'Jorgee Scanner' - elif 'Nmap' in payloadheader['user-agent']: - ET_identified = 'Nmap' - elif 'masscan' in payloadheader['user-agent']: - ET_identified = 'Mass IP port scanner' - elif ('ZmEu' in payloadheader['user-agent'] and 'w00tw00t' in payloaduri): - ET_identified = 'ZmEu Vulnerability Scanner' - elif 'immoral' in payloadheader['user-agent']: - ET_identified = 'immoral' - elif 'chroot' in payloadheader['user-agent']: - ET_identified = 'chroot' - elif 'DataCha0s' in payloadheader['user-agent']: - ET_identified = 'DataCha0s Web Scanner' - elif 'OpenVAS' in payloadheader['user-agent']: - ET_identified = 'Open Vulnerability Assessment System' - elif ('bash' or 'sh' or 'cmd' or 'wget') in (payloadheader['user-agent']): - ET_identified = 'Arbitrary Remote Code Execution/injection' - - if 'muieblackcat' in payloaduri: - ET_identified = 'Muieblackcat Web Scanner/Robot' - if '/HNAP1/' in payloaduri: - ET_identified = 'Home Network Administration Protocol' - - return ET_identified - - - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - - if not request: - return - - # Obtain the response content - try: - if 'gzip' in util.getHeader(response, 'content-encoding'): - self.response_body = self.decompressGzipContent(response.body) - if self.response_body == None: - self.response_body = '(gunzip failed)\n' + response.body - else: - self.response_body = '(gzip encoded)\n' + self.response_body - else: - self.response_body = response.body - except AttributeError as e: - self.response_body = None - - # Obtain the request content - try: - if 'gzip' in util.getHeader(request, 'content-encoding'): - self.request_body = self.decompressGzipContent(request.body) - if self.request_body == None: - self.request_body = '(gunzip failed)\n' + request.body - else: - self.request_body = '(gzip encoded)\n' + self.request_body - else: - self.request_body = request.body - except AttributeError as e: - self.request_body = None - - # Identify the Exploit/Hijacking Tool - self.request_ioc = self.check_payload(request.headers, request.uri, self.request_body) - - if self.request_ioc: - - # REQUEST - if request.method in ('GET', 'POST', 'HEAD'): - self.direction = "sc" - self.request_method = request.method - self.request_user_agent = request.headers.get('user-agent') - self.request_host = util.getHeader(request, 'host') - self.request_rangestr = util.getHeader(request,'range') - self.request_body = request.body - self.request_referer = util.getHeader(request, 'referer') - - if request.headers.has_key('user-agent'): - self.request_user_agent = request.headers['user-agent'] - - self.out.write("\nRequest Timestamp (UTC): {0} \nPenetration/Exploit/Hijacking Tool: {1}\nUser-Agent: {2}\nRequest Method: {3}\nURI: {4}\nSource IP: {5} - Source port: {6} - MAC: {7}\nHost requested: {8}\nReferer: {9}\n".format(datetime.datetime.utcfromtimestamp( - requesttime), self.request_ioc, self.request_user_agent, self.request_method, request.uri, conn.sip, conn.sport, conn.smac, self.request_host, self.request_referer), formatTag="H2", direction=self.direction) - - # Show request body content - if self.showcontent: - self.out.write("\n{0}\n".format(self.request_body), formatTag="H2", direction=self.direction) - - if not response: - self.direction = "cs" - self.out.write('\nNo response\n', formatTag="H2", direction=self.direction) - - # RESPONSE - else: - self.direction = "cs" - self.response_content_type = util.getHeader(response, 'content-type') - self.response_contentencoding = util.getHeader(response, 'content-encoding') - self.response_status = response.status - self.response_reason = response.reason - - self.out.write("\nResponse Timestamp (UTC): {0} \nResponse Reason: {1}\nResponse Status: {2}\nDestination IP: {3} - Destination port: {4} - MAC: {5}\n".format(datetime.datetime.utcfromtimestamp( - responsetime), self.response_reason, self.response_status, conn.dip, conn.dport, conn.dmac), formatTag="H2", direction=self.direction) - - # Show response body content - if self.showcontent: - self.out.write("\n{0}\n".format(self.response_body), formatTag="H2", direction=self.direction) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() \ No newline at end of file diff --git a/decoders/http/rip-http.py b/decoders/http/rip-http.py deleted file mode 100644 index 4b49f9f..0000000 --- a/decoders/http/rip-http.py +++ /dev/null @@ -1,203 +0,0 @@ -import dshell -import re -import datetime -import sys -import string - -# import any other modules here -import re -import os -import hashlib -import util - -# we extend this -from httpdecoder import HTTPDecoder - - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='rip-http', - description='rip files from HTTP traffic', - filter='tcp and port 80', - author='bg/twp', - optiondict={'append_conn': {'action': 'store_true', 'help': 'append sourceip-destip to filename'}, - 'append_ts': {'action': 'store_true', 'help': 'append timestamp to filename'}, - 'direction': {'help': 'cs=only capture client POST, sc=only capture server GET response'}, - 'outdir': {'help': 'directory to write output files (Default: current directory)', 'metavar': 'DIRECTORY', 'default': '.'}, - 'content_filter': {'help': 'regex MIME type filter for files to save'}, - 'name_filter': {'help': 'regex filename filter for files to save'}} - ) - - def preModule(self): - if self.content_filter: - self.content_filter = re.compile(self.content_filter) - if self.name_filter: - self.name_filter = re.compile(self.name_filter) - HTTPDecoder.preModule(self) - - self.openfiles = {} # dict of httpfile objects, indexed by url - - # Create output directory, if necessary - if not os.path.exists(self.outdir): - try: - os.makedirs(self.outdir) - except (IOError, OSError) as e: - self.error("Could not create directory '%s': %s" % - (self.outdir, e)) - sys.exit(1) - - def splitstrip(self, data, sep, strip=' '): - return [lpart.strip(strip) for lpart in data.split(sep)] - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - payload = None - self.debug('%s %s' % (repr(request), repr(response))) - if (not self.direction or self.direction == 'cs') and request and request.method == 'POST' and request.body: - payload = request - elif (not self.direction or self.direction == 'sc') and response and response.status[0] == '2': - payload = response - if payload: - if not (not self.content_filter or self.content_filter.search(payload.headers['content-type'])): - payload = None - if payload: - # Calculate URL - host = util.getHeader(request, 'host') - if host == '': - host = conn.serverip - url = host + request.uri - # File already open - if url in self.openfiles: - self.debug("Adding response section to %s" % url) - (s, e) = self.openfiles[url].handleresponse(response) - self.write(" --> Range: %d - %d\n" % (s, e)) - # New file - else: - filename = request.uri.split('?')[0].split('/')[-1] - if 'content-disposition' in payload.headers: - cdparts = self.splitstrip(payload.headers['content-disposition'], ';') - for cdpart in cdparts: - try: - k, v = self.splitstrip(cdpart, '=') - if k == 'filename': - filename = v - except: - pass - self.debug("New file with URL: %s" % url) - if not self.name_filter or self.name_filter.search(filename): - if self.append_conn: - filename += '_%s-%s' % (conn.serverip, - conn.clientip) - if self.append_ts: - filename += '_%d' % (conn.ts) - if not len(filename): - filename = '%s-%s_index.html' % ( - conn.serverip, conn.clientip) - while os.path.exists(os.path.join(self.outdir, filename)): - filename += '_' - self.alert("New file: %s (%s)" % - (filename, url), conn.info()) - self.openfiles[url] = httpfile( - os.path.join(self.outdir, filename), self) - (s, e) = self.openfiles[url].handleresponse(payload) - self.write(" --> Range: %d - %d\n" % (s, e)) - if self.openfiles[url].done(): - self.alert("File done: %s (%s)" % - (self.openfiles[url].filename, url), conn.info()) - del self.openfiles[url] - - -class httpfile: - - def __init__(self, filename, decoder_instance): - self.complete = False - # Expected size in bytes of full file transfer - self.size = 0 - # List of tuples indicating byte chunks already received and written to - # disk - self.ranges = [] - self.decoder = decoder_instance - self.filename = filename - try: - self.fh = open(filename, 'w') - except IOError as e: - self.decoder.error( - "Could not create file '%s': %s" % (filename, e)) - self.fh = None - - def __del__(self): - if self.fh is None: - return - self.fh.close() - if not self.done(): - print "Incomplete file: %s" % self.filename - try: - os.rename(self.filename, self.filename + "_INCOMPLETE") - except: - pass - ls = 0 - le = 0 - for s, e in self.ranges: - if s > le + 1: - print "Missing bytes between %d and %d" % (le, s) - ls, le = s, e - - def handleresponse(self, response): - # Check for Content Range - range_start = 0 - range_end = len(response.body) - 1 - if 'content-range' in response.headers: - m = re.search( - 'bytes (\d+)-(\d+)/(\d+|\*)', response.headers['content-range']) - if m: - range_start = int(m.group(1)) - range_end = int(m.group(2)) - if len(response.body) < (range_end - range_start + 1): - range_end = range_start + len(response.body) - 1 - try: - if int(m.group(3)) > self.size: - self.size = int(m.group(3)) - except: - pass - elif 'content-length' in response.headers: - try: - if int(response.headers['content-length']) > self.size: - self.size = int(response.headers['content-length']) - except: - pass - # Update range tracking - self.ranges.append((range_start, range_end)) - # Write part of file - if self.fh is not None: - self.fh.seek(range_start) - self.fh.write(response.body) - return (range_start, range_end) - - def done(self): - self.checkranges() - return self.complete - - def checkranges(self): - self.ranges.sort() - current_start = 0 - current_end = 0 - foundgap = False - # print self.ranges - for s, e in self.ranges: - if s <= current_end + 1: - current_end = e - else: - foundgap = True - current_start = s - current_end = e - if not foundgap: - if (current_end + 1) >= self.size: - self.complete = True - return foundgap - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/http/web.py b/decoders/http/web.py deleted file mode 100755 index 752d1dc..0000000 --- a/decoders/http/web.py +++ /dev/null @@ -1,180 +0,0 @@ -import dshell -import dfile -import util -import hashlib - -from httpdecoder import HTTPDecoder - - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='web', - description='Improved version of web that tracks server response', - filter='tcp and (port 80 or port 8080 or port 8000)', - filterfn=lambda ((sip, sp), (dip, dp)): sp in ( - 80, 8000, 8080) or dp in (80, 8000, 8080), - author='bg,twp', - optiondict={ - 'maxurilen': {'type': 'int', 'default': 30, 'help': 'Truncate URLs longer than max len. Set to 0 for no truncating. (default: 30)'}, - 'md5': {'action': 'store_true', 'help': 'calculate MD5 for each response. Available in CSV output.'} - }, - ) - self.gunzip = False # Not interested in response body - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - - # - # Establish kw_items dictionary for extracted details from tcp/ip layer and request/response - # - kw_items = conn.info() - - # - # Extract useful information from HTTP *request* - # - for h in request.headers.keys(): - kw_items[h] = util.getHeader(request, h) - # Rename user-agent for backward compatability - if 'user-agent' in kw_items: - kw_items['useragent'] = kw_items.pop('user-agent') - - # Override non-existent host header with server IP address - if kw_items['host'] == '': - kw_items['host'] = conn.serverip - - # request info string for standard output - requestInfo = '%s %s%s HTTP/%s' % (request.method, - kw_items['host'] if kw_items['host'] != request.uri else '', # With CONNECT method, the URI is or contains the host, making this redudant - request.uri[:self.maxurilen] + '[truncated]' if self.maxurilen > 0 and len( - request.uri) > self.maxurilen else request.uri, - request.version) - - # - # Extract useful information from HTTP *response* (if available) - # - status = '' - reason = '' - responsesize = 0 - loc = '' - lastmodified = '' - md5 = '' - if response!=None: - - try: - responsesize = len(response.body.rstrip('\0')) - except: - responsesize = 0 - - if self.md5: - md5 = self._bodyMD5(response) - else: - md5 = '' - - try: - status = response.status - except: - status = '' - try: - reason = response.reason - except: - reason = '' - - for h in response.headers.keys(): - if not h in kw_items: - kw_items[h] = util.getHeader(response, h) - else: - kw_items['server_'+h] = util.getHeader(response, h) - if 'content-type' in kw_items: - kw_items['contenttype'] = kw_items.pop('content-type') - - loc = '' - if status[:2] == '30': - loc = util.getHeader(response, 'location') - if len(loc): - loc = '-> ' + loc - - lastmodified = util.HTTPlastmodified(response) - - # response info string for standard output - responseInfo = '%s %s %s %s' % (status, reason, loc, lastmodified) - - else: - responseInfo = '' - - # - # File objects - # - try: - if len(response.body) > 0: - responsefile = dfile.dfile( - name=request.uri, data=response.body) - else: - responsefile = '' - except: - responsefile = '' - if request.method == 'POST' and len(request.body): - ulcontenttype, ulfilename, uldata = self.POSTHandler(request.body) - uploadfile = dfile.dfile(name=ulfilename, data=uldata) - else: - uploadfile = None - - # - # Call alert with text info and kw values - # - self.alert("%-80s // %s" % (requestInfo, responseInfo), request=requestInfo, response=responseInfo, - request_time=requesttime, response_time=responsetime, request_method=request.method, - uri=request.uri, status=status, reason=reason, lastmodified=lastmodified, - md5=md5, responsesize=responsesize, responsefile=responsefile, uploadfile=uploadfile, **kw_items) - - if self.out.sessionwriter: - self.write(request.data, direction='cs') - if response: - self.write(response.body, direction='sc') - - # MD5sum(hex) of the body portion of the response - def _bodyMD5(self, response): - try: - if len(response.body) > 0: - return hashlib.md5(response.body.rstrip('\0')).hexdigest() - else: - return '' - except: - return '' - - def POSTHandler(self, postdata): - next_line_is_data = False - contenttype = '' - filename = '' - for l in postdata.split("\r\n"): - if next_line_is_data: - break - if l == '': - next_line_is_data = True # \r\n\r\n before data - continue - try: - k, v = self.splitstrip(l, ':') - if k == 'Content-Type': - contenttype = v - if k == 'Content-Disposition': - cdparts = self.splitstrip(v, ';') - for cdpart in cdparts: - try: - k, v = self.splitstrip(cdpart, '=', '"') - if k == 'filename': - filename = v - except: - pass - except: - pass - return contenttype, filename, l - - def splitstrip(self, data, sep, strip=' '): - return [lpart.strip(strip) for lpart in data.split(sep)] - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/malware/emdivi/emdivi_c2.py b/decoders/malware/emdivi/emdivi_c2.py deleted file mode 100644 index 119058c..0000000 --- a/decoders/malware/emdivi/emdivi_c2.py +++ /dev/null @@ -1,115 +0,0 @@ -from httpdecoder import HTTPDecoder -import urllib -from collections import defaultdict - -''' -JPCERT Coordination Center (JPCERT/CC) released a series of capabilities to -detect and understand compromises involving the Emdivi HTTP bot. Additional -references from JPCERT: - -http://blog.jpcert.or.jp/2015/11/emdivi-and-the-rise-of-targeted-attacks-in-japan.html -https://github.com/JPCERTCC/aa-tools -https://github.com/JPCERTCC/aa-tools/blob/master/emdivi_postdata_decoder.py - -The emdivi_c2 decoder is based on the hardwork of JPCERT/CC and thus deserve -all the credit. - -LICENSE -Copyright (C) 2015 JPCERT Coordination Center. All Rights Reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, -this list of conditions and the following acknowledgments and disclaimers. -2. Redistributions in binary form must reproduce the above copyright notice, -this list of conditions and the following acknowledgments and disclaimers in -the documentation and/or other materials provided with the distribution. -3. Products derived from this software may not include "JPCERT Coordination -Center" in the name of such derived product, nor shall "JPCERT Coordination -Center" be used to endorse or promote products derived from this software -without prior written permission. For written permission, please contact -pr@jpcret.or.jp. -''' - - -class DshellDecoder(HTTPDecoder): - - def __init__(self): - HTTPDecoder.__init__(self, - name='emdivi_c2', - description='deobfuscate Emdivi http c2', - filter='tcp and port 80', - author='bg', - ) - - def decode_payload(self, payload): - '''logic from JPCERTCC emdivi_postdata_decoder.py''' - - decoded = defaultdict() - - if ';' in payload: - delim = ';' - else: - delim = '&' - - fields = [x for x in payload.split(delim) if x] - - for field in fields: - try: - name, value = field.split('=') - except ValueError: - continue - - xor_key = 0x00 - for c in name: - xor_key = (ord(c)) ^ xor_key - - plaintext = '' - for c in urllib.unquote(value): - plaintext += chr(ord(c) ^ xor_key) - - decoded[name] = plaintext - return decoded - - def validate_payload(self, payload_dict): - ''' attempt to validate Emdivi payload. if a valid payload is found, - return the key associated with Emdivi version information ''' - # this check is very simple and will only validate payloads that content like: - # ?VER: t20.09.Koitochu.8530.7965.4444 | NT: 5.1.2600.5512 [en-US] | MEM: 128M | GMT(-6) - - version_info_key = None - for k in payload_dict: - if 'GMT' in payload_dict[k]: - version_info_key = k - break - - return version_info_key - - def HTTPHandler(self, conn, request, response, requesttime, responsetime): - if not request: - return - - decoded = defaultdict() - - if request.method in ('GET', 'POST'): - # first check the body of the GET or POST - if len(request.body) > 0: - decoded.update(self.decode_payload(request.body)) - - if not decoded and 'cookie' in request.headers: - # some traffic had encoded information - # embedded within the Cookie header - decoded.update(self.decode_payload(request.headers['cookie'])) - - if decoded: - version_info_key = self.validate_payload(decoded) - - if version_info_key: - self.alert('{}'.format(decoded[version_info_key]), **conn.info()) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/misc/followstream.py b/decoders/misc/followstream.py deleted file mode 100644 index be89d97..0000000 --- a/decoders/misc/followstream.py +++ /dev/null @@ -1,118 +0,0 @@ -import dshell -import util -import colorout -#from impacket.ImpactDecoder import EthDecoder -import datetime -import sys -import traceback -import logging - -# import any other modules here -import cgi - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='followstream', - description='Generates color-coded Screen/HTML output similar to Wireshark Follow Stream', - longdescription=""" -Generates color-coded Screen/HTML output similar to Wireshark Follow Stream. - -Output by default uses the "colorout" output class. This will send TTY -color-formatted text to stdout (the screen) if available. If output -is directed to a file (-o or --outfile), the output will be in HTML format. - -Note that the default bpf filter is to view all tcp traffic. The decoder -can also process UDP traffic, or it can be limited to specific streams -with --bpf/--ebpf. - -Useful options: - - --followstream_hex -- generates output in hex mode - --followstream_time -- includes timestamp for each blob/transmission - -Example: - - decode -d followstream --ebpf 'port 80' mypcap.pcap --followstream_time - decode -d followstream --ebpf 'port 80' mypcap.pcap -o file.html --followstream_time - -""", - filter="tcp", - author='amm', - optiondict={ - 'hex': {'action': 'store_true', 'help': 'two-column hex/ascii output'}, - 'time': {'action': 'store_true', 'help': 'include timestamp for each blob'}, - 'encoding': {'type': 'string', 'help': 'attempt to interpret text as encoded with specified schema'}, - } - ) - self.out = colorout.ColorOutput() - - def __errorHandler(self, blob, expected, offset, caller): - # Custom error handler that is called when data in a blob is missing or - # overlapping - if offset > expected: # data is missing - self.data_missing_message += "[%d missing bytes]" % ( - offset - expected) - elif offset < expected: # data is overlapping - self.data_missing_message += "[%d overlapping bytes]" % ( - offset - expected) - return True - - def preModule(self): - self.connectionCount = 0 - # Reset the color mode, in case a file is specified - if 'setColorMode' in dir(self.out): - self.out.setColorMode() - # Used to indicate when data is missing or overlapping - self.data_missing_message = '' - # overwrite the output module's default error handler - self.out.errorH = self.__errorHandler - - def postModule(self): - self.out.close() - - def connectionHandler(self, connection): - - try: - - # Skip Connections with no data transferred - if connection.clientbytes + connection.serverbytes < 1: - return - - # Update Connection Counter - self.connectionCount += 1 - - # Connection Header Information - self.out.write("Connection %d (%s)\n" % ( - self.connectionCount, str(connection.proto)), formatTag='H1') - self.out.write("Start: %s UTC\n End: %s UTC\n" % (datetime.datetime.utcfromtimestamp( - connection.starttime), datetime.datetime.utcfromtimestamp(connection.endtime)), formatTag='H2') - self.out.write("%s:%s -> %s:%s (%d bytes)\n" % (connection.clientip, connection.clientport, - connection.serverip, connection.serverport, connection.clientbytes), formatTag="H2", direction="cs") - self.out.write("%s:%s -> %s:%s (%d bytes)\n\n" % (connection.serverip, connection.serverport, - connection.clientip, connection.clientport, connection.serverbytes), formatTag="H2", direction="sc") - - self.out.write( - connection, hex=self.hex, time=self.time, encoding=self.encoding) - if self.data_missing_message: - self.out.write( - self.data_missing_message + "\n", level=logging.WARNING, time=self.time) - self.data_missing_message = '' - - # Line break before next session - self.out.write("\n\n") - - except KeyboardInterrupt: - raise - except: - print 'Error in connectionHandler: ', sys.exc_info()[1] - traceback.print_exc(file=sys.stdout) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/misc/grep.py b/decoders/misc/grep.py deleted file mode 100644 index ea7f10a..0000000 --- a/decoders/misc/grep.py +++ /dev/null @@ -1,205 +0,0 @@ -import dshell -import datetime -import sys - -# import any other modules here -import re - - -class grepDecoder(dshell.TCPDecoder): - - def __init__(self): - dshell.TCPDecoder.__init__(self, - name='grep', - description='Search for patterns in streams.', - longdescription=""" -Grep is a utility decoder, useful on it's own or in combination with -downstream (chained) decoders. Your search expression is specified with the ---grep_expression option, and the default behavior is that the entire "line" -of text surround each match will be printed, along with the standard -connection information. However, granular match information is passed to the -output decoder giving the user more control about the type of output they -would like to see. Following is the named-variable convention passed to -output: - - match: Full expression match - m1: First sub-match - m2: Second sub-match - .. - mn: N'th sub-match - -Examples: - - Snag User-Agent, display as CSV: - - decode -d grep --grep_ignorecase --grep_expression 'User-Agent: (.*?)$' --output csvout,m1 - - The text following User-Agent will be the first sub-match and then - printed as a named field in CSV output. - - Better yet: - - decode -d grep --grep_ignorecase --grep_expression 'User-Agent: (.*?)$' --oformat "%(m1)s" - - This uses the same expression but instead of the default output, - specifies "m1" in a format string which makes it the ONLY value - displayed. This is nice for piping into sort/uniq or other - command-line filters. - -Iterative matching - -Rather than alerting on an entire line or just the first hit within that line, -Python's regular expression module offers a function called "finditer" which -scans across input text and provides an iterable object of ALL the matches. -So with "--grep_iterate" we can use that. - -Examples: - - Simplistically grab all hyperlinks and dump to stdout: - - decode -d grep --grep_expression '' --grep_iterate --grep_ignorecase --oformat "%(m1)s" - -Chainable - -Grep is chainable. What does this mean? If data within a connection -matches a grep expression, the entire connection is considered a "hit" and is -then allowed to be processed by subDecoders. Non-hits are dropped. - -So this means you can search for an expression and view all matching -connections in followstream, or process all as web traffic, etc. - -Examples: - - View all web traffic that originated from Windows 7 machines: - - decode -d grep+web --grep_ignorecase --grep_expression 'User-Agent: [^\\r\\n]*Windows 6.1' -""", - author='amm', - filter='tcp', - optiondict={ - 'expression': {'type': 'string', 'help': 'Search expression'}, - 'ignorecase': {'action': 'store_true', 'help': 'Case insensitive search.'}, - 'singleline': {'action': 'store_true', 'help': 'Treat entire connection as single line of text.'}, - 'iterate': {'action': 'store_true', 'help': 'Iterate hits on match string.'}, - 'invert': {'action': 'store_true', 'help': 'For chained only: Invert hit results.'} - } - ) - self.chainable = True - - def preModule(self): - - # - # Does subdecoder have a blobHandler - # - if self.subDecoder and 'blobHandler' in dir(self.subDecoder): - self.debug("subDecoder has blobHandler") - self.subblobHandler = True - # Indexed by connection, storage for all blobs being deferred - self.deferredBlobs = {} - else: - self.subblobHandler = False - - # Pass/Drop dictionary of connections to use in chain mode - self.connstate = {} - - # Must use singleLine mode when subDecoder is present - if self.subDecoder: - self.singleline = True - - # Re parameters - self.reFlags = 0 - if self.ignorecase: - self.reFlags = self.reFlags | re.IGNORECASE - if self.singleline or self.iterate: - self.reFlags = self.reFlags | re.S - - # Re Expression -> Object - if self.expression == None or not len(self.expression): - self.error( - "Must specify expression using --%s_expression" % self.name) - sys.exit(1) - else: - sys.stderr.write("Using expression: '%s'\n" % self.expression) - self.reObj = re.compile(self.expression, self.reFlags) - - dshell.TCPDecoder.preModule(self) - - def errorH(self, **x): - # custom errorHandler here - pass - - def blobHandler(self, connection, blob): - # Defer all Blob processing until the connection is handled, so we can - # grep the entire connection stream - if self.subblobHandler: - if connection not in self.deferredBlobs: - self.deferredBlobs[connection] = [] - self.deferredBlobs[connection].append(blob) - - def connectionHandler(self, connection): - - # Normal processing, no subDecoder - if not self.subDecoder: - self.__searchStream(connection.data(direction='cs', errorHandler=self.errorH) + - "\n" + connection.data(direction='sc', errorHandler=self.errorH), connection) - return - - # Call sub blobHandler for all blobs - if self.subblobHandler and self.__connectionTest(connection): - self.debug("Preparing to process %d blobs in subdecoder" % - len(self.deferredBlobs)) - for b in self.deferredBlobs[connection]: - self.subDecoder.blobHandler(connection, b) - self.deferredBlobs[connection] = None - - # Call sub connectionHandler if necessary - if 'connectionHandler' in dir(self.subDecoder) and self.__connectionTest(connection): - self.subDecoder.connectionHandler(connection) - - def __alert(self, conn, hitstring, matchObj): - kwargs = {'match': matchObj.group(0)} - matchNumber = 0 - for mgroup in matchObj.groups(): - matchNumber += 1 - kwargs['m' + str(matchNumber)] = mgroup - self.alert(hitstring, kwargs, **conn.info()) - - def __connectionTest(self, connection): - if connection not in self.connstate: - if self.reObj.search(connection.data(direction='cs', errorHandler=self.errorH) + "\n" + connection.data(direction='sc', errorHandler=self.errorH)): - self.connstate[connection] = True - else: - self.connstate[connection] = False - if self.invert: - self.connstate[connection] = not self.connstate[connection] - if self.connstate[connection]: - return True - else: - return False - - def __searchStream(self, d, conn): - - if self.singleline or self.iterate: - self.__runSearch(d, conn) - else: - lines = d.split('\n') - for l in lines: - l = l.rstrip() - self.__runSearch(l, conn) - - def __runSearch(self, d, conn): - if self.iterate: - for m in self.reObj.finditer(d): - self.__alert(conn, m.group(0), m) - else: - m = self.reObj.search(d) - if m: - self.__alert(conn, d, m) - - -# always instantiate an dObj of the class -if __name__ == '__main__': - dObj = grepDecoder() - print dObj -else: - dObj = grepDecoder() diff --git a/decoders/misc/merge.py b/decoders/misc/merge.py deleted file mode 100644 index fd9f868..0000000 --- a/decoders/misc/merge.py +++ /dev/null @@ -1,33 +0,0 @@ -import dshell -import dpkt - - -class DshellDecoder(dshell.Decoder): - - """ - merge.py - merge all pcap in to a single file - - Example: decode -d merge *.pcap -W merged.pcap - """ - - def __init__(self): - dshell.Decoder.__init__(self, - name='merge', - description='dump all packets to single file', - longdescription="""Example: decode -d merge *.pcap -W merged.pcap""", - author='bg/twp' - ) - self.chainable = True - - def rawHandler(self, pktlen, pkt, ts, **kw): - if self.subDecoder: - return self.subDecoder.rawHandler(pktlen, str(pkt), ts, **kw) - else: - return self.dump(pktlen, pkt, ts) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/misc/synrst.py b/decoders/misc/synrst.py deleted file mode 100644 index 4cac0f5..0000000 --- a/decoders/misc/synrst.py +++ /dev/null @@ -1,41 +0,0 @@ -import dshell -import dpkt - - -class DshellDecoder(dshell.IPDecoder): - - """ - Simple TCP syn/rst filter (ipv4) only - """ - - def __init__(self): - dshell.IPDecoder.__init__(self, - name='synrst', - description='detect failed attempts to connect (SYN followed by a RST/ACK)', - filter="tcp[13]=2 or tcp[13]=20", - author='bg' - ) - self.tracker = {} # key = (srcip,srcport,seqnum,dstip,dstport) - - def packetHandler(self, ip=None): - tcp = dpkt.ip.IP(ip.pkt).data - - if tcp.flags & 2: # check for SYN flag - seqnum = tcp.seq - key = '%s:%s:%d:%s:%s' % ( - ip.sip, ip.sport, seqnum, ip.dip, ip.dport) - self.tracker[key] = '' - elif tcp.flags & 20: # check for RST/ACK flags - acknum = tcp.ack - 1 - tmpkey = '%s:%s:%d:%s:%s' % ( - ip.dip, ip.dport, acknum, ip.sip, ip.sport) - if self.tracker.__contains__(tmpkey): - self.alert('Failed connection', **ip.info()) - del self.tracker[tmpkey] - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/misc/writer.py b/decoders/misc/writer.py deleted file mode 100644 index 28479b2..0000000 --- a/decoders/misc/writer.py +++ /dev/null @@ -1,52 +0,0 @@ -''' -Created on Jan 13, 2012 - -@author: tparker -''' - -import dshell -import dpkt -from output import PCAPWriter - - -class DshellDecoder(dshell.Decoder): - - ''' - session writer - chain to a decoder to end the chain if the decoder does not output session or packets on its own - if chained to a packet-based decoder, writes all packets to pcap file, can be used to convert or concatenate files - if chained to a connection-based decoder, writes selected streams to session file - ''' - - def __init__(self, **kwargs): - ''' - Constructor - ''' - self.file = None - dshell.Decoder.__init__(self, - name='writer', - description='pcap/session writer', - author='twp', - raw=True, - optiondict=dict( - filename=dict( - default='%(clientip)s:%(clientport)s-%(serverip)s:%(serverport)s-%(direction)s.txt' - ), - ) - ) - - def rawHandler(self, pktlen, pkt, ts, **kwargs): - self.decodedbytes += pktlen - self.count += 1 - self.dump(pktlen, pkt, ts) # pktlen may be wrong if we stripped vlan - - def IPHandler(self, addr, ip, ts, pkttype=None, **kw): - self.decodedbytes += len(ip.data) - self.count += 1 - # if we are passed in IP data vs layer-2 frames, we need to encapsulate - # them - self.dump(dpkt.ethernet.Ethernet(data=str(ip), pkttype=type), ts=ts) - - def connectionHandler(self, conn): - self.write(conn) - -dObj = DshellDecoder() diff --git a/decoders/misc/xor.py b/decoders/misc/xor.py deleted file mode 100644 index 40f1f9c..0000000 --- a/decoders/misc/xor.py +++ /dev/null @@ -1,120 +0,0 @@ -import dshell -import util -import struct - - -class DshellDecoder(dshell.TCPDecoder): - - def __init__(self): - self.xorconn = {} # required to track each individual connection - dshell.TCPDecoder.__init__(self, - name='xor', - description='XOR an entire stream with a given single byte key', - filter="tcp", - author='twp', - optiondict={ - 'key': {'type': 'str', 'default': '0xff', 'help': 'xor key [default 255]'}, - 'cskey': {'type': 'str', 'default': None, 'help': 'c->s xor key [default None]'}, - 'sckey': {'type': 'str', 'default': None, 'help': 's->c xor key [default None]'}, - 'resync': {'action': 'store_true', 'help': 'resync if the key is seen in the stream'}, - } - ) - # sets chainable to true and requires connectionInitHandler() and - # connectionCloseHandler() - self.chainable = True - - def preModule(self, *args, **kwargs): - dshell.TCPDecoder.preModule(self, *args, **kwargs) - # twp handle hex keys - self.key = self.makeKey(self.key) - if self.cskey: - self.cskey = self.makeKey(self.cskey) - if self.sckey: - self.sckey = self.makeKey(self.sckey) - - def makeKey(self, key): - if key.startswith('"'): - return key[1:-1] - if key.startswith('0x'): - k, key = '', key[2:] - for i in xrange(0, len(key), 2): - k += chr(int(key[i:i + 2], 16)) - return k - else: - return struct.pack('I', int(key)) - - # - # connectionInitHandler is required as this module (and all other chainable modules) will have to track all - # each connection independently of dshell.TCPDecoder - # - def connectionInitHandler(self, conn): - # need to set up a custom connection tracker to handle - self.xorconn[conn.addr] = dshell.Connection(self, conn.addr, conn.ts) - self.xorconn[conn.addr].nextoffset = conn.nextoffset - self.xorconn[conn.addr].proto = conn.proto - self.xorconn[conn.addr].info(proto=conn.proto) - - # - # Each blob will be xor'ed and the "newblob" data will be added to the connection - # we are individually tracking - # - def blobHandler(self, conn, blob): - k = 0 # key index - # create new data (ie. pkt data) - # with appropriate key - data, newdata = blob.data(), '' - self.debug('IN ' + util.hexPlusAscii(blob.data())) - if self.cskey != None and blob.direction == 'cs': - key = self.cskey - elif self.sckey != None and blob.direction == 'sc': - key = self.sckey - else: - key = self.key - for i in xrange(len(data)): - if self.resync and data[i:i + len(key)] == key: - k = 0 # resync if the key is seen - # xor this byte with the aligned byte from the key - newdata += chr(ord(data[i]) ^ ord(key[k])) - k = (k + 1) % len(key) # move key position - # update our connection object with the new data - newblob = self.xorconn[conn.addr].update( - conn.endtime, blob.direction, newdata) - self.debug('OUT ' + repr(self.key) + ' ' + util.hexPlusAscii(newdata)) - # if there is another decoder we want to pass this data too - if newblob and 'blobHandler' in dir(self.subDecoder): - # pass to the subDecoder's blobHandler() - self.subDecoder.blobHandler(self.xorconn[conn.addr], newblob) - - # - # The connection has finished without errors, then we pass the entire connection to the subDecoder's - # connectionHandler() - # - def connectionHandler(self, conn): - if conn.addr in self.xorconn: - self.xorconn[conn.addr].proto = conn.proto - if 'connectionHandler' in dir(self.subDecoder): - self.subDecoder.connectionHandler(self.xorconn[conn.addr]) - else: - self.write(self.xorconn[conn.addr]) - - # - # connectionCloseHandler is called when: - # - a connection finishes w/o errors (no data loss) - # - a connection finishes w errors - # - # If the connection exists in our custom connection tracker (self.xorconn), - # we will have to pass it to the subDecoder's connectionCloseHandler - # - # - def connectionCloseHandler(self, conn): - if conn.addr in self.xorconn: - if 'connectionCloseHandler' in dir(self.subDecoder): - self.subDecoder.connectionCloseHandler(self.xorconn[conn.addr]) - del self.xorconn[conn.addr] - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/nbns/nbns.py b/decoders/nbns/nbns.py deleted file mode 100644 index 55e9bed..0000000 --- a/decoders/nbns/nbns.py +++ /dev/null @@ -1,139 +0,0 @@ -import dpkt -import dshell -from struct import unpack - - -# A few common NBNS Protocol Info Opcodes -# Due to a typo in RFC 1002, 0x9 is also acceptable, but rarely used -# for 'NetBios Refresh' -# 'NetBios Multi-Homed Name Regsitration' (0xf) was added after the RFC -nbns_op = { 0: 'NB_NAME_QUERY', - 5: 'NB_REGISTRATION', - 6: 'NB_RELEASE', - 7: 'NB_WACK', - 8: 'NB_REFRESH', - 9: 'NB_REFRESH', - 15: 'NB_MULTI_HOME_REG' } - - -class DshellDecoder(dshell.UDPDecoder): - - def __init__(self): - dshell.UDPDecoder.__init__(self, - name='nbns', - description='Extract client information from NBNS traffic', - longdescription=""" -The nbns (NetBIOS Name Service) decoder will extract the Transaction ID, Protocol Info, -Client Hostname, and Client MAC address from every UDP NBNS packet found in the given -pcap using port 137. UDP is the standard transport protocol for NBNS traffic. -This filter pulls pertinent information from NBNS packets. - -Examples: - - General usage: - - decode -d nbns - - This will display the connection info including the timestamp, - the source IP, destination IP, Transaction ID, Protocol Info, - Client Hostname, and the Client MAC address in a tabular format. - - - Malware Traffic Analysis Exercise Traffic from 2014-12-08 where a user was hit with a Fiesta exploit kit: - - We want to find out more about the infected machine, and some of this information can be pulled from NBNS traffic - - decode -d nbns /2014-12-08-traffic-analysis-exercise.pcap - - OUTPUT (first few packets): - nbns 2014-12-08 18:19:13 192.168.204.137:137 -- 192.168.204.2:137 ** - Transaction ID: 0xb480 - Info: NB_NAME_QUERY - Client Hostname: WPAD - Client MAC: 00:0c:29:9d:b8:6d - ** - nbns 2014-12-08 18:19:14 192.168.204.137:137 -- 192.168.204.2:137 ** - Transaction ID: 0xb480 - Info: NB_NAME_QUERY - Client Hostname: WPAD - Client MAC: 00:0c:29:9d:b8:6d - ** - nbns 2014-12-08 18:19:16 192.168.204.137:137 -- 192.168.204.2:137 ** - Transaction ID: 0xb480 - Info: NB_NAME_QUERY - Client Hostname: WPAD - Client MAC: 00:0c:29:9d:b8:6d - ** - nbns 2014-12-08 18:19:17 192.168.204.137:137 -- 192.168.204.255:137 ** - Transaction ID: 0xb480 - Info: NB_NAME_QUERY - Client Hostname: WPAD - Client MAC: 00:0c:29:9d:b8:6d - """, - filter='udp and port 137', - author='dek', - ) - self.mac_address = None - self.client_hostname = None - self.xid = None - self.prot_info = None - - - def packetHandler(self, udp, data): - try: - nbns_packet = dpkt.netbios.NS(data) - except (dpkt.dpkt.UnpackError, IndexError) as e: - self.warn('{}: dpkt could not parse session data \ - (NBNS packet not found)'.format(str(e))) - return - - - # Extract the Client hostname from the connection data - # It is represented as 32-bytes half-ASCII - try: - nbns_name = unpack('32s', data[13:45])[0] - except error as e: - self.warn('{}: (NBNS packet not found)'.format(str(e))) - return - - - # Decode the 32-byte half-ASCII name to its 16 byte NetBIOS name - try: - self.client_hostname = dpkt.netbios.decode_name(nbns_name) - - # For uniformity, strip excess byte - self.client_hostname = self.client_hostname[0:-1] - except ValueError as e: - self.warn('{}: Hostname in improper format \ - (NBNS packet not found)'.format(str(e))) - return - - - # Extract the Transaction ID from the NBNS packet - self.xid = hex(nbns_packet.id) - - # Extract the opcode info from the NBNS Packet - op = nbns_packet.op - # Remove excess bits - op = (op >> 11) & 15 - - # Extract protocol info if present in the payload - if nbns_op[op]: - self.prot_info = nbns_op[op] - else: - self.prot_info = hex(nbns_packet.op) - - # Extract the MAC address from the ethernet layer of the packet - self.mac_address = udp.smac - - - if self.xid and self.prot_info and self.client_hostname and self.mac_address: - self.alert('\n\tTransaction ID:\t\t{:<8} \n\tInfo:\t\t\t{:<16} \n\tClient Hostname:\t{:<16} \n\tClient MAC:\t\t{:<18}\n'.format( - self.xid, self.prot_info, self.client_hostname, self.mac_address), **udp.info()) - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/protocol/ether.py b/decoders/protocol/ether.py deleted file mode 100644 index 23b3c2e..0000000 --- a/decoders/protocol/ether.py +++ /dev/null @@ -1,31 +0,0 @@ -import dshell -import util -import dpkt -import datetime -import binascii - - -class DshellDecoder(dshell.Decoder): - - def __init__(self): - dshell.Decoder.__init__(self, - name='ether', - description='raw ethernet capture decoder', - filter='', - author='twp', asdatetime=True - ) - - def rawHandler(self, dlen, data, ts, **kw): - if self.verbose: - self.log("%.06f %d\n%s" % (ts, dlen, util.hexPlusAscii(str(data)))) - eth = dpkt.ethernet.Ethernet(str(data)) - src = binascii.hexlify(eth.src) - dst = binascii.hexlify(eth.dst) - self.alert('%6x->%6x %4x len %d' % (long(src, 16), long(dst, 16), eth.type, - len(eth.data)), type=eth.type, bytes=len(eth.data), src=src, dst=dst, ts=ts) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/protocol/ip.py b/decoders/protocol/ip.py deleted file mode 100644 index f432172..0000000 --- a/decoders/protocol/ip.py +++ /dev/null @@ -1,30 +0,0 @@ -import dshell -import util -import dpkt -import traceback - - -class DshellDecoder(dshell.IP6Decoder): - - _PROTO_MAP = {dpkt.ip.IP_PROTO_TCP: 'TCP', 17: 'UDP'} - - def __init__(self): - dshell.IP6Decoder.__init__(self, - name='ip', - description='IPv4/IPv6 decoder', - filter='ip or ip6', - author='twp', - ) - - def packetHandler(self, ip=None, proto=None): - if self.verbose: - self.out.log(util.hexPlusAscii(ip.pkt)) - self.alert(**ip.info()) - if self.out.sessionwriter: - self.write(ip) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/protocol/protocol.py b/decoders/protocol/protocol.py deleted file mode 100644 index de402dc..0000000 --- a/decoders/protocol/protocol.py +++ /dev/null @@ -1,40 +0,0 @@ -import dshell -import dpkt - -# Build a list of known IP protocols from dpkt -try: - PROTOCOL_MAP = dict((v, k[9:]) for k, v in dpkt.ip.__dict__.iteritems() if type( - v) == int and k.startswith('IP_PROTO_') and k != 'IP_PROTO_HOPOPTS') -except: - PROTOCOL_MAP = {} - - -class DshellDecoder(dshell.IPDecoder): - - """ - protocol.py - - Identifies non-standard protocols (not tcp, udp or icmp) - - References: - http://www.networksorcery.com/enp/protocol/ip.htm - """ - - def __init__(self): - dshell.IPDecoder.__init__(self, - name='protocol', - description='Identifies non-standard protocols (not tcp, udp or icmp)', - filter='(ip and not tcp and not udp and not icmp)', - author='bg', - ) - - def packetHandler(self, ip): - p = PROTOCOL_MAP.get(ip.proto, ip.proto) - self.alert('PROTOCOL: %s (%d)' % - (p, ip.proto), sip=ip.sip, dip=ip.dip, ts=ip.ts) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/smb/psexec.py b/decoders/smb/psexec.py deleted file mode 100644 index 8d2455d..0000000 --- a/decoders/smb/psexec.py +++ /dev/null @@ -1,247 +0,0 @@ -""" -2015 Feb 13 - -Processes SMB traffic and attempts to extract command/response information -from psexec. - -When a successful SMB connection is seen and it matches a psexec regular -expression, it creates a new "psexec" object to store connection information -and messages. - -Once the connection closes, an alert is generated (of configurable verbosity) -relaying basic information and messages passed. -""" - -#import dshell -from smbdecoder import SMBDecoder -import colorout -import util -import re -import datetime - -SMB_STATUS_SUCCESS = 0x0 -SMB_COM_OPEN = 0x02 # Open a file. -SMB_COM_CLOSE = 0x04 # Close a file. -SMB_COM_NT_CREATE_ANDX = 0xa2 # Create or open a file or a directory. -SMB_COM_WRITE_ANDX = 0x2f # Extended file write with AndX chaining. -SMB_COM_READ_ANDX = 0x2E -SMB_COM_SESSION_SETUP_ANDX = 0x73 - - -class DshellDecoder(SMBDecoder): - - def __init__(self): - # dictionary indexed by uid, points to login domain\name (string) - self.uidname = {} - self.fidhandles = {} # dictionary to map fid handles to psexec objects - # dictionary of psexec objects, indexed by conn+PID (use sessIndex - # function) - self.psexecobjs = {} - # FID won't work as an index because each stream has its own - SMBDecoder.__init__(self, - name='psexec', - description='Extract command/response information from psexec over smb', - filter='tcp and (port 445 or port 139)', - filterfn=lambda t: t[0][1] == 445 or t[1][1] == 445 or t[0][1] == 139 or t[1][1] == 139, - author='amm', - optiondict={ - 'alertsonly': {'action': 'store_true', 'help': 'only dump alerts, not content'}, - 'htmlalert': {'action': 'store_true', 'help': 'include html as named value in alerts'}, - 'time': {'action': 'store_true', 'help': 'display command/response timestamps'} - } - ) - self.legacy = True - # self.out=colorout.ColorOutput(title='psexec') - self.out = colorout.ColorOutput() - - def sessIndexFromPID(self, conn, pid): - return ':'.join((str(conn.starttime), conn.sip, str(conn.sport), conn.dip, str(conn.dport), pid)) - - def connectionHandler(self, conn): - SMBDecoder.connectionHandler(self, conn) - for k in self.psexecobjs.keys(): - del self.psexecobjs[k] - - # - # Internal class to contain psexec session information - # - class psexec: - - def __init__(self, parent, conn, hostname, pid, opentime): - self.parent = parent - self.conn = conn - self.hostname = hostname - self.pid = pid - self.opentime = opentime - self.closetime = conn.endtime - self.username = '' - self.open_iohandles = {} # indexed by FID, points to filename - self.closed_iohandles = {} - self.msgList = [] # List of tuples (text, direction) - self.csCount = 0 - self.scCount = 0 - self.csBytes = 0 - self.scBytes = 0 - self.lastDirection = '' - - def addmsg(self, text, direction, ts): - # Only store timestamp information if this is a change in direction - if direction == self.lastDirection: - self.msgList.append((text, direction, None)) - else: - self.msgList.append((text, direction, ts)) - self.lastDirection = direction - if direction == 'cs': - self.csCount += 1 - self.csBytes += len(text) - elif direction == 'sc': - self.scCount += 1 - self.scBytes += len(text) - - def addIO(self, fid, name): - if fid in self.open_iohandles: - self.parent.warn("IO Handle with FID %s (%s) is already associated with psexec session %d" % ( - hex(fid), name, self.pid)) - self.open_iohandles[fid] = name - - def delIO(self, fid): - if fid in self.open_iohandles: - self.closed_iohandles[fid] = self.open_iohandles[fid] - del self.open_iohandles[fid] - - def handleCount(self): - return len(self.open_iohandles) - # - # Long output (screen/html) - # - - def write(self, out=None): - if out == None: - out = self.parent.out - out.write("PSEXEC Service from host %s with PID %s\n" % - (self.hostname, self.pid), formatTag='H1') - if len(self.username): - out.write("User: %s\n" % (self.username), formatTag='H2') - out.write("Start: %s UTC\n End: %s UTC\n" % (datetime.datetime.utcfromtimestamp( - self.conn.starttime), datetime.datetime.utcfromtimestamp(self.conn.endtime)), formatTag='H2') - out.write("%s:%s -> %s:%s\n" % (self.conn.clientip, self.conn.clientport, - self.conn.serverip, self.conn.serverport), formatTag="H2", direction="cs") - out.write("%s:%s -> %s:%s\n\n" % (self.conn.serverip, self.conn.serverport, - self.conn.clientip, self.conn.clientport), formatTag="H2", direction="sc") - for msg in self.msgList: - out.write( - msg[0], direction=msg[1], timestamp=msg[2], time=self.parent.time) - out.write("\n") - # - # Short output (alert) - # - - def alert(self): - kwargs = {'hostname': self.hostname, 'pid': self.pid, 'username': self.username, - 'opentime': self.opentime, 'closetime': self.closetime, - 'csCount': self.csCount, 'scCount': self.scCount, 'csBytes': self.csBytes, 'scBytes': self.scBytes} - if self.parent.htmlalert: - htmlfactory = colorout.ColorOutput( - htmlgenerator=True, title="psexec") - self.write(htmlfactory) - htmlfactory.close() - kwargs['html'] = htmlfactory.htmldump() - kwargs.update(self.conn.info()) - kwargs['ts'] = self.opentime - self.parent.alert( - "Host: %s, PID: %s, CS: %d, SC: %d, User: %s" % ( - self.hostname, self.pid, self.csBytes, self.scBytes, self.username), - kwargs - ) - - def __del__(self): - if self.parent.alertsonly: - self.alert() - else: - self.write() - - def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None): - # we only care about valid responses and matching request/response user - # IDs - if status == SMB_STATUS_SUCCESS and request.uid == response.uid: - - if cmd == SMB_COM_SESSION_SETUP_ANDX and type(status) != type(None): - auth_record = request.PARSE_SESSION_SETUP_ANDX_REQUEST( - request.smbdata) - if not(auth_record): - return - domain_name = auth_record.domain_name - user_name = auth_record.user_name - self.uidname[response.uid] = "%s\\%s" % ( - domain_name, user_name) - - # file is being requested/opened - elif cmd == SMB_COM_NT_CREATE_ANDX: - self.debug('%s UID: %s MID: %s NT Create AndX Status: %s' % ( - conn.addr, request.uid, response.mid, hex(status))) - filename = request.PARSE_NT_CREATE_ANDX_REQUEST( - request.smbdata) - if type(filename) == type(None): - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_REQUEST\n%s' % util.hexPlusAscii( - request.smbdata)) - return - - fid = response.PARSE_NT_CREATE_ANDX_RESPONSE(response.smbdata) - - if fid == -1: - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_RESPONSE\n%s' % util.hexPlusAscii( - response.smbdata)) - self.debug(util.hexPlusAscii(response.smbdata)) - return - match = re.search( - r'psexecsvc-(.*)-(\d+)-(stdin|stdout|stderr)', filename) - if not match: - return - - # We have a PSEXEC File Handle! - hostname = match.group(1) - pid = match.group(2) - iohandleName = match.group(3) - sessionIndex = self.sessIndexFromPID(conn, pid) - if not sessionIndex in self.psexecobjs: - self.psexecobjs[sessionIndex] = self.psexec( - self, conn, hostname, pid, requesttime) - self.fidhandles[fid] = self.psexecobjs[sessionIndex] - self.fidhandles[fid].addIO(fid, filename) - if response.uid in self.uidname: - self.fidhandles[fid].username = self.uidname[response.uid] - - elif cmd == SMB_COM_WRITE_ANDX: # write data to the file - fid, rawbytes = request.PARSE_WRITE_ANDX(request.smbdata) - self.debug('COM_WRITE_ANDX\n%s' % - (util.hexPlusAscii(request.smbdata))) - if fid in self.fidhandles: - self.fidhandles[fid].addmsg(rawbytes, 'cs', requesttime) - - elif cmd == SMB_COM_READ_ANDX: # write data to the file - fid = request.PARSE_READ_ANDX_Request(request.smbdata) - rawbytes = response.PARSE_READ_ANDX_Response(response.smbdata) - self.debug('COM_READ_ANDX (FID %s)\n%s' % - (fid, util.hexPlusAscii(response.smbdata))) - if fid in self.fidhandles: - self.fidhandles[fid].addmsg(rawbytes, 'sc', responsetime) - - elif cmd == SMB_COM_CLOSE: # file is being closed - fid = request.PARSE_COM_CLOSE(request.smbdata) - if fid in self.fidhandles.keys(): - self.fidhandles[fid].delIO(fid) - self.debug('Closing FID: %s Filename: %s' % - (hex(fid), self.fidhandles[fid])) - if self.fidhandles[fid].handleCount() < 1 and self.sessIndexFromPID(conn, self.fidhandles[fid].pid) in self.psexecobjs: - self.psexecobjs[ - self.sessIndexFromPID(conn, self.fidhandles[fid].pid)].closetime = responsetime - del self.psexecobjs[ - self.sessIndexFromPID(conn, self.fidhandles[fid].pid)] - del self.fidhandles[fid] - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/smb/rip-smb-uploads.py b/decoders/smb/rip-smb-uploads.py deleted file mode 100644 index 5abe9e6..0000000 --- a/decoders/smb/rip-smb-uploads.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -2015 Feb 13 - -Goes through SMB traffic and snips out any file uploads it sees. - -Specifically, it looks for create, write, and close commands and creates a -local file, writes the raw data to the local file, and closes the file, -respectively. -""" - -import dshell -from smbdecoder import SMBDecoder -import sys -import util -import os - -SMB_STATUS_SUCCESS = 0x0 -SMB_COM_OPEN = 0x02 # Open a file. -SMB_COM_CLOSE = 0x04 # Close a file. -SMB_COM_NT_CREATE_ANDX = 0xa2 # Create or open a file or a directory. -SMB_COM_WRITE_ANDX = 0x2f # Extended file write with AndX chaining. - - -class DshellDecoder(SMBDecoder): - - def __init__(self): - self.fidhandles = {} # dictionary to map fid handles to filenames - # dictionary to map fid handles to local filedescriptors - # (ie. fd = open(fname,'wb')) - self.fds = {} - self.outdir = None - SMBDecoder.__init__(self, - name='rip-smb-uploads', - description='Extract files uploaded via SMB', - filter='tcp and port 445', - filterfn=lambda t: t[0][1] == 445 or t[1][1] == 445, - author='bg', - optiondict={ - "outdir": {"help": "Directory to place files (default: ./smb_out)", "default": "./smb_out", "metavar": "DIRECTORY"}, - } - ) - self.legacy = True - - def preModule(self): - if not os.path.exists(self.outdir): - try: - os.makedirs(self.outdir) - except OSError as e: - self.error("Could not create directory '%s'\n%s" % (self.outdir, e)) - sys.exit(1) - - def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None): - # we only care about valid responses and matching request/response user - # IDs - if status == SMB_STATUS_SUCCESS and request.uid == response.uid: - - if cmd == SMB_COM_NT_CREATE_ANDX: # file is being requested/opened - self.debug('%s UID: %s MID: %s NT Create AndX Status: %s' % ( - conn.addr, request.uid, response.mid, hex(status))) - filename = request.PARSE_NT_CREATE_ANDX_REQUEST( - request.smbdata) - if type(filename) == type(None): - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_REQUEST\n%s' % util.hexPlusAscii(request.smbdata)) - return - - fid = response.PARSE_NT_CREATE_ANDX_RESPONSE(response.smbdata) - self.debug('%s FID: %s' % (conn.addr, fid)) - - if fid == -1: - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_RESPONSE\n%s' % util.hexPlusAscii(response.smbdata)) - self.debug(util.hexPlusAscii(response.smbdata)) - return - self.fidhandles[fid] = self.__localfilename(self.outdir, os.path.normpath(filename)) - - elif cmd == SMB_COM_WRITE_ANDX: # write data to the file - fid, rawbytes = request.PARSE_WRITE_ANDX(request.smbdata) - - # do we have a local fd already open to handle this write? - if fid in self.fds.keys(): - self.fds[fid].write(rawbytes) - else: - try: - fidhandle = self.fidhandles[fid] - self.fds[fid] = open(fidhandle, 'wb') - self.fds[fid].write(rawbytes) - except KeyError: - self.debug("Error: Could not find fidhandle for FID %s" % (fid)) - return - - elif cmd == SMB_COM_CLOSE: # file is being closed - fid = request.PARSE_COM_CLOSE(request.smbdata) - if fid in self.fds.keys(): - self.log(repr(conn) + '\t%s' % (self.fidhandles[fid])) - self.fds[fid].close() - del self.fds[fid] - if fid in self.fidhandles.keys(): - self.debug('Closing FID: %s Filename: %s' % - (hex(fid), self.fidhandles[fid])) - del self.fidhandles[fid] - - - def __localfilename(self, path, origname): - # Generates a local file name based on the original - tmp = origname.replace("\\", "_") - tmp = tmp.replace("/", "_") - tmp = tmp.replace(":", "_") - localname = '' - for c in tmp: - if ord(c) > 32 and ord(c) < 127: - localname += c - else: - localname += "%%%02X" % ord(c) - localname = os.path.join(path, localname) - postfix = '' - i = 0 - while os.path.exists(localname + postfix): - i += 1 - postfix = "_%02d" % i - return localname + postfix - - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/smb/smbfiles.py b/decoders/smb/smbfiles.py deleted file mode 100644 index 6b87ad1..0000000 --- a/decoders/smb/smbfiles.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -2015 Feb 13 - -Processes SMB traffic and tries to find file reads and writes. - -When a read or write action is seen, the size of the transfer is recorded in -a new "smbfile" object and a count is incremented for the type of action taken -(i.e. reads+1 or writes+1). - -After the connection closes, an alert is generated showing some information -about the connection, the action taken (read, write, or both), the full name -of the file and how much data was transferred. -""" - -from smbdecoder import SMBDecoder -import util - -SMB_STATUS_SUCCESS = 0x0 -SMB_COM_OPEN = 0x02 # Open a file. -SMB_COM_CLOSE = 0x04 # Close a file. -SMB_COM_NT_CREATE_ANDX = 0xa2 # Create or open a file or a directory. -SMB_COM_WRITE_ANDX = 0x2f # Extended file write with AndX chaining. -SMB_COM_READ_ANDX = 0x2E -SMB_COM_SESSION_SETUP_ANDX = 0x73 -SMB_COM_TREE_CONNECT_ANDX = 0x75 - - -class DshellDecoder(SMBDecoder): - - def __init__(self): - # dictionary indexed by uid, points to login tuple (hostname, - # domain\name) (string) - self.uidname = {} - self.tidmap = {} # dictionary indexed by tid, points to tree path - # dictionary of smb file objects, indexed by conn+fid (use - # sessIndexFromFID function) - self.smbfileobjs = {} - SMBDecoder.__init__(self, - name='smbfiles', - description='List files accessed via smb', - filter='tcp and (port 445 or port 139)', - filterfn=lambda t: t[0][1] == 445 or t[1][1] == 445 or t[0][1] == 139 or t[1][1] == 139, - author='amm', - optiondict={ - 'nopsexec': {'action': 'store_true', 'help': 'supress psexecsvc streams from output'}, - 'activeonly': {'action': 'store_true', 'help': 'only output files with reads or writes'} - } - ) - - def fileIndexFromFID(self, conn, fid): - return ':'.join((str(conn.starttime), conn.sip, str(conn.sport), conn.dip, str(conn.dport), str(fid))) - - def connectionHandler(self, conn): - SMBDecoder.connectionHandler(self, conn) - for k in self.smbfileobjs.keys(): - del self.smbfileobjs[k] - - # - # Internal class to contain info about files - # - class smbfile: - - def __init__(self, parent, conn, fid, opentime, filename, username, hostname, treepath): - self.parent = parent - self.conn = conn - self.opentime = opentime - self.closetime = conn.endtime - self.filename = filename - self.username = username - self.hostname = hostname - self.treepath = treepath - self.writes = 0 - self.reads = 0 - self.byteswritten = 0 - self.bytesread = 0 - - def writeblock(self, data): - self.writes += 1 - self.byteswritten += len(data) - - def readblock(self, data): - self.reads += 1 - self.bytesread += len(data) - - def alert(self): - if self.parent.nopsexec and self.filename.lower().startswith('\psexecsvc'): - return - if self.reads > 0 and self.writes > 0: - mode = 'B' - elif self.reads > 0: - mode = 'R' - elif self.writes > 0: - mode = 'W' - else: - mode = '-' - if self.parent.activeonly and mode == '-': - return - kwargs = { - 'filename': self.filename, 'username': self.username, 'hostname': self.hostname, 'treepath': self.treepath, - 'opentime': self.opentime, 'closetime': self.closetime, 'mode': mode, - 'writes': self.writes, 'reads': self.reads, 'byteswritten': self.byteswritten, 'bytesread': self.bytesread - } - kwargs.update(self.conn.info()) - kwargs['ts'] = self.opentime - self.parent.alert( - "%s %s%s (%s)" % ( - self.username, self.treepath, self.filename, mode), - kwargs - ) - - def __del__(self): - self.alert() - - def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None): - # we only care about valid responses and matching request/response user - # IDs - if status == SMB_STATUS_SUCCESS and request.uid == response.uid: - - # - # SMB_COM_SESSION_SETUP - Start tracking user authentication by UID - # - if cmd == SMB_COM_SESSION_SETUP_ANDX and type(status) != type(None): - auth_record = request.PARSE_SESSION_SETUP_ANDX_REQUEST( - request.smbdata) - if not(auth_record): - return - domain_name = auth_record.domain_name - user_name = auth_record.user_name - host_name = auth_record.host_name - self.uidname[response.uid] = ( - host_name, "%s\%s" % (domain_name, user_name)) - - # - # SMB_COM_TREE_CONNECT - Start tracking tree by TID - # - if cmd == SMB_COM_TREE_CONNECT_ANDX: - request_path = unicode(request.SMB_COM_TREE_CONNECT_ANDX_Request( - request.smbdata), 'utf-16').encode('utf-8').rstrip('\0') - self.tidmap[response.tid] = request_path - - # - # SMB_COM_NT_CREATE - Start tracking file handle by FID - # - # file is being requested/opened - elif cmd == SMB_COM_NT_CREATE_ANDX: - self.debug('%s UID: %s MID: %s NT Create AndX Status: %s' % ( - conn.addr, request.uid, response.mid, hex(status))) - filename = request.PARSE_NT_CREATE_ANDX_REQUEST( - request.smbdata) - if type(filename) == type(None): - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_REQUEST\n%s' % util.hexPlusAscii( - request.smbdata)) - return - fid = response.PARSE_NT_CREATE_ANDX_RESPONSE(response.smbdata) - if fid == -1: - self.debug('Error: smb.SMB.PARSE_NT_CREATE_ANDX_RESPONSE\n%s' % util.hexPlusAscii( - response.smbdata)) - self.debug(util.hexPlusAscii(response.smbdata)) - return - # Setup smbfile object - if response.uid in self.uidname: - hostname, username = self.uidname[response.uid] - else: - hostname = 'Unknown' - username = 'Unknown\\Unknown' - if response.tid in self.tidmap: - treepath = self.tidmap[response.tid] - else: - treepath = '' - fileobj = self.smbfile( - self, conn, fid, requesttime, filename, username, hostname, treepath) - fileIndex = self.fileIndexFromFID(conn, fid) - self.smbfileobjs[fileIndex] = fileobj - - # - # SMB_COM_WRITE - File writes - # - elif cmd == SMB_COM_WRITE_ANDX: # write data to the file - fid, rawbytes = request.PARSE_WRITE_ANDX(request.smbdata) - #self.debug('COM_WRITE_ANDX\n%s' % (util.hexPlusAscii(request.smbdata))) - fileIndex = self.fileIndexFromFID(conn, fid) - if fileIndex in self.smbfileobjs: - self.smbfileobjs[fileIndex].writeblock(rawbytes) - - # - # SMB_COM_READ - File reads - # - elif cmd == SMB_COM_READ_ANDX: # read data from the file - fid = request.PARSE_READ_ANDX_Request(request.smbdata) - rawbytes = response.PARSE_READ_ANDX_Response(response.smbdata) - #self.debug('COM_READ_ANDX (FID %s)\n%s' % (fid, util.hexPlusAscii(response.smbdata))) - fileIndex = self.fileIndexFromFID(conn, fid) - if fileIndex in self.smbfileobjs: - self.smbfileobjs[fileIndex].readblock(rawbytes) - - # - # SMB_COM_CLOSE - Closing file - # - elif cmd == SMB_COM_CLOSE: # file is being closed - fid = request.PARSE_COM_CLOSE(request.smbdata) - fileIndex = self.fileIndexFromFID(conn, fid) - if fileIndex in self.smbfileobjs: - self.smbfileobjs[fileIndex].closetime = responsetime - del self.smbfileobjs[fileIndex] - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/decoders/templates/PacketDecoder.py b/decoders/templates/PacketDecoder.py deleted file mode 100644 index 16becda..0000000 --- a/decoders/templates/PacketDecoder.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python - -import dshell -import output -import util - - -class DshellDecoder(dshell.IPDecoder): - - '''generic packet-level decoder template''' - - def __init__(self, **kwargs): - '''decoder-specific config''' - - '''pairs of 'option':{option-config}''' - self.optiondict = {} - - '''bpf filter, for ipV4''' - self.filter = '' - '''filter function''' - # self.filterfn= - - '''init superclasses''' - self.__super__().__init__(**kwargs) - - def packetHandler(self, ip): - '''handle as Packet() ojects''' - pass - -# create an instance at load-time -dObj = DshellDecoder() diff --git a/decoders/templates/SessionDecoder.py b/decoders/templates/SessionDecoder.py deleted file mode 100644 index c2bc04d..0000000 --- a/decoders/templates/SessionDecoder.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env python - -import dshell -import output -import util - - -class DshellDecoder(dshell.TCPDecoder): - - '''generic session-level decoder template''' - - def __init__(self, **kwargs): - '''decoder-specific config''' - - '''pairs of 'option':{option-config}''' - self.optiondict = {} - - '''bpf filter, for ipV4''' - self.filter = '' - '''filter function''' - # self.filterfn= - - '''init superclasses''' - self.__super__().__init__(**kwargs) - - def packetHandler(self, udp, data): - '''handle UDP as Packet(),payload data - remove this if you want to make UDP into pseudo-sessions''' - pass - - def connectionInitHandler(self, conn): - '''called when connection starts, before any data''' - pass - - def blobHandler(self, conn, blob): - '''handle session data as soon as reassembly is possible''' - pass - - def connectionHandler(self, conn): - '''handle session once all data is reassembled''' - pass - - def connectionCloseHandler(self, conn): - '''called when connection ends, after data is handled''' - -# create an instance at load-time -dObj = DshellDecoder() diff --git a/decoders/voip/sip.py b/decoders/voip/sip.py deleted file mode 100644 index 7850dc1..0000000 --- a/decoders/voip/sip.py +++ /dev/null @@ -1,217 +0,0 @@ -# -# Author: MM - https://github.com/1modm -# -# The Session Initiation Protocol (SIP) is the IETF protocol for VOIP and other text and multimedia sessions -# and is a communications protocol for signaling and controlling. -# SIP is independent from the underlying transport protocol. It runs on the Transmission Control Protocol (TCP), -# the User Datagram Protocol (UDP) or the Stream Control Transmission Protocol (SCTP) -# -# Rate and codec calculation thanks to https://git.ucd.ie/volte-and-of/voip-pcapy -# -# RFC: https://www.ietf.org/rfc/rfc3261.txt -# -# SIP is a text-based protocol with syntax similar to that of HTTP. -# There are two different types of SIP messages: requests and responses. -# - Requests initiate a SIP transaction between two SIP entities for establishing, controlling, and terminating sessions. -# - Responses are send by the user agent server indicating the result of a received request. -# -# - SIP session setup example: -# -# Alice's . . . . . . . . . . . . . . . . . . . . Bob's -# softphone SIP Phone -# | | | | -# | INVITE F1 | | | -# |--------------->| INVITE F2 | | -# | 100 Trying F3 |--------------->| INVITE F4 | -# |<---------------| 100 Trying F5 |--------------->| -# | |<-------------- | 180 Ringing F6 | -# | | 180 Ringing F7 |<---------------| -# | 180 Ringing F8 |<---------------| 200 OK F9 | -# |<---------------| 200 OK F10 |<---------------| -# | 200 OK F11 |<---------------| | -# |<---------------| | | -# | ACK F12 | -# |------------------------------------------------->| -# | Media Session | -# |<================================================>| -# | BYE F13 | -# |<-------------------------------------------------| -# | 200 OK F14 | -# |------------------------------------------------->| -# | | -# - -import dshell -import dpkt -import datetime -import colorout - -class DshellDecoder(dshell.UDPDecoder): - - def __init__(self): - dshell.UDPDecoder.__init__(self, - name='sip', - description='Session Initiation Protocol (SIP) capture decoder', - longdescription=""" -The Session Initiation Protocol (SIP) decoder will extract the Call ID, User agent, Codec, Method, -SIP call, Host, and Client MAC address from every SIP request or response packet found in the given pcap. - -General usage: - decode -d sip - -Detailed usage: - decode -d sip --sip_showpkt - -Layer2 sll usage: - decode -d sip --no-vlan --layer2=sll.SLL - -SIP over TCP: - decode -d sip --bpf 'tcp' - -SIP is a text-based protocol with syntax similar to that of HTTP, so you can use followstream decoder: - decode -d followstream --ebpf 'port 5060' --bpf 'udp' - -Examples: - - https://wiki.wireshark.org/SampleCaptures#SIP_and_RTP - http://vignette3.wikia.nocookie.net/networker/images/f/fb/Sample_SIP_call_with_RTP_in_G711.pcap/revision/latest?cb=20140723121754 - - decode -d sip metasploit-sip-invite-spoof.pcap - decode -d sip Sample_SIP_call_with_RTP_in_G711.pcap - -Output: - - <-- SIP Request --> - Timestamp: 2016-09-21 22:44:28.220185 UTC - Protocol: UDP - Size: 435 bytes - Sequence and Method: 1 ACK - From: 10.5.1.8:5060 (00:20:80:a1:13:db) to 10.5.1.7:5060 (15:2a:01:b4:0f:47) - Via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK940bdac4-8a13-1410-9e58-08002772a6e9;rport - SIP call: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 --> "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce - Call ID: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC - - --> SIP Response <-- - Timestamp: 2016-09-21 22:44:27.849761 UTC - Protocol: UDP - Size: 919 bytes - Sequence and Method: 1 INVITE - From: 10.5.1.7:5060 (02:0a:40:12:30:23) to 10.5.1.8:5060 (d5:02:03:94:31:1b) - Via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 - SIP call: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 --> "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce - Call ID: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC - Codec selected: PCMU - Rate selected: 8000 - -Detailed Output: - - --> SIP Response <-- - Timestamp: 2016-09-21 22:44:25.360974 UTC - Protocol: UDP - Size: 349 bytes - From: 10.5.1.7:5060 (15:2a:01:b4:0f:47) to 10.5.1.8:5060 (00:20:80:a1:13:db) - SIP/2.0 100 Trying - content-length: 0 - via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 - from: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 - to: - cseq: 1 INVITE - call-id: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC - - --> SIP Response <-- - Timestamp: 2016-09-21 22:44:25.387780 UTC - Protocol: UDP - Size: 585 bytes - From: 10.5.1.7:5060 (15:2a:01:b4:0f:47) to 10.5.1.8:5060 (00:20:80:a1:13:db) - SIP/2.0 180 Ringing - content-length: 0 - via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 - from: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 - require: 100rel - rseq: 694867676 - user-agent: Ekiga/4.0.1 - to: "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce - contact: "miguel" - cseq: 1 INVITE - allow: INVITE,ACK,OPTIONS,BYE,CANCEL,SUBSCRIBE,NOTIFY,REFER,MESSAGE,INFO,PING,PRACK - call-id: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC -""", - filter='udp', - author='mm', - optiondict={ - 'showpkt': {'action': 'store_true', 'default': False, 'help': 'Display the full SIP response or request body.'} - } - ) - - self.out = colorout.ColorOutput() - self.rate = None - self.codec = None - self.direction = None - self.output = None - - def preModule(self): - if 'setColorMode' in dir(self.out): - self.out.setColorMode() - - def packetHandler(self, udp, data): - # Initialize - self.output = False - self.rate = str() - self.codec = str() - self.direction = str() - - # Check if exists SIP Request - try: - if dpkt.sip.Request(data): - siptxt = "<-- SIP Request -->" - sippkt = dpkt.sip.Request(data) - self.direction = "sc" - self.output = True - except dpkt.UnpackError, e: - pass - - # Check if exists SIP Response - try: - if dpkt.sip.Response(data): - siptxt = "--> SIP Response <--" - sippkt = dpkt.sip.Response(data) - self.direction = "cs" - self.output = True - except dpkt.UnpackError, e: - pass - - # If a SIP request or SIP response exists, print the results - if self.output: - # Common output - self.out.write("\n{0} \nTimestamp: {1} UTC - Protocol: {2} - Size: {3} bytes\n".format(siptxt, datetime.datetime.utcfromtimestamp( - udp.ts), udp.proto, udp.info()['bytes']), formatTag="H2", direction=self.direction) - self.out.write("From: {0}:{1} ({2}) to {3}:{4} ({5}) \n".format(udp.sip, udp.sport, udp.smac, - udp.dip, udp.dport, udp.dmac), formatTag="H2", direction=self.direction) - - # Show full SIP packet detail - if self.showpkt: - self.out.write("{0}\n".format(sippkt), formatTag="H2", direction=self.direction) - - # Show essential SIP Requests or Responses headers - else: - user_agent = sippkt.headers.get('user-agent') - allow = sippkt.headers.get('allow') - sip_from = sippkt.headers.get('from') - sip_to = sippkt.headers.get('to') - sip_callid = sippkt.headers.get('call-id') - via = sippkt.headers.get('via') - cseq = sippkt.headers.get('cseq') - - if cseq: - self.out.write("Sequence and Method: {0}\n".format(cseq), formatTag="H2", direction=self.direction) - - if via: - self.out.write("Via: {0}\nSIP call: {1} --> {2}\nCall ID: {3}\n".format(via, - sip_from, sip_to, sip_callid), formatTag="H2", direction=self.direction) - - # codec and rate negotiated - for x in range(sippkt.body.find(' ',sippkt.body.find('a='))+1,sippkt.body.find('/',sippkt.body.find('a='))): - self.codec += sippkt.body[x] - for x in range(sippkt.body.find(' ',sippkt.body.find('a='))+6,sippkt.body.find('/',sippkt.body.find('a='))+5): - self.rate +=sippkt.body[x] - - if (self.codec and self.rate): - self.out.write("Codec selected: {0} \nRate selected: {1} \n".format(self.codec, self.rate), formatTag="H2", direction=self.direction) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() \ No newline at end of file diff --git a/dist/Dshell-3.1.3.tar.gz b/dist/Dshell-3.1.3.tar.gz new file mode 100644 index 0000000..b3fc79d Binary files /dev/null and b/dist/Dshell-3.1.3.tar.gz differ diff --git a/doc/UsingDshellWithPyCharm.md b/doc/UsingDshellWithPyCharm.md deleted file mode 100644 index 2e82496..0000000 --- a/doc/UsingDshellWithPyCharm.md +++ /dev/null @@ -1,88 +0,0 @@ -# Using DShell with PyCharm - -This document will outline how Dshell decoders and the framework itself can be setup to allow development and -debugging within the Python IDE, PyCharm. If preferred, the concepts can likely be translated to other IDE’s and debugging environments. - -After first installing all prerequistes mentioned in the [README](../README.md) and then opening the source repository -in Pycharm, the following needs to be done in order for PyCharm to correctly recognize the Dshell source code and executables. - - -## Settings - -### Project Structure -In order to properly use PyCharm with Dshell, we need to tell it where to find the relevant Python files. -Under `File -> Settings -> Project: DShell -> Project Structure` set the folders `bin`, `decoders`, `lib`, and `lib/output` -as sources. Then click OK. After a few seconds, you should now see all the red underlines on imports disappear. - -![](image1.png) - -### Project Interpreter -We also need to tell the Python interpreter where to find the relevant files. This is useful if you want to import -Dshell modules within the interpreter. Under `File -> Settings -> Project: DShell -> Project Interpreter` click the -gear icon next to the dropdown with the project interpreter you are going to use, and then select `More…` - -![](image2.png) - -Then with your current Python interpreter highlighted, click the small folder tree icon (below the filter icon) to see a -window of `Interpreter Paths`. In this window click the `+` icon to the right and add the four folders added earlier -(`bin`, `decoders`, `lib`, and `lib/output`). - -![](image3.png) - -You should now be able to import decoders and other Dshell modules using the built in Python interpreter. - -![](image4.png) - - -## Run Configurations -Run configurations in PyCharm allow a user to specify how to run tasks and programs. In the context of Dshell, a -configuration can be setup to specify which decoder to run, how to setup the environment variables needed by Dshell, and what working directory to use. - -### Opening Configurations -The first step within PyCharm is to open up the interface to edit configurations. This can be done through the toolbar as -shown below or by using the menu to navigate to `Run -> Edit Configurations…` - -![](image5.png) - -### Adding a New Python Configuration for Dshell -Once the configurations interface is open, the next step is to add a new configuration. Since Dshell utilizes Python, a -Python configuration needs to be created. Select the `+` symbol near the top left and select `Python` in the drop down menu. - -![](image6.png) - -### Specifying the Configuration Details -Now that the configuration window is open, the following fields need to be populated: - -- Script: The full path to the `decode.py` script in Dshell. -- Script parameters: The parameters that would be passed in when running the decode command. -- Environment variables: The environment variables found in the `.dshellrc` file in Dshell’s root directory. This is detailed more below. -- Working directory: If all the settings use absolute paths, the working directory can be set to anything. Setting the working directory to a location where output files should be placed is a reasonable option. - -![](image7.png) - -### Specifying the Environment Variables -The environment variables in the configuration are important to mirror the Dshell shell. - -When first running Dshell, some of the first steps are to build the project using make and to run the "dshell" script. -The build step is relevant here because it produces the `.dshellrc` file that the "dshell" script depends on to configure the shell’s environment. - -For the PyCharm configuration, open up the `.dshellrc` file in Dshell’s directory and take a look at the environment -variables. Use these values to set the `Environment variables` field in the configuration. - -![](image8.png) - -### Save the Configuration -Now that the configuration has been setup, just give it a name and hit OK. - - -## Debugging - -### Select Your Configuration -Before debugging, make sure the configuration you want is selected. - -![](image9.png) - -### Start Debugging -Once the configuration is set, make sure to set any breakpoints. Then, start up the debugger. Further details on how to use the debugger can be found online. - -![](image10.png) diff --git a/doc/generate-doc.sh b/doc/generate-doc.sh deleted file mode 100755 index 878fd98..0000000 --- a/doc/generate-doc.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -d=`pwd` -if [ "$1" ]; then d=$1; fi -source $d/.dshellrc || exit - -for f in $d/lib/*.py $d/lib/output/*.py $d/bin/*.py; do - pydoc -w `basename $f|cut -d. -f1` -done - -for f in `find $d/decoders -name \*.py -not -name __init__.py`; do - pydoc -w $f -done diff --git a/doc/image1.png b/doc/image1.png deleted file mode 100644 index a273304..0000000 Binary files a/doc/image1.png and /dev/null differ diff --git a/doc/image10.png b/doc/image10.png deleted file mode 100644 index 0ff29dc..0000000 Binary files a/doc/image10.png and /dev/null differ diff --git a/doc/image2.png b/doc/image2.png deleted file mode 100644 index e15f57c..0000000 Binary files a/doc/image2.png and /dev/null differ diff --git a/doc/image3.png b/doc/image3.png deleted file mode 100644 index 23a9f58..0000000 Binary files a/doc/image3.png and /dev/null differ diff --git a/doc/image4.png b/doc/image4.png deleted file mode 100644 index c4dbc18..0000000 Binary files a/doc/image4.png and /dev/null differ diff --git a/doc/image5.png b/doc/image5.png deleted file mode 100644 index a9b02f9..0000000 Binary files a/doc/image5.png and /dev/null differ diff --git a/doc/image6.png b/doc/image6.png deleted file mode 100644 index 23c6a5a..0000000 Binary files a/doc/image6.png and /dev/null differ diff --git a/doc/image7.png b/doc/image7.png deleted file mode 100644 index 25dfbf8..0000000 Binary files a/doc/image7.png and /dev/null differ diff --git a/doc/image8.png b/doc/image8.png deleted file mode 100644 index cb2ec6f..0000000 Binary files a/doc/image8.png and /dev/null differ diff --git a/doc/image9.png b/doc/image9.png deleted file mode 100644 index fe4c9a6..0000000 Binary files a/doc/image9.png and /dev/null differ diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index f41132f..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,36 +0,0 @@ -FROM ubuntu:18.04 - -ENV DEBIAN_FRONTEND="noninteractive" - -# install depdencies -RUN apt-get update && apt-get install -y \ - python-crypto \ - python-dpkt \ - python-ipy \ - python-pypcap \ - python-pip \ - python-geoip2 \ - wget \ - git - -# Download the latest version of the code from GitHub -RUN git -C /opt clone https://github.com/USArmyResearchLab/Dshell.git - -# download and untar GeoIP files -WORKDIR /opt/Dshell/share/GeoIP/ -RUN wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz \ - && wget https://geolite.maxmind.com/download/geoip/database/GeoLite2-ASN.tar.gz \ - && tar -zxf GeoLite2-Country.tar.gz \ - && tar -zxf GeoLite2-ASN.tar.gz \ - && ln -s GeoLite2-Country*/GeoLite2-Country.mmdb . \ - && ln -s GeoLite2-ASN*/GeoLite2-ASN.mmdb . \ - && rm -rf /var/lib/apt/lists/* - -# make Dshell -WORKDIR /opt/Dshell/ -RUN make - -# Used to mount pcap from a host OS directory -VOLUME ["/mnt/pcap"] - -ENTRYPOINT ["/opt/Dshell/dshell"] diff --git a/docker/README.md b/docker/README.md deleted file mode 100644 index 5cf8cd6..0000000 --- a/docker/README.md +++ /dev/null @@ -1,16 +0,0 @@ -## Building a Dshell Docker image - -Step 1: Build a Docker image that has Dshell installed and configured -```bash -sudo docker build -t dshell . -``` - -Step 2: Run the container with a native host directory (/home/user/pcap/) mounted in /mnt/pcap -```bash -sudo docker run -v /home/user/pcap:/mnt/pcap -it dshell -``` - -Step 3: Use Dshell to analyze network traffic -```bash -decode -d netflow /mnt/pcap/*.pcap -``` diff --git a/dshell/__init__.py b/dshell/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/core.py b/dshell/core.py new file mode 100644 index 0000000..bc78a9e --- /dev/null +++ b/dshell/core.py @@ -0,0 +1,1448 @@ +""" +The core Dshell library + +This library contains the base level plugins that all others will inherit. + +PacketPlugin contains attributes and functions for plugins that work with +individual packets. + +ConnectionPlugin inherits from PacketPlugin and includes additional functions +for handling reassembled connections. + +It also contains class definitions used by the plugins, including definitions +for Blob, Connection, and Packet. + +""" + +# standard Python imports +import datetime +import inspect +import ipaddress +import logging +import os +#import pprint +import struct +from collections import defaultdict +from multiprocessing import Value + +# Dshell imports +from dshell.output.output import Output +from dshell.dshellgeoip import DshellGeoIP, DshellFailedGeoIP + +# third-party imports +import pcapy +from pypacker.layer12 import can, ethernet, ieee80211, linuxcc, ppp, pppoe, radiotap +from pypacker.layer3 import ip, ip6, icmp, icmp6 +from pypacker.layer4 import tcp, udp + +logging.basicConfig(format="%(levelname)s (%(name)s) - %(message)s") +logger = logging.getLogger("dshell.core") + +__version__ = "1.1" + +class SequenceNumberError(Exception): + """ + Raised when reassembling connections and data is missing or overlapping. + See Blob.reassemble function + """ + pass + +class DataError(Exception): + """ + Raised when any data being handled just isn't right. + For example, invalid headers in httpplugin.py + """ + pass + + +# Create GeoIP refrence object +try: + geoip = DshellGeoIP(logger=logging.getLogger("dshellgeoip.py")) +except FileNotFoundError: + logger.error("Could not find GeoIP data files! Country and ASN lookups will not be possible. Check README for instructions on where to find and install necessary data files.") + geoip = DshellFailedGeoIP() + + +def print_handler_exception(e, plugin, handler): + """ + A convenience function to display an error message when a handler raises + an exception. + + If using --debug, it will print a full traceback. + + Args: + e: the exception object + plugin: the plugin object + handler: name of the handler function + """ + etype = e.__class__.__name__ + if logger.isEnabledFor(logging.DEBUG): + logger.error("The {!s} for the {!r} plugin raised an exception and failed! ({}: {!s})".format(handler, plugin.name, etype, e)) + logger.exception(e) + else: + logger.error("The {!s} for the {!r} plugin raised an exception and failed! ({}: {!s}) Use --debug for more details.".format(handler, plugin.name, etype, e)) + + +class PacketPlugin(object): + """ + Base level class that plugins will inherit. + + This plugin handles individual packets. To handle reconstructed + connections, use the ConnectionPlugin. + + Attributes: + name: the name of the plugin + description: short description of the plugin (used with decode -l) + longdescription: verbose description of the plugin (used with -h) + bpf: default BPF to apply to traffic entering plugin + compiled_bpf: a compiled BPF for pcapy, usually created in bin/decode + vlan_bpf: boolean that tells whether BPF should be compiled with + VLAN support + author: preferably, the initials of the plugin's author + seen_packet_count: number of packets this plugin has seen + handled_packet_count: number of packets this plugin has passed + through a handler function + seen_conn_count: number of connections this plugin has seen + handled_conn_count: number of connections this plugin has passed + through a handler function + out: output module instance + raw_decoder: pypacker module to use for unpacking packet + link_layer_type: numeric label for link layer + striplayers: number of layers to automatically strip before handling + (such as PPPoE, IP-over-IP, etc.) + defrag_ip: rebuild fragmented IP packets (default: True) + """ + + IP_PROTOCOL_MAP = dict((v, k[9:]) for k, v in ip.__dict__.items() if type(v) == int and k.startswith('IP_PROTO_') and k != 'IP_PROTO_HOPOPTS') + + def __init__(self, **kwargs): + self.name = kwargs.get('name', __name__) + self.description = kwargs.get('description', '') + self.longdescription = kwargs.get('longdescription', self.description) + self.bpf = kwargs.get('bpf', '') + self.compiled_bpf = kwargs.get('compiled_bpf', None) + self.vlan_bpf = kwargs.get("vlan_bpf", True) + self.author = kwargs.get('author', '') + # define overall counts as multiprocessing Values for --parallel + self.seen_packet_count = Value('i', 0) + self.handled_packet_count = Value('i', 0) + self.seen_conn_count = Value('i', 0) + self.handled_conn_count = Value('i', 0) + # dict of options specific to this plugin in format + # 'optname':{configdict} translates to --pluginname_optname + self.optiondict = kwargs.get('optiondict', {}) + + # queues used by decode.py + # if a handler decides a packet is worth keeping, it is placed in a + # queue and later grabbed by decode.py to pass to subplugins + self.raw_packet_queue = [] + self.packet_queue = [] + + # self.out holds the output plugin instance + # can be overwritten in decode.py by user selection + self.out = kwargs.get('output', Output(label=__name__)) + + # capture options + # these can be updated with set_link_layer_type function + self.raw_decoder = ethernet.Ethernet # assumed link-layer type + self.link_layer_type = 1 # assume Ethernet + # strip extra layers before IP/IPv6? (such as PPPoE, IP-over-IP, etc..) + self.striplayers = 0 + # rebuild fragmented IP packets + self.defrag_ip = True + + # holder for the pcap file being processing + self.current_pcap_file = None + + # get the list of functions for this plugin + # this is used in decode.py + self.members = tuple([x[0] for x in inspect.getmembers(self, inspect.ismethod)]) + + # a holder for IP packet fragments when attempting to reassemble them + self.packet_fragments = defaultdict(dict) + + def write(self, *args, **kwargs): + """ + Sends information to the output formatter, after adding some + additional fields. + """ + if 'plugin' not in kwargs: + kwargs['plugin'] = self.name + if 'pcapfile' not in kwargs: + kwargs['pcapfile'] = self.current_pcap_file + self.out.write(*args, **kwargs) + + def log(self, msg, level=logging.INFO): + ''' + logs msg argument at specified level + (default of INFO is for -v/--verbose output) + + Arguments: + msg: text string to log + level: logging level (default: logging.INFO) + ''' + self.out.log(msg, level=level) + + def debug(self, msg): + '''logs msg argument at debug level''' + self.log(msg, level=logging.DEBUG) + + def warn(self, msg): + '''logs msg argument at warning level''' + self.log(msg, level=logging.WARN) + + def error(self, msg): + '''logs msg argument at error level''' + self.log(msg, level=logging.ERROR) + + def __str__(self): + return "<{}: {}>".format("Plugin", self.name) + + def __repr__(self): + return '<{}: {}/{}/{}>'.format("Plugin", self.name, self.bpf, + ','.join([('%s=%s' % (x, str(self.__dict__.get(x)))) for x in self.optiondict])) + + def set_link_layer_type(self, datalink): + """ + Attempts to set the raw_decoder attribute based on the capture file's + datalink type, which is fetched by pcapy when used in decode.py. It + takes one argument: the numeric value of the link layer. + + http://www.tcpdump.org/linktypes.html + """ + # NOTE: Not all of these have been tested + # TODO add some more of these + self.link_layer_type = datalink + if datalink == 1: + self.raw_decoder = ethernet.Ethernet + elif datalink == 9: + self.raw_decoder = ppp.PPP + elif datalink == 51: + self.raw_decoder = pppoe.PPPoE + elif datalink == 105: + self.raw_decoder = ieee80211.IEEE80211 + elif datalink == 113: + self.raw_decoder = linuxcc.LinuxCC + elif datalink == 127: + self.raw_decoder = radiotap.Radiotap + elif datalink == 204: + self.raw_decoder = ppp.PPP + elif datalink == 227: + self.raw_decoder = can.CAN + elif datalink == 228: + self.raw_decoder = ip.IP + elif datalink == 229: + self.raw_decoder = ip6.IP6 + else: + # by default, assume Ethernet and hope for the best + self.link_layer_type = 1 + self.raw_decoder = ethernet.Ethernet + self.debug("Datalink input: {!s}. Setting raw_decoder to {!r}, link_layer_type to {!s}".format(datalink, self.raw_decoder, self.link_layer_type)) + + def recompile_bpf(self): + "Compile the BPF stored in the .bpf attribute" + # This function is normally only called by the bin/decode.py script, + # but can also be called by plugins that need to dynamically update + # their filter. + if not self.bpf: + logger.debug("Cannot compile BPF: .bpf attribute not set for plugin {!r}.".format(self.name)) + self.compiled_bpf = None + return + + # Add VLAN wrapper, if necessary + if self.vlan_bpf: + bpf = "({0}) or (vlan and {0})".format(self.bpf) + else: + bpf = self.bpf + self.debug("Compiling BPF as {!r}".format(bpf)) + + # Compile BPF and handle any expected errors + try: + self.compiled_bpf = pcapy.compile( + self.link_layer_type, 65536, bpf, True, 0xffffffff + ) + except pcapy.PcapError as e: + if str(e).startswith("no VLAN support for data link type"): + logger.error("Cannot use VLAN filters for {!r} plugin. Recommend running with --no-vlan argument.".format(self.name)) + elif str(e) == "syntax error": + logger.error("Fatal error when compiling BPF: {!r}".format(bpf)) + sys.exit(1) + else: + raise e + + def ipdefrag(self, pkt): + "IP fragment reassembly" + if isinstance(pkt, ip.IP): # IPv4 + f = self.packet_fragments[(pkt.src, pkt.dst, pkt.id)] + f[pkt.offset] = pkt + + if not pkt.flags & 0x1: + data = b'' + for key in sorted(f.keys()): + data += f[key].body_bytes + del self.packet_fragments[(pkt.src, pkt.dst, pkt.id)] + newpkt = ip.IP(pkt.header_bytes + data) + newpkt.bin(update_auto_fields=True) # refresh checksum + return newpkt + + elif isinstance(pkt, ip6.IP6): # IPv6 + # TODO handle IPv6 offsets https://en.wikipedia.org/wiki/IPv6_packet#Fragment + return pkt + + def handle_plugin_options(self): + """ + A placeholder. + + This function is called immediately after plugin args are processed + and set in decode.py. A plugin can overwrite this function to perform + actions based on the arg values as soon as they are set, before + decoder.py does any further processing (e.g. updating a BPF based on + provided arguments before handling --ebpf and --bpf flags). + """ + pass + + def _premodule(self): + """ + _premodule is called before capture starts or files are read. It will + attempt to call the child plugin's premodule function. + """ + self.premodule() + self.out.setup() +# self.debug('{}'.format(pprint.pformat(self.__dict__))) + self.debug(str(self.__dict__)) + + def premodule(self): + """ + A placeholder. + + A plugin can overwrite this function to perform an action before + capture starts or files are read. + """ + pass + + def _postmodule(self): + """ + _postmodule is called when capture ends. It will attempt to call the + child plugin's postmodule function. It will also print stats if in + debug mode. + """ + self.postmodule() + self.out.close() + self.log("{} seen packets, {} handled packets, {} seen connections, {} handled connections".format(self.seen_packet_count.value, self.handled_packet_count.value, self.seen_conn_count.value, self.handled_conn_count.value)) + + def postmodule(self): + """ + A placeholder. + + A plugin can overwrite this function to perform an action after + capture ends or all files are processed. + """ + pass + + def _prefile(self, infile=None): + """ + _prefile is called just before an individual file is processed. + Stores the current pcap file string and calls the child plugin's + prefile function. + """ + self.current_pcap_file = infile + self.prefile(infile) + self.log('working on file "{}"'.format(infile)) + + def prefile(self, infile=None): + """ + A placeholder. + + A plugin will be able to overwrite this function to perform an action + before an individual file is processed. + + Arguments: + infile: filepath or interface that will be processed + """ + pass + + def _postfile(self): + """ + _postfile is called just after an individual file is processed. + It may expand some day, but for now it just calls a child's postfile + function. + """ + self.postfile() + + def postfile(self): + """ + A placeholder. + + A plugin will be able to overwrite this function to perform an action + after an individual file is processed. + """ + pass + + def _raw_handler(self, pktlen, pkt, ts): + """ + Accepts raw packet data (pktlen, pkt, ts), and handles decapsulation + and layer stripping. + + Then, it passes the massaged data to the child's raw_handler function, + if additional custom handling is necessary. The raw_handler function + should return (pktlen, pkt, ts) if it wishes to continue with the call + chain. Otherwise, return None. + """ +# with self.seen_packet_count.get_lock(): +# self.seen_packet_count.value += 1 +# +# # call raw_handler and check its output +# # decode.py will continue down the chain if it returns proper output or +# # display a warning if it doesn't return the correct things +# try: +# raw_handler_out = self.raw_handler(pktlen, pkt, ts) +# except Exception as e: +# print_handler_exception(e, self, 'raw_handler') +# return +# +# failed_msg = "The output of {} raw_handler must be (pktlen, pkt, ts) or a list of such lists! Further packet refinement and plugin chaining will not be possible".format(self.name) +# if raw_handler_out and isinstance(raw_handler_out, (list, tuple)): +# self.warn(failed_msg) +# return + + with self.seen_packet_count.get_lock(): + self.seen_packet_count.value += 1 + # decode with the raw decoder (probably ethernet.Ethernet) + pkt = self.raw_decoder(pkt) + + # strip any intermediate layers (e.g. PPPoE, etc.) + # NOTE: make sure only the first plugin in a chain has striplayers set + for _ in range(self.striplayers): + try: + pkt = pkt.upper_layer + except AttributeError: + # No more layers to strip + break + + # call raw_handler and check its output + # decode.py will continue down the chain if it returns proper output or + # display a warning if it doesn't return the correct things + try: + raw_handler_out = self.raw_handler(pktlen, pkt, ts) + except Exception as e: + print_handler_exception(e, self, 'raw_handler') + return + failed_msg = "The output of {} raw_handler must be (pktlen, pkt, ts) or a list of such lists! Further packet refinement and plugin chaining will not be possible".format(self.name) + if isinstance(raw_handler_out, (list, tuple)): + if len(raw_handler_out) == 3 and ( + isinstance(raw_handler_out[0], type(pktlen)) and + isinstance(raw_handler_out[1], type(pkt)) and + isinstance(raw_handler_out[2], type(ts))): + # If it returns one properly formed response, queue and continue + self.raw_packet_queue.append(raw_handler_out) + else: + # If it returns several responses, check them individually + for rhout in raw_handler_out: + if isinstance(rhout, (list, tuple)) and \ + len(rhout) == 3 and \ + isinstance(rhout[0], type(pktlen)) and \ + isinstance(rhout[1], type(pkt)) and \ + isinstance(rhout[2], type(ts)): + self.raw_packet_queue.append(rhout) + elif rhout: + self.warn(failed_msg) + elif raw_handler_out: + self.warn(failed_msg) + + + def raw_handler(self, pktlen, pkt, ts): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + raw packet data, such as decapsulation or decryption, before it + becomes further refined down the chain. It should return the same + arguments: pktlen, pkt, ts + + Generally speaking, however, this should never be overwritten unless + there is a very, very good reason for it. + + Arguments: + pktlen: length of packet + pkt: raw bytes of the packet + ts: timestamp of packet + """ + return pktlen, pkt, ts + + def _packet_handler(self, pktlen, pkt, ts): + """ + Accepts the output of raw_handler, pulls out addresses, and converts + it all into a dshell.Packet object before calling the child's + packet_handler function. + """ + # Attempt to perform defragmentation + if isinstance(pkt.upper_layer, (ip.IP, ip6.IP6)): + ipp = pkt.upper_layer + if self.defrag_ip: + ipp = self.ipdefrag(ipp) + if not ipp: + # we do not yet have all of the packet fragments, so move + # on to next packet for now + return + else: + pkt.upper_layer = ipp + + # Initialize a Packet object + # This will be populated with values as we continue through + # the function and eventually be passed to packet_handler + packet = Packet(self, pktlen, pkt, ts) + + # call packet_handler and return its output + # decode.py will continue down the chain if it returns anything + try: + packet_handler_out = self.packet_handler(packet) + except Exception as e: + print_handler_exception(e, self, 'packet_handler') + return + failed_msg = "The output from {} packet_handler must be of type dshell.Packet or a list of such objects! Handling connections or chaining from this plugin may not be possible.".format(self.name) + if isinstance(packet_handler_out, (list, tuple)): + for phout in packet_handler_out: + if isinstance(phout, Packet): + self.packet_queue.append(phout) + with self.handled_packet_count.get_lock(): + self.handled_packet_count.value += 1 + elif phout: + self.warn(failed_msg) + elif isinstance(packet_handler_out, Packet): + self.packet_queue.append(packet_handler_out) + with self.handled_packet_count.get_lock(): + self.handled_packet_count.value += 1 + elif packet_handler_out: + self.warn(failed_msg) + + + def packet_handler(self, pkt): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + Packet data. + + It should return a Packet object for functions further down the chain + (i.e. connection_handler and/or blob_handler) + + Arguments: + pkt: a Packet object + """ + return pkt + + + +class ConnectionPlugin(PacketPlugin): + """ + Base level class that plugins will inherit. + + This plugin reassembles connections from packets. + """ + + def __init__(self, **kwargs): + PacketPlugin.__init__(self, **kwargs) + + # similar to packet_queue and raw_packet_queue in superclass + self.connection_queue = [] + + # dictionary to store packets for connections according to addr() + self.connection_tracker = {} + # maximum number of blobs a connection will store before calling + # connection_handler + # it defaults to infinite, but this should be lowered for huge datasets + self.maxblobs = float("inf") # infinite + # how long do we wait before deciding a connection is "finished" + # time is checked by iterating over cached connections and checking if + # the timestamp of the connection's last packet is older than the + # timestamp of the current packet, minus this value + self.connection_timeout = datetime.timedelta(hours=1) + + def _connection_handler(self, pkt): + """ + Accepts a single Packet object and tracks the connection it belongs to. + + If it is the first packet in a connection, it creates a new Connection + object and passes it to connection_init_handler. Otherwise, it will + find the existing Connection in self.connection_tracker. + + The Connection will then be passed to connection_handler. + + If a connection changes direction with this packet, blob_handler will + be called. + + Finally, if this packet is a FIN or RST, it will determine if the + connection should close. + """ + # Sort the addr value for consistent dictionary key purposes + addr = tuple(sorted(pkt.addr)) + + # If this is a new connection, initialize it and call the init handler + if addr not in self.connection_tracker: + conn = Connection(self, pkt) + self.connection_tracker[addr] = conn + try: + self.connection_init_handler(conn) + except Exception as e: + print_handler_exception(e, self, 'connection_init_handler') + return + with self.seen_conn_count.get_lock(): + self.seen_conn_count.value += 1 + else: + conn = self.connection_tracker[addr] + + if conn.stop: + # This connection was flagged to not be tracked + return + + # If connection data is about to change, we set it to a "dirty" state + # for future calls to connection_handler + if pkt.data: + conn.handled = False + + # Check and update the connection's current state + if pkt.tcp_flags in (tcp.TH_SYN, tcp.TH_ACK, tcp.TH_SYN|tcp.TH_ACK, tcp.TH_SYN|tcp.TH_ACK|tcp.TH_ECE): + # if new connection and a handshake is taking place, set to "init" + if not conn.client_state: + conn.client_state = "init" + if not conn.server_state: + conn.server_state = "init" + else: + # otherwise, if the connection isn't closed, set to "established" + # TODO do we care about "listen", "syn-sent", and other in-between states? + if conn.client_state not in ('finishing', 'closed'): + conn.client_state = "established" + if conn.server_state not in ('finishing', 'closed'): + conn.server_state = "established" + + # Add the packet to the connection + # If the direction changed, a Blob will be returned for handling + # Note: The Blob will not be reassembled ahead of time. reassemble() + # must be run inside the blob_handler to catch any unwanted exceptions. + previous_blob = conn.add_packet(pkt) + if previous_blob: + try: + blob_handler_out = self._blob_handler(conn, previous_blob) + except Exception as e: + print_handler_exception(e, self, 'blob_handler') + return + if (blob_handler_out + and not isinstance(blob_handler_out[0], Connection) + and not isinstance(blob_handler_out[1], Blob)): + self.warn("The output from {} blob_handler must be of type (dshell.Connection, dshell.Blob)! Chaining plugins from here may not be possible.".format(self.name)) + blob_handler_out = None + # If the blob_handler decides this Blob isn't interesting, it sets + # the hidden flag, which excludes it and its packets from further + # processing along the plugin chain + if not blob_handler_out: + conn.blobs[-2].hidden = True + + # Check if a side of the connection is attempting to close the + # connection using a FIN or RST packet. Once both sides make a + # closing gesture, the connection is considered closed and handled + if pkt.tcp_flags and pkt.tcp_flags & (tcp.TH_RST | tcp.TH_FIN): + if pkt.sip == conn.clientip: + conn.client_state = "closed" + else: + conn.server_state = "closed" + + if conn.connection_closed: + # Both sides have closed the connection + self._close_connection(conn, full=True) + + elif len(conn.blobs) > self.maxblobs: + # Max blobs hit, so we will run connection_handler and decode.py + # will clear the connection's blob cache + self._close_connection(conn) + + # The current connection is done processing. Now, look over existing + # connections and look for any that have timed out. + # This is based on comparing the time of the current packet, minus + # self.connection_timeout, to each connection's current endtime value. + for addr, conn in self.connection_tracker.items(): + if conn.handled: + continue + if conn.endtime < (pkt.dt - self.connection_timeout): + self._close_connection(conn) + + + def _close_connection(self, conn, full=False): + """ + Runs through some standard actions to close a connection + """ + try: + connection_handler_out = self.connection_handler(conn) + except Exception as e: + print_handler_exception(e, self, 'connection_handler') + return None + conn.handled = True + if connection_handler_out and not isinstance(connection_handler_out, Connection): + self.warn("The output from {} connection_handler must be of type dshell.Connection! Chaining plugins from here may not be possible.".format(self.name)) + connection_handler_out = None + if connection_handler_out: + self.connection_queue.append(connection_handler_out) + with self.handled_conn_count.get_lock(): + self.handled_conn_count.value += 1 + if full: + try: + self.connection_close_handler(conn) + except Exception as e: + print_handler_exception(e, self, 'connection_close_handler') + return connection_handler_out + + + def _cleanup_connections(self): + """ + decode.py will often reach the end of packet capture before all of the + connections are closed properly. This function is called at the end + of things to process those dangling connections. + + NOTE: Because the connections did not close cleanly, + connection_close_handler will not be called. + """ + for addr, conn in self.connection_tracker.items(): + if not conn.stop and not conn.handled: + # try to process the final blob in the connection + try: + blob_handler_out = self._blob_handler(conn, conn.blobs[-1]) + except Exception as e: + print_handler_exception(e, self, 'blob_handler') + blob_handler_out = None + if (blob_handler_out + and not isinstance(blob_handler_out[0], Connection) + and not isinstance(blob_handler_out[1], Blob)): + self.warn("The output from {} blob_handler must be of type (dshell.Connection, dshell.Blob)! Chaining plugins from here may not be possible.".format(self.name)) + blob_handler_out = None + if not blob_handler_out: + conn.blobs[-1].hidden = True + + # then, handle the connection itself + connection_handler_out = self._close_connection(conn) + yield connection_handler_out + + def _purge_connections(self): + """ + When finished with handling a pcap file, calling this will clear all + caches in preparation for next file. + """ + self.connection_queue = [] + self.connection_tracker = {} + + def _blob_handler(self, conn, blob): + """ + Accepts a Connection and a Blob. + + It doesn't really do anything except call the blob_handler and is only + here for consistency and possible future features. + """ + return self.blob_handler(conn, blob) + + def blob_handler(self, conn, blob): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + Blob data. + + It should return a Connection object and a Blob object for functions + further down the chain. + + Args: + conn: Connection object + blob: Blob object + """ + return conn, blob + + def connection_init_handler(self, conn): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + a connection it is first seen. + + Args: + conn: Connection object + """ + return + + def connection_handler(self, conn): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + Connection data. + + It should return a Connection object for functions further down the chain + + Args: + conn: Connection object + """ + return conn + + def connection_close_handler(self, conn): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites on + a TCP connection when it is cleanly closed with RST or FIN. + + Args: + conn: Connection object + """ + return + +class Packet(object): + """ + Class for holding data of individual packets + + def __init__(self, plugin, pktlen, pkt, ts): + + Args: + plugin: an instance of the plugin creating this packet + pktlen: length of packet + pkt: pypacker object for the packet + ts: timestamp of packet + + Attributes: + plugin: name of plugin creating Packet + ts: timestamp of packet + dt: datetime of packet + pkt: pypacker object for the packet + rawpkt: raw bytestring of the packet + pktlen: length of packet + byte_count: length of packet body + sip: source IP + dip: destination IP + sip_bytes: source IP as bytes + dip_bytes: destination IP as bytes + sport: source port + dport: destination port + smac: source MAC + dmac: destination MAC + sipcc: source IP country code + dipcc: dest IP country code + siplat: source IP latitude + diplat: dest IP latitude + siplon: source IP longitude + diplon: dest IP longitude + sipasn: source IP ASN + dipasn: dest IP ASN + protocol: text version of protocol in layer-3 header + protocol_num: numeric version of protocol in layer-3 header + data: data of the packet after TCP layer, or highest layer + sequence_number: TCP sequence number, or None + ack_number: TCP ACK number, or None + tcp_flags: TCP header flags, or None + """ + + def __init__(self, plugin, pktlen, pkt, ts): + self.plugin = plugin.name + self.ts = ts + self.dt = datetime.datetime.fromtimestamp(ts) + self.pkt = pkt + self.rawpkt = pkt.bin() + self.pktlen = pktlen + self.byte_count = None + self.sip = None + self.dip = None + self.sport = None + self.dport = None + self.smac = None + self.dmac = None + self.sipcc = None + self.dipcc = None + self.siplat = None + self.diplat = None + self.siplon = None + self.diplon = None + self.sipasn = None + self.dipasn = None + self.protocol = None + self.protocol_num = None + self.data = b'' + self.sequence_number = None + self.ack_number = None + self.tcp_flags = None + + # these are the layers Dshell will help parse + # try to find them in the packet and eventually pull out useful data + ethernet_p = None + ieee80211_p = None + ip_p = None + tcp_p = None + udp_p = None + current_layer = pkt + while current_layer: + if isinstance(current_layer, ethernet.Ethernet) and not ethernet_p: + ethernet_p = current_layer + elif isinstance(current_layer, ieee80211.IEEE80211) and not ieee80211_p: + ieee80211_p = current_layer + elif isinstance(current_layer, (ip.IP, ip6.IP6)) and not ip_p: + ip_p = current_layer + elif isinstance(current_layer, tcp.TCP) and not tcp_p: + tcp_p = current_layer + elif isinstance(current_layer, udp.UDP) and not udp_p: + udp_p = current_layer + try: + current_layer = current_layer.upper_layer + except AttributeError: + break + + # attempt to grab MAC addresses + if ethernet_p: + # from Ethernet + self.smac = ethernet_p.src_s + self.dmac = ethernet_p.dst_s + elif ieee80211_p: + # from 802.11 + try: + if ieee80211_p.subtype == ieee80211.M_BEACON: + ieee80211_p2 = ieee80211_p.beacon + elif ieee80211_p.subtype == ieee80211.M_DISASSOC: + ieee80211_p2 = ieee80211_p.disassoc + elif ieee80211_p.subtype == ieee80211.M_AUTH: + ieee80211_p2 = ieee80211_p.auth + elif ieee80211_p.subtype == ieee80211.M_DEAUTH: + ieee80211_p2 = ieee80211_p.deauth + elif ieee80211_p.subtype == ieee80211.M_ACTION: + ieee80211_p2 = ieee80211_p.action + else: + # can't figure out how pypacker stores the other subtypes + raise AttributeError + self.smac = ieee80211_p2.src_s + self.dmac = ieee80211_p2.dst_s + except AttributeError as e: + pass + + # process IP addresses and associated metadata (if applicable) + if ip_p: + # get IP addresses + sip = ipaddress.ip_address(ip_p.src) + dip = ipaddress.ip_address(ip_p.dst) + self.sip = sip.compressed + self.dip = dip.compressed + self.sip_bytes = sip.packed + self.dip_bytes = dip.packed + + # get protocols, country codes, and ASNs + self.protocol_num = ip_p.p if isinstance(ip_p, ip.IP) else ip_p.nxt + self.protocol = PacketPlugin.IP_PROTOCOL_MAP.get(self.protocol_num, str(self.protocol_num)) + self.sipcc, self.siplat, self.siplon = geoip.geoip_location_lookup(self.sip) + self.sipasn = geoip.geoip_asn_lookup(self.sip) + self.dipcc, self.diplat, self.diplon = geoip.geoip_location_lookup(self.dip) + self.dipasn = geoip.geoip_asn_lookup(self.dip) + + if tcp_p: + self.sport = tcp_p.sport + self.dport = tcp_p.dport + self.sequence_number = tcp_p.seq + self.ack_number = tcp_p.ack + self.tcp_flags = tcp_p.flags + self.data = tcp_p.body_bytes + + elif udp_p: + self.sport = udp_p.sport + self.dport = udp_p.dport + self.data = udp_p.body_bytes + + else: + self.data = pkt.highest_layer.body_bytes + + self.byte_count = len(self.data) + + + + @property + def addr(self): + """ + A standard representation of the address: + ((self.sip, self.sport), (self.dip, self.dport)) + or + ((self.smac, self.sport), (self.dmac, self.dport)) + """ + # try using IP addresses first + if self.sip or self.dip: + return ((self.sip, self.sport), (self.dip, self.dport)) + # then try MAC addresses + elif self.smac or self.dmac: + return ((self.smac, self.sport), (self.dmac, self.dport)) + # if all else fails, return Nones + else: + return ((None, None), (None, None)) + + @property + def packet_tuple(self): + """ + A standard representation of the raw packet tuple: + (self.pktlen, self.rawpkt, self.ts) + """ + return (self.pktlen, self.rawpkt, self.ts) + + def __repr__(self): + return "%s %16s :%-5s -> %5s :%-5s (%s -> %s)" % (self.dt, self.sip, self.sport, self.dip, self.dport, self.sipcc, self.dipcc) + + def info(self): + """ + Provides a dictionary with information about a packet. Useful for + calls to a plugin's write() function, e.g. self.write(\\*\\*pkt.info()) + """ + d = dict(self.__dict__) + del d['pkt'] + del d['rawpkt'] + del d['data'] + return d + + +class Connection(object): + """ + Class for holding data about connections + + def __init__(self, plugin, first_packet) + + Args: + plugin: an instance of the plugin creating this connection + first_packet: the first Packet object to initialize connection + + Attributes: + plugin: name of the plugin that created object + addr: .addr attribute of first packet + sip: source IP + smac: source MAC address + sport: source port + sipcc: country code of source IP + siplat: latitude of source IP + siplon: longitude of source IP + sipasn: ASN of source IP + clientip: same as sip + clientmac: same as smac + clientport: same as sport + clientcc: same as sipcc + clientlat: same as siplat + clientlon: same as siplon + clientasn: same as sipasn + dip: dest IP + dmac: dest MAC address + dport: dest port + dipcc: country code of dest IP + diplat: latitude of dest IP + diplon: longitude of dest IP + dipasn: ASN of dest IP + serverip: same as dip + servermac: same as dmac + serverport: same as dport + servercc: same as dipcc + serverlat: same as diplat + serverlon: same as diplon + serverasn: same as dipasn + protocol: text version of protocol in layer-3 header + clientpackets: counts of packets from client side + clientbytes: total bytes transferred from client side + serverpackets: counts of packets from server side + serverbytes: total bytes transferred from server side + ts: timestamp of first packet + dt: datetime of first packet + starttime: datetime of first packet + endtime: datetime of last packet + client_state: the TCP state on the client side ("init", + "established", "closed", etc.) + server_state: the TCP state on server side + blobs: list of reassembled half-stream Blobs + stop: if True, stop following connection + handled: used to indicate if a connection was already passed through + a plugin's connection_handler function. Resets when new + data for a connection comes in. + + """ + + def __init__(self, plugin, first_packet): + """ + Initializes Connection object + + Args: + plugin: an instance of the plugin creating this connection + first_packet: the first Packet object to initialize connection + """ + self.plugin = plugin.name + self.addr = first_packet.addr + self.sip = first_packet.sip + self.smac = first_packet.smac + self.sport = first_packet.sport + self.sipcc = first_packet.sipcc + self.siplat = first_packet.siplat + self.siplon = first_packet.siplon + self.sipasn = first_packet.sipasn + self.clientip = first_packet.sip + self.clientmac = first_packet.smac + self.clientport = first_packet.sport + self.clientcc = first_packet.sipcc + self.clientlat = first_packet.siplat + self.clientlon = first_packet.siplon + self.clientasn = first_packet.sipasn + self.dip = first_packet.dip + self.dmac = first_packet.dmac + self.dport = first_packet.dport + self.dipcc = first_packet.dipcc + self.diplat = first_packet.diplat + self.diplon = first_packet.diplon + self.dipasn = first_packet.dipasn + self.serverip = first_packet.dip + self.servermac = first_packet.dmac + self.serverport = first_packet.dport + self.servercc = first_packet.dipcc + self.serverlat = first_packet.diplat + self.serverlon = first_packet.diplon + self.serverasn = first_packet.dipasn + self.protocol = first_packet.protocol + self.clientpackets = 0 + self.clientbytes = 0 + self.serverpackets = 0 + self.serverbytes = 0 + self.ts = first_packet.ts + self.dt = first_packet.dt + self.starttime = first_packet.dt + self.endtime = first_packet.dt + self.client_state = None + self.server_state = None + self.blobs = [] + self.stop = False + self.handled = False + # used to determine if direction changes + self._current_addr_pair = None + + @property + def duration(self): + "total seconds from starttime to endtime" + tdelta = self.endtime - self.starttime + return tdelta.total_seconds() + + @property + def connection_closed(self): + return self.client_state == "closed" and self.server_state == "closed" + + def add_packet(self, packet): + """ + Accepts a Packet object and attempts to push it into the current Blob. + If the direction changes, it creates a new Blob and returns the old one + to the caller. + + Args: + packet: a Packet object to add to the connection + + Returns: + Previous Blob if direction has changed + """ + if packet.sip == self.clientip and (not packet.sport or packet.sport == self.clientport): + # packet moving from client to server + direction = 'cs' + else: + # packet moving from server to client + direction = 'sc' + + if (packet.addr != self._current_addr_pair and packet.data) or len(self.blobs) == 0: + try: + old_blob = self.blobs[-1] + except IndexError: + old_blob = None + self.blobs.append(Blob(packet, direction)) + self._current_addr_pair = packet.addr + else: + old_blob = None + + blob = self.blobs[-1] + blob.add_packet(packet) + + # Only count packets if they have data (i.e. ignore SYNs, ACKs, etc.) + if packet.data: + if packet.addr == self.addr: + self.clientpackets += 1 + self.clientbytes += packet.byte_count + else: + self.serverpackets += 1 + self.serverbytes += packet.byte_count + + if packet.dt > self.endtime: + self.endtime = packet.dt + + if old_blob: + return old_blob + + def info(self): + """ + Provides a dictionary with information about a connection. Useful for + calls to a plugin's write() function, e.g. self.write(\\*\\*conn.info()) + + Returns: + Dictionary with information + """ + d = dict(self.__dict__) + d['duration'] = self.duration + del d['blobs'] + del d['stop'] + del d['_current_addr_pair'] + del d['handled'] + return d + + def __repr__(self): + return '%s %16s -> %16s (%s -> %s) %6s %6s %5d %5d %7d %7d %-.4fs' % ( + self.starttime, + self.clientip, + self.serverip, + self.clientcc, + self.servercc, + self.clientport, + self.serverport, + self.clientpackets, + self.serverpackets, + self.clientbytes, + self.serverbytes, + self.duration, + ) + +class Blob(object): + """ + Class for holding and reassembling pieces of a connection. + + A Blob holds the packets and reassembled data for traffic moving in one + direction in a connection, before direction changes. + + def __init__(self, first_packet, direction) + + Args: + first_packet: the first Packet object to initialize Blob + direction: direction of blob - + 'cs' for client-to-server, 'sc' for sever-to-client + + Attributes: + addr: .addr attribute of the first packet + ts: timestamp of the first packet + starttime: datetime for first packet + endtime: datetime of last packet + sip: source IP + smac: source MAC address + sport: source port + sipcc: country code of source IP + sipasn: ASN of source IP + dip: dest IP + dmac: dest MAC address + dport: dest port + dipcc: country code of dest IP + dipasn: ASN of dest IP + protocol: text version of protocol in layer-3 header + direction: direction of the blob - + 'cs' for client-to-server, 'sc' for sever-to-client + ack_sequence_numbers: set of ACK numbers from the receiver for #################################### + collected data packets + all_packets: list of all packets in the blob + hidden (bool): Used to indicate that a Blob should not be passed to + next plugin. Can theoretically be overruled in, say, a + connection_handler to force a Blob to be passed to next + plugin. + """ + + # max offset before wrap, default is MAXINT32 for TCP sequence numbers + MAX_OFFSET = 0xffffffff + + def __init__(self, first_packet, direction): + self.addr = first_packet.addr + self.ts = first_packet.ts + self.starttime = first_packet.dt + self.endtime = first_packet.dt + self.sip = first_packet.sip + self.smac = first_packet.smac + self.sport = first_packet.sport + self.sipcc = first_packet.sipcc + self.sipasn = first_packet.sipasn + self.dip = first_packet.dip + self.dmac = first_packet.dmac + self.dport = first_packet.dport + self.dipcc = first_packet.dipcc + self.dipasn = first_packet.dipasn + self.protocol = first_packet.protocol + self.direction = direction +# self.ack_sequence_numbers = {} + self.all_packets = [] +# self.data_packets = [] + self.__data_bytes = b'' + + # Used to indicate that a Blob should not be passed to next plugin. + # Can theoretically be overruled in, say, a connection_handler to + # force a Blob to be passed to next plugin. + self.hidden = False + + @property + def data(self): + """ + Returns the reassembled byte string. + + If it was not already reassembled, reassemble is called with default + arguments. + """ + if not self.__data_bytes: + self.reassemble() + return self.__data_bytes + + def reassemble(self, allow_padding=True, allow_overlap=True, padding=b'\x00'): + """ + Rebuild the data string from the current list of data packets + For each packet, the TCP sequence number is checked. + + If overlapping or padding is disallowed, it will raise a + SequenceNumberError exception if a respective event occurs. + + Args: + allow_padding (bool): If data is missing and allow_padding = True + (default: True), then the padding argument + will be used to fill the gaps. + allow_overlap (bool): If data is overlapping, the new data is + used if the allow_overlap argument is True + (default). Otherwise, the earliest data is + kept. + padding: Byte character(s) to use to fill in missing data. Used + in conjunction with allow_padding (default: b'\\\\x00') + """ + data = b"" + unacknowledged_data = [] + acknowledged_data = {} + for pkt in self.all_packets: + if not pkt.sequence_number: + # if there are no sequence numbers (i.e. not TCP), just rebuild + # in chronological order + data += pkt.data + continue + + if pkt.data: + if pkt.sequence_number in acknowledged_data: + continue + unacknowledged_data.append(pkt) + + elif pkt.tcp_flags and pkt.tcp_flags & tcp.TH_ACK: + ackpkt = pkt + for i, datapkt in enumerate(unacknowledged_data): + if (datapkt.ack_number == ackpkt.sequence_number + and ackpkt.ack_number == (datapkt.sequence_number + len(datapkt.data))): + # if the seq/ack numbers align, this is the data packet + # we want + # TODO confirm this logic is correct + acknowledged_data[datapkt.sequence_number] = datapkt.data + unacknowledged_data.pop(i) + break + + if not acknowledged_data and not unacknowledged_data: + # For non-sequential protocols, just return what we have + self.__data_bytes = data + + else: + # Create a list of each segment of the complete data. Use + # acknowledged data first, and then try to fill in the blanks with + # unacknowledged data. + segments = acknowledged_data.copy() + for pkt in reversed(unacknowledged_data): + if pkt.sequence_number in segments: continue + segments[pkt.sequence_number] = pkt.data + + offsets = sorted(segments.keys()) + # iterate over the segments and try to piece them together + # handle any instances of missing or overlapping segments + nextoffset = offsets[0] + startoffset = offsets[0] + for offset in offsets: + if offset > nextoffset: + # data is missing + if allow_padding: + data += padding * (offset - nextoffset) + else: + raise SequenceNumberError("Missing data for sequence number %d %s" % (nextoffset, self.addr)) + elif offset < nextoffset: + # data is overlapping + if not allow_overlap: + raise SequenceNumberError("Overlapping data for sequence number %d %s" % (nextoffset, self.addr)) + + nextoffset = (offset + len(segments[offset])) & self.MAX_OFFSET + data = data[:offset - startoffset] + \ + segments[offset] + \ + data[nextoffset - startoffset:] + self.__data_bytes = data + + return data + + + + +# segments = {} +# for pkt in self.data_packets: +# if pkt.sequence_number: +# segments.setdefault(pkt.sequence_number, []).append(pkt.data) +# else: +# # if there are no sequence numbers (i.e. not TCP), just rebuild +# # in chronological order +# data += pkt.data +# +# if not segments: +# # For non-sequential protocols, just return what we have +# self.__data_bytes = data +# return data +# +# offsets = sorted(segments.keys()) +# +# # iterate over the segments and try to piece them together +# # handle any instances of missing or overlapping segments +# nextoffset = offsets[0] +# startoffset = offsets[0] +# for offset in offsets: +# # TODO do we still want to implement custom error handling? +# if offset > nextoffset: +# # data is missing +# if allow_padding: +# data += padding * (offset - nextoffset) +# else: +# raise SequenceNumberError("Missing data for sequence number %d %s" % (nextoffset, self.addr)) +# elif offset < nextoffset: +# # data is overlapping +# if not allow_overlap: +# raise SequenceNumberError("Overlapping data for sequence number %d %s" % (nextoffset, self.addr)) +## nextoffset = (offset + len(segments[offset][dup])) & self.MAX_OFFSET +## if nextoffset in self.ack_sequence_numbers: +# if offset in self.ack_sequence_numbers: +# # If the data packet was acknowledged by the receiver, +# # we use the first packet received. +# dup = 0 +# else: +# # If it went unacknowledged, we use the last packet and hope +# # for the best. +# dup = -1 +# print(dup) +# print(offset) +# print(nextoffset) +# print(str(self.ack_sequence_numbers)) +# nextoffset = (offset + len(segments[offset][dup])) & self.MAX_OFFSET +# data = data[:offset - startoffset] + \ +# segments[offset][dup] + \ +# data[nextoffset - startoffset:] +# self.__data_bytes = data +# return data + + def info(self): + """ + Provides a dictionary with information about a blob. Useful for + calls to a plugin's write() function, e.g. self.write(\\*\\*conn.info()) + + Returns: + Dictionary with information + """ + d = dict(self.__dict__) + del d['hidden'] + del d['_Blob__data_bytes'] + del d['all_packets'] + return d + + def add_packet(self, packet): + """ + Accepts a Packet object and stores it. + + Args: + packet: a Packet object + """ + self.all_packets.append(packet) + + if packet.dt > self.endtime: + self.endtime = packet.dt diff --git a/share/GeoIP/readme.txt b/dshell/data/GeoIP/readme.txt similarity index 100% rename from share/GeoIP/readme.txt rename to dshell/data/GeoIP/readme.txt diff --git a/dshell/data/dshellrc b/dshell/data/dshellrc new file mode 100644 index 0000000..f428e2e --- /dev/null +++ b/dshell/data/dshellrc @@ -0,0 +1,2 @@ +export PS1="`whoami`@`hostname`:\w Dshell> " +alias decode="python3 -m dshell.decode " diff --git a/dshell/data/empty.pcap b/dshell/data/empty.pcap new file mode 100644 index 0000000..a324304 Binary files /dev/null and b/dshell/data/empty.pcap differ diff --git a/dshell/decode.py b/dshell/decode.py new file mode 100755 index 0000000..2afa423 --- /dev/null +++ b/dshell/decode.py @@ -0,0 +1,742 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This is the core script for running plugins. + +It works by grabbing individual packets from a file or interface and feeding +them into a chain of plugins (plugin_chain). Each plugin in the chain +decides if the packet will continue on to the next plugin or just fade away. + +In practice, users generally only use one plugin, so the "chain" will only +have one plugin, which is perfectly fine. The chain exists to allow plugins +to alter or filter packets before passing them to more general plugins. For +example, --plugin=country+netflow would pass packets through the country +plugin, and then the netflow plugin. This would allow filtering traffic by +country code before viewing flow data. + +Many things go into making this chain run smoothly, however. This includes +reading in user arguments, setting filters, opening files/interfaces, etc. All +of this largely takes place in the main() function. +""" + +# set up logging first, since some third-party libraries would try to +# configure things their own way +import logging +logging.basicConfig(format="%(levelname)s (%(name)s) - %(message)s") +logger = logging.getLogger("decode.py") + +# since pypacker handles its own exceptions (loudly), this attempts to keep +# it quiet +from pypacker import pypacker +pypacker.logger.setLevel(logging.CRITICAL) + +import dshell.core +from dshell.dshelllist import get_plugins, get_output_modules +from dshell.dshellargparse import DshellArgumentParser +from dshell.output.output import QueueOutputWrapper +from dshell.util import get_output_path + +import pcapy + +# standard Python library imports +import bz2 +import copy +import faulthandler +import gzip +import multiprocessing +import operator +import os +import queue +#import signal +import sys +import tempfile +import zipfile +from collections import OrderedDict +from getpass import getpass +from glob import glob +from importlib import import_module + +# plugin_chain will eventually hold the user-selected plugins that packets +# will trickle through. +plugin_chain = [] + +def feed_plugin_chain(plugin_index, packet_tuple): + """ + Every packet fed into Dshell goes through this function. + Its goal is to pass each packet down the chain of selected plugins. + Each plugin decides whether the packet(s) will proceed to the next + plugin, i.e. act as a filter. + """ + global plugin_chain + (pktlen, pkt, ts) = packet_tuple + current_plugin = plugin_chain[plugin_index] + next_plugin_index = plugin_index + 1 + if next_plugin_index >= len(plugin_chain): + next_plugin_index = None + + # Check the plugin's filter to see if this packet should go any further + if current_plugin.compiled_bpf and not current_plugin.compiled_bpf.filter(pkt): + return + + # Begin stepping through the plugin and feeding each handler function in + # order: + # raw_handler --> packet_handler -?-> connection_handler (init/blob_handler/close) + current_plugin._raw_handler(pktlen, pkt, ts) + + # feed any available raw packets to the packet_handler + while len(current_plugin.raw_packet_queue) > 0: + rawpacket = current_plugin.raw_packet_queue.pop(0) + current_plugin._packet_handler(*rawpacket) + + # Check if this plugin handles connections + # If it doesn't, we can just pass the packet to the next plugin now + if "_connection_handler" not in current_plugin.members: + if next_plugin_index: + while len(current_plugin.packet_queue) > 0: + packet = current_plugin.packet_queue.pop(0) + feed_plugin_chain(next_plugin_index, packet.packet_tuple) + return + + # Connection handlers are a little different. + # They only enqueue anything when a connection closes or times out. + while len(current_plugin.packet_queue) > 0: + packet = current_plugin.packet_queue.pop(0) + current_plugin._connection_handler(packet) + + # Connections are "passed" to the next plugin by popping off their blobs, + # then passing all of the packets within. + # Afterwards, the connection is cleared from the plugin's cache. + while len(current_plugin.connection_queue) > 0: + connection = current_plugin.connection_queue.pop(0) + if next_plugin_index: + for blob in connection.blobs: + if not blob.hidden: + for packet in blob.all_packets: + feed_plugin_chain(next_plugin_index, packet.packet_tuple) + conn_key = tuple(sorted(connection.addr)) + try: + # Attempt to clear out the connection, now that it has been handled. + del current_plugin.connection_tracker[conn_key] + except KeyError: + # If the plugin messed with the connection's address, it might + # fail to clear it. + # TODO find some way to better handle this scenario + pass + + +def clean_plugin_chain(plugin_index): + """ + This is called at the end of packet capture. + It will go through the plugins and attempt to cleanup any connections + that were not yet closed. + """ + current_plugin = plugin_chain[plugin_index] + next_plugin_index = plugin_index + 1 + if next_plugin_index >= len(plugin_chain): + next_plugin_index = None + + # Check if the plugin handles connections + # If it does, close out its open connections and pass the stored packets + # down the chain. + if "_connection_handler" in current_plugin.members: + for connection_handler_out in current_plugin._cleanup_connections(): + if not connection_handler_out: + continue + if next_plugin_index: + for blob in connection_handler_out.blobs: + if not blob.hidden: + for packet in blob.all_packets: + feed_plugin_chain(next_plugin_index, packet.packet_tuple) + if next_plugin_index: + clean_plugin_chain(next_plugin_index) + + +def decompress_file(filepath, extension, unzipdir): + """ + Attempts to decompress a provided file and write the data to a temporary + file. The list of created temporary files is returned. + """ + filename = os.path.split(filepath)[-1] + openfiles = [] + logger.debug("Attempting to decompress {!r}".format(filepath)) + if extension == '.gz': + f = gzip.open(filepath, 'rb') + openfiles.append(f) + elif extension == '.bz2': + f = bz2.open(filepath, 'rb') + openfiles.append(f) + elif extension == '.zip': + pswd = getpass("Enter password for .zip file {!r} [default: none]: ".format(filepath)) + pswd = pswd.encode() # TODO I'm not sure encoding to utf-8 will work in all cases + try: + z = zipfile.ZipFile(filepath) + for z2 in z.namelist(): + f = z.open(z2, 'r', pswd) + openfiles.append(f) + except (RuntimeError, zipfile.BadZipFile) as e: + logger.error("Could not process .zip file {!r}. {!s}".format(filepath, e)) + return [] + + tempfiles = [] + for openfile in openfiles: + try: + # check if this file is actually something decompressable + openfile.peek(1) + except OSError as e: + logger.error("Could not process compressed file {!r}. {!s}".format(filepath, e)) + openfile.close() + continue + tfile = tempfile.NamedTemporaryFile(dir=unzipdir, delete=False, prefix=filename) + for piece in openfile: + tfile.write(piece) + tempfiles.append(tfile.name) + openfile.close() + tfile.close() + return tempfiles + + + +def print_plugins(plugins): + "Print list of plugins with additional info" + row = "{:<40} {:15} {:<20} {:<20} {:<10} {}" + print(row.format('module', 'name', 'title', 'type', 'author', 'description')) + print('-' * 121) + for name, module in plugins.items(): + print(row.format(module.__module__, + name, + module.name, + module.__class__.__bases__[0].__name__, + module.author, + module.description)) + +def main(plugin_args={}, **kwargs): + global plugin_chain + + # dictionary of all available plugins: {name: module path} + plugin_map = get_plugins(logger) + + # Attempt to catch segfaults caused when certain linktypes (e.g. 204) are + # given to pcapy + faulthandler.enable() + + if not plugin_chain: + logger.error("No plugin selected") + sys.exit(1) + + plugin_chain[0].defrag_ip = kwargs.get("defrag", False) + + if kwargs.get("verbose", False): + logger.setLevel(logging.INFO) + dshell.core.logger.setLevel(logging.INFO) + dshell.core.geoip.logger.setLevel(logging.INFO) + # Activate verbose mode in each of the plugins + for plugin in plugin_chain: + plugin.out.set_level(logging.INFO) + + if kwargs.get("allcc", False): + # Activate all country code (allcc) mode to display all 3 GeoIP2 country + # codes + dshell.core.geoip.acc = True + + if kwargs.get("debug", False): + pypacker.logger.setLevel(logging.WARNING) + logger.setLevel(logging.DEBUG) + dshell.core.logger.setLevel(logging.DEBUG) + dshell.core.geoip.logger.setLevel(logging.DEBUG) + # Activate debug mode in each of the plugins + for plugin in plugin_chain: + plugin.out.set_level(logging.DEBUG) + + if kwargs.get("quiet", False): + logger.disabled = True + dshell.core.logger.disabled = True + dshell.core.geoip.logger.disabled = True + # Disable logging for each of the plugins + for plugin in plugin_chain: + plugin.out.logger.disabled = True + + dshell.core.geoip.check_file_dates() + + # If alternate output module is selected, tell each plugin to use that + # instead + if kwargs.get("omodule", None): + # Check if any user-defined output arguments are provided + oargs = {} + if kwargs.get("oargs", None): + for oarg in kwargs["oargs"]: + if '=' in oarg: + key, val = oarg.split('=', 1) + oargs[key] = val + else: + oargs[oarg] = True + try: + omodule = import_module("dshell.output."+kwargs["omodule"]) + omodule = omodule.obj + for plugin in plugin_chain: + oargs['label'] = plugin.__module__ + oomodule = omodule(**oargs) + if kwargs.get("verbose", False): + oomodule.set_level(logging.INFO) + if kwargs.get("debug", False): + oomodule.set_level(logging.DEBUG) + if kwargs.get("quiet", False): + oomodule.logger.disabled = True + plugin.out = oomodule + except ImportError as e: + logger.error("Could not import module named '{}'. Use --list-output flag to see available modules".format(kwargs["omodule"])) + sys.exit(1) + + # If writing to a file, set for each output module here + if kwargs.get("outfile", None): + for plugin in plugin_chain: + try: + plugin.out.reset_fh(filename=kwargs["outfile"]) + # Try and catch common exceptions to avoid lengthy tracebacks + except OSError as e: + if not self.debug: + logger.error(str(e)) + sys.exit(1) + else: + raise e + + # Set nobuffer mode if that's what the user wants + if kwargs.get("nobuffer", False): + for plugin in plugin_chain: + plugin.out.nobuffer = True + + # Set the extra flag for all output modules + if kwargs.get("extra", False): + for plugin in plugin_chain: + plugin.out.extra = True + plugin.out.set_format(plugin.out.format) + + # Set the BPF filters + # Each plugin has its own default BPF that will be extended or replaced + # based on --no-vlan, --ebpf, or --bpf arguments. + for plugin in plugin_chain: + if kwargs.get("bpf", None): + plugin.bpf = kwargs.get("bpf", "") + continue + if plugin.bpf: + if kwargs.get("ebpf", None): + plugin.bpf = "({}) and ({})".format(plugin.bpf, kwargs.get("ebpf", "")) + else: + if kwargs.get("ebpf", None): + plugin.bpf = kwargs.get("ebpf", "") + if kwargs.get("novlan", False): + plugin.vlan_bpf = False + + # Decide on the inputs to use for pcap + # If --interface is set, ignore all files and listen live on the wire + # Otherwise, use all of the files and globs to open offline pcap. + # Recurse through any directories if the command-line flag is set. + if kwargs.get("interface", None): + inputs = [kwargs.get("interface")] + else: + inputs = [] + inglobs = kwargs.get("files", []) + infiles = [] + for inglob in inglobs: + outglob = glob(inglob) + if not outglob: + logger.warning("Could not find file(s) matching {!r}".format(inglob)) + continue + infiles.extend(outglob) + while len(infiles) > 0: + infile = infiles.pop(0) + if kwargs.get("recursive", False) and os.path.isdir(infile): + morefiles = os.listdir(infile) + for morefile in morefiles: + infiles.append(os.path.join(infile, morefile)) + elif os.path.isfile(infile): + inputs.append(infile) + + # Process plugin-specific options + for plugin in plugin_chain: + for option, args in plugin.optiondict.items(): + if option in plugin_args.get(plugin, {}): + setattr(plugin, option, plugin_args[plugin][option]) + else: + setattr(plugin, option, args.get("default", None)) + plugin.handle_plugin_options() + + + #### Dshell is ready to read pcap! #### + for plugin in plugin_chain: + plugin._premodule() + + # If we are not multiprocessing, simply pass the files for processing + if not kwargs.get("multiprocessing", False): + process_files(inputs, **kwargs) + # If we are multiprocessing, things get more complicated. + else: + # Create an output queue, and wrap the 'write' function of each + # plugins's output module to send calls to the multiprocessing queue + output_queue = multiprocessing.Queue() + output_wrappers = {} + for plugin in plugin_chain: + qo = QueueOutputWrapper(plugin.out, output_queue) + output_wrappers[qo.id] = qo + plugin.out.write = qo.write + + # Create processes to handle each separate input file + processes = [] + for i in inputs: + processes.append( + multiprocessing.Process(target=process_files, args=([i]), kwargs=kwargs) + ) + + # Spawn processes, and keep track of which ones are running + running = [] + max_writes_per_batch = 50 + while processes or running: + if processes and len(running) < kwargs.get("process_max", 4): + # Start a process and move it to the 'running' list + proc = processes.pop(0) + proc.start() + logger.debug("Started process {}".format(proc.pid)) + running.append(proc) + for proc in running: + if not proc.is_alive(): + # Remove finished processes from 'running' list + logger.debug("Ended process {} (exit code: {})".format(proc.pid, proc.exitcode)) + running.remove(proc) + try: + # Process write commands in the output queue. + # Since some plugins write copiously and may block other + # processes from launching, only write up to a maximum number + # before breaking and rechecking the processes. + writes = 0 + while writes < max_writes_per_batch: + wrapper_id, args, kwargs = output_queue.get(True, 1) + owrapper = output_wrappers[wrapper_id] + owrapper.true_write(*args, **kwargs) + writes += 1 + except queue.Empty: + pass + + output_queue.close() + + for plugin in plugin_chain: + plugin._postmodule() + + +def process_files(inputs, **kwargs): + # Iterate over each of the input files + # For live capture, the "input" would just be the name of the interface + global plugin_chain + + while len(inputs) > 0: + input0 = inputs.pop(0) + + # Check if file needs to be decompressed by its file extension + extension = os.path.splitext(input0)[-1] + if extension in (".gz", ".bz2", ".zip") and not "interface" in kwargs: + tempfiles = decompress_file(input0, extension, kwargs.get("unzipdir", tempfile.gettempdir())) + inputs = tempfiles + inputs + continue + + for plugin in plugin_chain: + plugin._prefile(input0) + + if kwargs.get("interface", None): + # Listen on an interface if the option is set + try: + capture = pcapy.open_live(input0, 65536, True, 0) + except pcapy.PcapError as e: + # User probably doesn't have permission to listen on interface + # In any case, print just the error without traceback + logger.error(str(e)) + sys.exit(1) + else: + # Otherwise, read from pcap file(s) + try: + capture = pcapy.open_offline(input0) + except pcapy.PcapError as e: + logger.error("Could not open '{}': {!s}".format(input0, e)) + continue + + # Try and use the first plugin's BPF as the initial filter + # The BPFs for other plugins will be applied along the chain as needed + initial_bpf = plugin_chain[0].bpf + try: + if initial_bpf: + capture.setfilter(initial_bpf) + except pcapy.PcapError as e: + if str(e).startswith("no VLAN support for data link type"): + logger.error("Cannot use VLAN filters for {!r}. Recommend running with --no-vlan argument.".format(input0)) + continue + elif "syntax error" in str(e) or "link layer applied in wrong context" == str(e): + logger.error("Could not compile BPF: {!s} ({!r})".format(e, initial_bpf)) + sys.exit(1) + elif "802.11 link-layer types supported only on 802.11" == str(e): + logger.error("BPF incompatible with pcap file: {!s}".format(e)) + continue + else: + raise e + + # Set the datalink layer for each plugin, based on the pcapy capture. + # Also compile a pcapy BPF object for each. + for plugin in plugin_chain: + # TODO Find way around libpcap bug that segfaults when certain BPFs + # are used with certain datalink types + # (e.g. datalink=204, bpf="ip") + plugin.set_link_layer_type(capture.datalink()) + plugin.recompile_bpf() + + # Iterate over the file/interface and pass the packets down the chain + while True: + try: + header, packet = capture.next() + if header == None and not packet: + # probably the end of the capture + break + if kwargs.get("count", 0) and plugin_chain[0].seen_packet_count.value >= kwargs["count"]: + # we've reached the maximum number of packets to process + break + pktlen = header.getlen() + ts = header.getts() + ts = ts[0] + ts[1] / 1000000.0 + feed_plugin_chain(0, (pktlen, packet, ts)) + except pcapy.PcapError as e: + estr = str(e) + eformat = "Error processing '{i}' - {e}" + if estr.startswith("truncated dump file"): + logger.error( eformat.format(i=input0, e=estr) ) + if kwargs.get("debug", False): + logger.exception(e) + elif estr.startswith("bogus savefile header"): + logger.error( eformat.format(i=input0, e=estr) ) + if kwargs.get("debug", False): + logger.exception(e) + else: + raise e + break + + clean_plugin_chain(0) + for plugin in plugin_chain: + try: + plugin._purge_connections() + except AttributeError: + # probably just a packet plugin + pass + plugin._postfile() + + +def main_command_line(): + global plugin_chain + # dictionary of all available plugins: {name: module path} + plugin_map = get_plugins(logger) + # dictionary of plugins that the user wants to use: {name: object} + active_plugins = OrderedDict() + + # The main argument parser. It will have every command line option + # available and should be used when actually parsing + parser = DshellArgumentParser( + usage="%(prog)s [options] [plugin options] file1 file2 ... fileN", + add_help=False) + parser.add_argument('-c', '--count', type=int, default=0, + help='Number of packets to process') + parser.add_argument('--debug', action="store_true", + help="Show debug messages") + parser.add_argument('-v', '--verbose', action="store_true", + help="Show informational messages") + parser.add_argument('-acc', '--allcc', action="store_true", + help="Show all 3 GeoIP2 country code types (represented_country/registered_country/country)") + parser.add_argument('-d', '-p', '--plugin', dest='plugin', type=str, + action='append', metavar="DECODER", + help="Use a specific plugin module. Can be chained with '+'.") + parser.add_argument('--defragment', dest='defrag', action='store_true', + help='Reconnect fragmented IP packets') + parser.add_argument('-h', '-?', '--help', dest='help', + help="Print common command-line flags and exit", action='store_true', + default=False) + parser.add_argument('-i', '--interface', default=None, type=str, + help="Listen live on INTERFACE instead of reading pcap") + parser.add_argument('-l', '--ls', '--list', action="store_true", + help='List all available plugins', dest='list') + parser.add_argument('-r', '--recursive', dest='recursive', action='store_true', + help='Recursively process all PCAP files under input directory') + parser.add_argument('--unzipdir', type=str, metavar="DIRECTORY", + default=tempfile.gettempdir(), + help='Directory to use when decompressing input files (.gz, .bz2, and .zip only)') + + multiprocess_group = parser.add_argument_group("multiprocessing arguments") + multiprocess_group.add_argument('-P', '--parallel', dest='multiprocessing', action='store_true', + help='Handle each file in separate parallel processes') + multiprocess_group.add_argument('-n', '--nprocs', type=int, default=4, + metavar='NUMPROCS', dest='process_max', + help='Define max number of parallel processes (default: 4)') + + filter_group = parser.add_argument_group("filter arguments") + filter_group.add_argument('--bpf', default='', type=str, + help="Overwrite all BPFs and use provided input. Use carefully!") + filter_group.add_argument('--ebpf', default='', type=str, metavar="BPF", + help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"() and ()\"") + filter_group.add_argument("--no-vlan", action="store_true", dest="novlan", + help="Ignore packets with VLAN headers") + + output_group = parser.add_argument_group("output arguments") + output_group.add_argument("--lo", "--list-output", action="store_true", + help="List available output modules", + dest="listoutput") + output_group.add_argument("--no-buffer", action="store_true", + help="Do not buffer plugin output", + dest="nobuffer") + output_group.add_argument("-x", "--extra", action="store_true", + help="Appends extra data to all plugin output.") + # TODO Figure out how to make --extra flag play nicely with user-only + # output modules, like jsonout and csvout + output_group.add_argument("-O", "--omodule", type=str, dest="omodule", + metavar="MODULE", + help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.") + output_group.add_argument("--oarg", type=str, metavar="ARG=VALUE", + dest="oargs", action="append", + help="Supply a specific keyword argument to user-defined output module. Only used in conjunction with --omodule. Can be used multiple times for multiple arguments. Not using an equal sign will treat it as a flag and set the value to True. Example: --omodule=alertout --oarg \"timeformat=%%H %%M %%S\"") + output_group.add_argument("-q", "--quiet", action="store_true", + help="Disable logging") + output_group.add_argument("-W", metavar="OUTFILE", dest="outfile", + help="Write to OUTFILE instead of stdout") + + parser.add_argument('files', nargs='*', + help="pcap files or globs to process") + + # A short argument parser, meant to only hold the simplified list of + # arguments for when a plugin is called without a pcap file. + # DO NOT USE for any serious argument parsing. + parser_short = DshellArgumentParser( + usage="%(prog)s [options] [plugin options] file1 file2 ... fileN", + add_help=False) + parser_short.add_argument('-h', '-?', '--help', dest='help', + help="Print common command-line flags and exit", action='store_true', + default=False) + parser.add_argument('--version', action='version', + version="Dshell " + str(dshell.core.__version__)) + parser_short.add_argument('-d', '-p', '--plugin', dest='plugin', type=str, + action='append', metavar="DECODER", + help="Use a specific plugin module") + parser_short.add_argument('--ebpf', default='', type=str, metavar="BPF", + help="Extend existing BPFs with provided input for additional filtering. It will transform input into \"() and ()\"") + parser_short.add_argument('-i', '--interface', + help="Listen live on INTERFACE instead of reading pcap") + parser_short.add_argument('-l', '--ls', '--list', action="store_true", + help='List all available plugins', dest='list') + parser_short.add_argument("--lo", "--list-output", action="store_true", + help="List available output modules") + parser_short.add_argument("-o", "--omodule", type=str, metavar="MODULE", + help="Use specified output module for plugins instead of defaults. For example, --omodule=jsonout for JSON output.") + parser_short.add_argument('files', nargs='*', + help="pcap files or globs to process") + + # Start parsing the arguments + # Specifically, we want to grab the desired plugin list + # This will let us add the plugin-specific arguments and reprocess the args + opts, xopts = parser.parse_known_args() + if opts.plugin: + # Multiple plugins can be chained using either multiple instances + # of -d/-p/--plugin or joining them together with + signs. + plugins = '+'.join(opts.plugin) + plugins = plugins.split('+') + # check for invalid plugins + for plugin in plugins: + plugin = plugin.strip() + if not plugin: + # User probably mistyped '++' instead of '+' somewhere. + # Be nice and ignore this minor infraction. + continue + if plugin not in plugin_map: + parser_short.epilog = "ERROR! Invalid plugin provided: '{}'".format(plugin) + parser_short.print_help() + sys.exit(1) + # While we're at it, go ahead and import the plugin modules now + # This can probably be done further down the line, but here is + # just convenient + plugin_module = import_module(plugin_map[plugin]) + # Handle multiple instances of same plugin by appending number to + # end of plugin name. This is used mostly to separate + # plugin-specific arguments from each other + if plugin in active_plugins: + i = 1 + plugin = plugin + str(i) + while plugin in active_plugins: + i += 1 + plugin = plugin[:-(len(str(i-1)))] + str(i) + # Add copy of plugin object to chain and add to argument parsers + active_plugins[plugin] = plugin_module.DshellPlugin() + plugin_chain.append(active_plugins[plugin]) + parser.add_plugin_arguments(plugin, active_plugins[plugin]) + parser_short.add_plugin_arguments(plugin, active_plugins[plugin]) + opts, xopts = parser.parse_known_args() + + if xopts: + for xopt in xopts: + logger.warning('Could not understand argument {!r}'.format(xopt)) + + if opts.help: + # Just print the full help message and exit + parser.print_help() + print("\n") + for plugin in plugin_chain: + print("############### {}".format(plugin.name)) + print(plugin.longdescription) + print("\n") + print('Default BPF: "{}"'.format(plugin.bpf)) + print("\n") + sys.exit() + + if opts.list: + # Import ALL of the plugins and print info about them before exiting + listing_plugins = OrderedDict() + for name, module in sorted(plugin_map.items(), key=operator.itemgetter(1)): + try: + module = import_module(module) + if not module.DshellPlugin: + continue + module = module.DshellPlugin() + listing_plugins[name] = module + except Exception as e: + logger.error("Could not load {!r}. ({!s})".format(module, e)) + if opts.debug: + logger.exception(e) + print_plugins(listing_plugins) + sys.exit() + + if opts.listoutput: + # List available output modules and a brief description + output_map = get_output_modules(get_output_path(), logger) + for modulename in sorted(output_map): + try: + module = import_module("dshell.output."+modulename) + module = module.obj + except Exception as e: + etype = e.__class__.__name__ + logger.debug("Could not load {} module. ({}: {!s})".format(modulename, etype, e)) + else: + print("\t{:<25} {}".format(modulename, module._DESCRIPTION)) + sys.exit() + + if not opts.plugin: + # If a plugin isn't provided, print the short help message + parser_short.epilog = "Select a plugin to use with -d or --plugin" + parser_short.print_help() + sys.exit() + + if not opts.files and not opts.interface: + # If no files are provided, print the short help message + parser_short.epilog = "Include a pcap file to get started. Use --help for more information." + parser_short.print_help() + sys.exit() + + # Process the plugin-specific args and set the attributes within them + plugin_args = {} + for plugin_name, plugin in active_plugins.items(): + plugin_args[plugin] = {} + args_and_attrs = parser.get_plugin_arguments(plugin_name, plugin) + for darg, dattr in args_and_attrs: + value = getattr(opts, darg) + plugin_args[plugin][dattr] = value + + main(plugin_args=plugin_args, **vars(opts)) + +if __name__ == "__main__": + main_command_line() diff --git a/dshell/dshellargparse.py b/dshell/dshellargparse.py new file mode 100644 index 0000000..82feaa0 --- /dev/null +++ b/dshell/dshellargparse.py @@ -0,0 +1,40 @@ +""" +This argument parser is almost identical to the Python standard argparse. +This one adds a function to automatically add plugin-specific arguments. +""" + +import argparse + +class DshellArgumentParser(argparse.ArgumentParser): + + def add_plugin_arguments(self, plugin_name, plugin_obj): + """ + add_plugin_arguments(self, plugin_name, plugin_obj) + + Give it the name of the plugin and an instance of the plugin, and + it will automatically create argument entries. + """ + if plugin_obj.optiondict: + group = '{} plugin options'.format(plugin_obj.name) + group = self.add_argument_group(group) + for argname, optargs in plugin_obj.optiondict.items(): + optname = "{}_{}".format(plugin_name, argname) + group.add_argument("--" + optname, dest=optname, **optargs) + + def get_plugin_arguments(self, plugin_name, plugin_obj): + """ + get_plugin_arguments(self, plugin_name, plugin_obj) + + Returns a list of argument names and the attributes they're associated + with. + + e.g. --country_code for the "country" plugin ties to the "code" attr + in the plugin object. Thus, the return would be + [("country_code", "code"), ...] + """ + args_and_attrs = [] + if plugin_obj.optiondict: + for argname in plugin_obj.optiondict.keys(): + optname = "{}_{}".format(plugin_name, argname) + args_and_attrs.append((optname, argname)) + return args_and_attrs diff --git a/dshell/dshellgeoip.py b/dshell/dshellgeoip.py new file mode 100644 index 0000000..f53b257 --- /dev/null +++ b/dshell/dshellgeoip.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" +A wrapper around GeoIP2 that provides convenience functions for querying and +collecting GeoIP data +""" + +import datetime +import logging +import os +from collections import OrderedDict + +import geoip2.database +import geoip2.errors + +from dshell.util import get_data_path + +class DshellGeoIP(object): + + MAX_CACHE_SIZE = 5000 + + def __init__(self, acc=False, logger=logging.getLogger("dshellgeoip.py")): + self.logger = logger + self.geodir = os.path.join(get_data_path(), 'GeoIP') + self.geoccfile = os.path.join(self.geodir, 'GeoLite2-City.mmdb') + self.geoasnfile = os.path.join(self.geodir, 'GeoLite2-ASN.mmdb') + self.geoccdb = geoip2.database.Reader(self.geoccfile) + self.geoasndb = geoip2.database.Reader(self.geoasnfile) + self.geo_asn_cache = DshellGeoIPCache(max_cache_size=self.MAX_CACHE_SIZE) + self.geo_loc_cache = DshellGeoIPCache(max_cache_size=self.MAX_CACHE_SIZE) + self.acc = acc + + def check_file_dates(self): + "Check the data file age, and log a warning if it's over a year old" + cc_mtime = datetime.datetime.fromtimestamp(os.path.getmtime(self.geoccfile)) + asn_mtime = datetime.datetime.fromtimestamp(os.path.getmtime(self.geoasnfile)) + n = datetime.datetime.now() + year = datetime.timedelta(days=365) + if (n - cc_mtime) > year or (n - asn_mtime) > year: + self.logger.debug("GeoIP data file(s) over a year old, and possibly outdated.") + + def geoip_country_lookup(self, ip): + "Looks up the IP and returns the two-character country code." + location = self.geoip_location_lookup(ip) + return location[0] + + def geoip_asn_lookup(self, ip): + """ + Looks up the IP and returns an ASN string. + Example: + print geoip_asn_lookup("74.125.26.103") + "AS15169 Google LLC" + """ + try: + return self.geo_asn_cache[ip] + except KeyError: + try: + template = "AS{0.autonomous_system_number} {0.autonomous_system_organization}" + asn = template.format(self.geoasndb.asn(ip)) + self.geo_asn_cache[ip] = asn + return asn + except geoip2.errors.AddressNotFoundError: + return None + + def geoip_location_lookup(self, ip): + """ + Looks up the IP and returns a tuple containing country code, latitude, + and longitude. + """ + try: + return self.geo_loc_cache[ip] + except KeyError: + try: + location = self.geoccdb.city(ip) + # Get country code based on order of importance + # 1st: Country that owns an IP address registered in another + # location (e.g. military bases in foreign countries) + # 2nd: Country in which the IP address is registered + # 3rd: Physical country where IP address is located + # https://dev.maxmind.com/geoip/geoip2/whats-new-in-geoip2/#Country_Registered_Country_and_Represented_Country + # Handle flag from plugin optional args to enable all 3 country codes + if self.acc: + try: + cc = "{}/{}/{}".format(location.represented_country.iso_code, + location.registered_country.iso_code, + location.country.iso_code) + cc = cc.replace("None", "--") + + except KeyError: + pass + else: + cc = (location.represented_country.iso_code or + location.registered_country.iso_code or + location.country.iso_code or + '--') + + location = ( + cc, + location.location.latitude, + location.location.longitude + ) + self.geo_loc_cache[ip] = location + return location + except geoip2.errors.AddressNotFoundError: + # Handle flag from plugin optional args to enable all 3 country codes + if self.acc: + location = ("--/--/--", None, None) + else: + location = ("--", None, None) + self.geo_loc_cache[ip] = location + return location + +class DshellFailedGeoIP(object): + "Class used in place of DshellGeoIP if GeoIP database files are not found." + + def __init__(self): + self.geodir = os.path.join(get_data_path(), 'GeoIP') + self.geoccdb = None + self.geoasndb = None + + def check_file_dates(self): + pass + + def geoip_country_lookup(self, ip): + return "??" + + def geoip_asn_lookup(self, ip): + return None + + def geoip_location_lookup(self, ip): + return ("??", None, None) + +class DshellGeoIPCache(OrderedDict): + "A cache for storing recent IP lookups to improve performance." + def __init__(self, *args, **kwargs): + self.max_cache_size = kwargs.pop("max_cache_size", 500) + OrderedDict.__init__(self, *args, **kwargs) + + def __setitem__(self, key, value): + OrderedDict.__setitem__(self, key, value) + self.check_max_size() + + def check_max_size(self): + while len(self) > self.max_cache_size: + self.popitem(last=False) diff --git a/dshell/dshelllist.py b/dshell/dshelllist.py new file mode 100644 index 0000000..450f14e --- /dev/null +++ b/dshell/dshelllist.py @@ -0,0 +1,55 @@ +""" +A library containing functions for generating lists of important modules. +These are mostly used in decode.py and in unit tests +""" + +import os +import pkg_resources +from glob import iglob + +from dshell.util import get_plugin_path + +#def get_plugins(plugin_path, logger=None): +def get_plugins(logger=None): + """ + Generate a list of all available plugin modules, either in the + dshell.plugins directory or external packages + """ + plugins = {} + # List of directories above the plugins directory that we don't care about + import_base = get_plugin_path().split(os.path.sep)[:-1] + + # Walk through the plugin path and find any Python modules that aren't + # __init__.py. These are assumed to be plugin modules and will be + # treated as such. + for root, dirs, files in os.walk(get_plugin_path()): + if '__init__.py' in files: + import_path = root.split(os.path.sep)[len(import_base):] + for f in iglob("{}/*.py".format(root)): + name = os.path.splitext(os.path.basename(f))[0] + if name != '__init__': + if name in plugins and logger: + logger.warning("Duplicate plugin name found: {}".format(name)) + module = '.'.join(["dshell"] + import_path + [name]) + plugins[name] = module + + # Next, try to discover additional plugins installed externally. + # Uses entry points in setup.py files. + for ep_plugin in pkg_resources.iter_entry_points("dshell_plugins"): + if ep_plugin.name in plugins and logger: + logger.warning("Duplicate plugin name found: {}".format(name)) + plugins[ep_plugin.name] = ep_plugin.module_name + + return plugins + +def get_output_modules(output_module_path, logger=None): + """ + Generate a list of all available output modules under an output_module_path + """ + modules = [] + for f in iglob("{}/*.py".format(output_module_path)): + name = os.path.splitext(os.path.basename(f))[0] + if name != '__init__' and name != 'output': + # Ignore __init__ and the base output.py module + modules.append(name) + return modules diff --git a/dshell/output/__init__.py b/dshell/output/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/output/alertout.py b/dshell/output/alertout.py new file mode 100644 index 0000000..e977599 --- /dev/null +++ b/dshell/output/alertout.py @@ -0,0 +1,15 @@ +""" +This output module is used to display single-line alerts + +It inherits nearly everything from the base Output class, and only resets the +_DEFAULT_FORMAT to a more expressive format. +""" + +from dshell.output.output import Output + +class AlertOutput(Output): + "A class that provides a default format for printing a single-line alert" + _DESCRIPTION = "Default format for printing a single-line alert" + _DEFAULT_FORMAT = "[%(plugin)s] %(ts)s %(sip)16s:%(sport)-5s %(dir_arrow)s %(dip)16s:%(dport)-5s ** %(data)s **\n" + +obj = AlertOutput diff --git a/dshell/output/colorout.py b/dshell/output/colorout.py new file mode 100644 index 0000000..04fb17d --- /dev/null +++ b/dshell/output/colorout.py @@ -0,0 +1,88 @@ +""" +Generates packet or reconstructed stream output with ANSI color codes. + +Based on output module originally written by amm +""" + +from dshell.output.output import Output +import dshell.core +import dshell.util + +class ColorOutput(Output): + _DESCRIPTION = "Reconstructed output with ANSI color codes" + _PACKET_FORMAT = """Packet %(counter)s (%(proto)s) +Start: %(ts)s +%(sip)16s:%(sport)6s -> %(dip)16s:%(dport)6s (%(bytes)s bytes) + +%(data)s + +""" + _CONNECTION_FORMAT = """Connection %(counter)s (%(protocol)s) +Start: %(starttime)s +End: %(endtime)s +%(clientip)16s:%(clientport)6s -> %(serverip)16s:%(serverport)6s (%(clientbytes)s bytes) +%(serverip)16s:%(serverport)6s -> %(clientip)16s:%(clientport)6s (%(serverbytes)s bytes) + +%(data)s + +""" + _DEFAULT_FORMAT = _PACKET_FORMAT + + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.counter = 1 + self.colors = { + 'cs': '31', # client-to-server is red + 'sc': '32', # server-to-client is green + '--': '34', # everything else is blue + } # TODO configurable for color-blind users? + self.delim = "\n\n" + self.hexmode = kwargs.get('hex', False) + self.format_is_set = False + + def write(self, *args, **kwargs): + if not self.format_is_set: + if 'clientip' in kwargs: + self.set_format(self._CONNECTION_FORMAT) + else: + self.set_format(self._PACKET_FORMAT) + self.format_is_set = True + + # a template string for data output + colorformat = "\x1b[%sm%s\x1b[0m" + + # Iterate over the args and try to parse out any raw data strings + rawdata = [] + for arg in args: + if type(arg) == dshell.core.Blob: + if arg.data: + rawdata.append((arg.data, arg.direction)) + elif type(arg) == dshell.core.Connection: + for blob in arg.blobs: + if blob.data: + rawdata.append((blob.data, blob.direction)) + elif type(arg) == dshell.core.Packet: + rawdata.append((arg.pkt.body_bytes, kwargs.get('direction', '--'))) + else: + rawdata.append((arg, kwargs.get('direction', '--'))) + + # Clean up the rawdata into something more presentable + if self.hexmode: + cleanup_func = dshell.util.hex_plus_ascii + else: + cleanup_func = dshell.util.printable_text + for k, v in enumerate(rawdata): + newdata = cleanup_func(v[0]) + rawdata[k] = (newdata, v[1]) + + # Convert the raw data strings into color-coded output + data = [] + for arg in rawdata: + datastring = colorformat % (self.colors.get(arg[1], '0'), arg[0]) + data.append(datastring) + + super().write(counter=self.counter, *data, **kwargs) + self.counter += 1 + +obj = ColorOutput diff --git a/dshell/output/csvout.py b/dshell/output/csvout.py new file mode 100644 index 0000000..e23e864 --- /dev/null +++ b/dshell/output/csvout.py @@ -0,0 +1,61 @@ +""" +This output module converts plugin output into a CSV format +""" + +import csv +from dshell.output.output import Output + +class CSVOutput(Output): + """ + Takes specified fields provided to the write function and print them in + a CSV format. + + Delimiter can be set with --oarg delim= + + A header row can be printed with --oarg header + + Additional fields can be included with --oarg fields=field1,field2,field3 + + Note: Field names much match the variable names in the plugin + """ + + # TODO refine plugin to do things like wrap quotes around long strings + + _DEFAULT_FIELDS = ['plugin', 'ts', 'sip', 'sport', 'dip', 'dport', 'data'] + _DEFAULT_DELIM = ',' + _DESCRIPTION = "CSV format output" + + def __init__(self, *args, **kwargs): + self.delimiter = kwargs.get('delim', self._DEFAULT_DELIM) + if self.delimiter == 'tab': + self.delimiter = '\t' + + self.use_header = kwargs.get("header", False) + + self.fields = list(self._DEFAULT_FIELDS) + exfields = kwargs.get("fields", "") + for field in exfields.split(','): + self.fields.append(field) + + super().__init__(**kwargs) + + self.set_format() + + def set_format(self, _=None): + "Set the format to a CSV list of fields" + columns = [] + for f in self.fields: + if f: + columns.append(f) + if self.extra: + columns.append("extra") + fmt = self.delimiter.join('%%(%s)r' % f for f in columns) + fmt += "\n" + super().set_format(fmt) + + def setup(self): + if self.use_header: + self.fh.write(self.delimiter.join([f for f in self.fields]) + "\n") + + +obj = CSVOutput diff --git a/dshell/output/elasticout.py b/dshell/output/elasticout.py new file mode 100644 index 0000000..9cec05d --- /dev/null +++ b/dshell/output/elasticout.py @@ -0,0 +1,74 @@ +""" +This output module converts plugin output into JSON and indexes it into +an Elasticsearch datastore + +NOTE: This module requires the third-party 'elasticsearch' Python module +""" + +import ipaddress +import json + +from elasticsearch import Elasticsearch + +import dshell.output.jsonout + +class ElasticOutput(dshell.output.jsonout.JSONOutput): + """ + Elasticsearch output module + Use with --output=elasticsearchout + + It is recommended that it be run with some options set: + host: server hosting the database (localhost) + port: HTTP port listening (9200) + index: name of index storing results ("dshell") + type: the type for each alert ("alerts") + + Example use: + decode --output=elasticout --oargs="index=dshellalerts" --oargs="type=netflowout" -d netflow ~/pcap/example.pcap + """ + + _DESCRIPTION = "Automatically insert data into an elasticsearch instance" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs.copy()) + + self.options = {} + self.options['host'] = kwargs.get('host', 'localhost') + self.options['port'] = int(kwargs.get('port', 9200)) + self.options['index'] = kwargs.get('index', 'dshell') + self.options['type'] = kwargs.get('type', 'alerts') + + self.es = Elasticsearch([self.options['host']], port=self.options['port']) + + def write(self, *args, **kwargs): + "Converts alert's keyword args to JSON and indexes it into Elasticsearch datastore." + if args and 'data' not in kwargs: + kwargs['data'] = self.delim.join(map(str, args)) + + # Elasticsearch can't handle IPv6 (at time of writing) + # Just delete the ints and expand the string notation. + # Hopefully, it will be possible to perform range searches on this + # consistent IP string format. + try: + del kwargs['dipint'] + except KeyError: + pass + try: + del kwargs['sipint'] + except KeyError: + pass + try: + kwargs['dip'] = ipaddress.ip_address(kwargs['dip']).exploded + except KeyError: + pass + try: + kwargs['sip'] = ipaddress.ip_address(kwargs['sip']).exploded + except KeyError: + pass + + jsondata = json.dumps(kwargs, ensure_ascii=self.ensure_ascii, default=self.json_default) +# from pprint import pprint +# pprint(jsondata) + self.es.index(index=self.options['index'], doc_type=self.options['type'], body=jsondata) + +obj = ElasticOutput diff --git a/dshell/output/htmlout.py b/dshell/output/htmlout.py new file mode 100644 index 0000000..57ab4b3 --- /dev/null +++ b/dshell/output/htmlout.py @@ -0,0 +1,127 @@ +""" +Generates packet or reconstructed stream output as a HTML page. + +Based on colorout module originally written by amm +""" + +from dshell.output.output import Output +import dshell.util +import dshell.core +from xml.sax.saxutils import escape + +class HTMLOutput(Output): + _DESCRIPTION = "HTML format output" + _PACKET_FORMAT = """

Packet %(counter)s (%(protocol)s)

Start: %(ts)s +%(sip)s:%(sport)s -> %(dip)s:%(dport)s (%(bytes)s bytes) +

+%(data)s +""" + _CONNECTION_FORMAT = """

Connection %(counter)s (%(protocol)s)

Start: %(starttime)s +End: %(endtime)s +%(clientip)s:%(clientport)s -> %(serverip)s:%(serverport)s (%(clientbytes)s bytes) +%(serverip)s:%(serverport)s -> %(clientip)s:%(clientport)s (%(serverbytes)s bytes) +

+%(data)s +""" + _DEFAULT_FORMAT = _PACKET_FORMAT + + _HTML_HEADER = """ + + + + Dshell Output + + + +""" + + _HTML_FOOTER = """ + + +""" + + def __init__(self, *args, **kwargs): + "Can be called with an optional 'hex' argument to display output in hex" + super().__init__(*args, **kwargs) + self.counter = 1 + self.colors = { + 'cs': 'red', # client-to-server is red + 'sc': 'green', # server-to-client is green + '--': 'blue', # everything else is blue + } + self.delim = "
" + self.hexmode = kwargs.get('hex', False) + self.format_is_set = False + + def setup(self): + self.fh.write(self._HTML_HEADER) + + def write(self, *args, **kwargs): + if not self.format_is_set: + if 'clientip' in kwargs: + self.set_format(self._CONNECTION_FORMAT) + else: + self.set_format(self._PACKET_FORMAT) + self.format_is_set = True + + # a template string for data output + colorformat = '%s' + + # Iterate over the args and try to parse out any raw data strings + rawdata = [] + for arg in args: + if type(arg) == dshell.core.Blob: + if arg.data: + rawdata.append((arg.data, arg.direction)) + elif type(arg) == dshell.core.Connection: + for blob in arg.blobs: + if blob.data: + rawdata.append((blob.data, blob.direction)) + elif type(arg) == dshell.core.Packet: + rawdata.append((arg.pkt.body_bytes, kwargs.get('direction', '--'))) + else: + rawdata.append((arg, kwargs.get('direction', '--'))) + + # Clean up the rawdata into something more presentable + if self.hexmode: + cleanup_func = dshell.util.hex_plus_ascii + else: + cleanup_func = dshell.util.printable_text + for k, v in enumerate(rawdata): + newdata = cleanup_func(v[0]) + newdata = escape(newdata) + rawdata[k] = (newdata, v[1]) + + # Convert the raw data strings into color-coded output + data = [] + for arg in rawdata: + datastring = colorformat % (self.colors.get(arg[1], ''), arg[0]) + data.append(datastring) + + super().write(counter=self.counter, *data, **kwargs) + self.counter += 1 + + def close(self): + self.fh.write(self._HTML_FOOTER) + Output.close(self) + +obj = HTMLOutput diff --git a/dshell/output/jsonout.py b/dshell/output/jsonout.py new file mode 100644 index 0000000..49014d9 --- /dev/null +++ b/dshell/output/jsonout.py @@ -0,0 +1,44 @@ +""" +This output module converts plugin output into JSON +""" + +from datetime import datetime +import json +from dshell.output.output import Output + +class JSONOutput(Output): + """ + Converts arguments for every write into JSON + Can be called with ensure_ascii=True to pass flag on to the json module. + """ + _DEFAULT_FORMAT = "%(jsondata)s\n" + _DESCRIPTION = "JSON format output" + + def __init__(self, *args, **kwargs): + self.ensure_ascii = kwargs.get('ensure_ascii', False) + super().__init__(*args, **kwargs) + + def write(self, *args, **kwargs): + if self.extra: + # JSONOutput does not make use of the --extra flag, so disable it + # before printing output + self.extra = False + if args and 'data' not in kwargs: + kwargs['data'] = self.delim.join(map(str, args)) + jsondata = json.dumps(kwargs, ensure_ascii=self.ensure_ascii, default=self.json_default) + super().write(jsondata=jsondata) + + def json_default(self, obj): + """ + JSON serializer for objects not serializable by default json code + https://stackoverflow.com/a/22238613 + """ + if isinstance(obj, datetime): + serial = obj.strftime(self.timeformat) + return serial + if isinstance(obj, bytes): + serial = repr(obj) + return serial + raise TypeError ("Type not serializable ({})".format(str(type(obj)))) + +obj = JSONOutput diff --git a/dshell/output/netflowout.py b/dshell/output/netflowout.py new file mode 100644 index 0000000..48469ca --- /dev/null +++ b/dshell/output/netflowout.py @@ -0,0 +1,86 @@ +""" +This output module is used for generating flow-format output +""" + +from dshell.output.output import Output +from datetime import datetime + +class NetflowOutput(Output): + """ + A class for printing connection information for pcap + + Output can be grouped by setting the group flag to a field or fields + separated by a forward-slash + For example: + --output=netflowout --oarg="group=clientip/serverip" + """ + + _DESCRIPTION = "Flow (connection overview) format output" + # Define two types of formats: + # Those for plugins handling individual packets (not really helpful) + _PACKET_FORMAT = "%(ts)s %(sip)16s -> %(dip)16s (%(sipcc)s -> %(dipcc)s) %(protocol)5s %(sport)6s %(dport)6s %(bytes)7s %(msg)s\n" + _PACKET6_FORMAT = "%(ts)s %(sip)40s -> %(dip)40s (%(sipcc)s -> %(dipcc)s) %(protocol)5s %(sport)6s %(dport)6s %(bytes)7s %(msg)s\n" + # And those plugins handling full connections (more useful and common) + _CONNECTION_FORMAT = "%(starttime)s %(clientip)16s -> %(serverip)16s (%(clientcc)s -> %(servercc)s) %(protocol)5s %(clientport)6s %(serverport)6s %(clientpackets)5s %(serverpackets)5s %(clientbytes)7s %(serverbytes)7s %(duration)-.4fs %(data)s\n" + _CONNECTION6_FORMAT = "%(starttime)s %(clientip)40s -> %(serverip)40s (%(clientcc)s -> %(servercc)s) %(protocol)5s %(clientport)6s %(serverport)6s %(clientpackets)5s %(serverpackets)5s %(clientbytes)7s %(serverbytes)7s %(duration)-.4fs %(data)s\n" + # TODO decide if IPv6 formats are necessary, and how to switch between them + # and IPv4 formats + # Default to packets since those fields are in both types of object + _DEFAULT_FORMAT = _PACKET_FORMAT + + def __init__(self, *args, **kwargs): + # Are we grouping the results, and by what fields? + if 'group' in kwargs: + self.group = True + self.group_fields = kwargs['group'].split('/') + else: + self.group = False + self.group_cache = {} # results will be stored here, if grouping + self.format_is_set = False + Output.__init__(self, *args, **kwargs) + + def write(self, *args, **kwargs): + # Change output format depending on if we're handling a connection or + # a single packet + if not self.format_is_set: + if "clientip" in kwargs: + self.set_format(self._CONNECTION_FORMAT) + else: + self.set_format(self._PACKET_FORMAT) + self.format_is_set = True + + if self.group: + # If grouping, check if the IP tuple is in the cache already. + # If not, check the reverse of the tuple (i.e. opposite direction) + try: + key = tuple([kwargs[g] for g in self.group_fields]) + except KeyError as e: + self.logger.error("Could not group by key %s" % str(e)) + Output.write(self, *args, **kwargs) + return + if key not in self.group_cache: + rkey = key[::-1] + if rkey in self.group_cache: + key = rkey + else: + self.group_cache[key] = [] + self.group_cache[key].append(kwargs) + else: + # If not grouping, just write out the connection immediately + Output.write(self, *args, **kwargs) + + def close(self): + if self.group: + self.group = False # we're done grouping, so turn it off + for key in sorted(self.group_cache.keys()): + # write header by mapping key index with user's group list + self.fh.write(' '.join([ + '%s=%s' % (self.group_fields[i], key[i]) for i in range(len(self.group_fields))]) + + "\n") + for kw in self.group_cache[key]: + self.fh.write("\t") + Output.write(self, **kw) + self.fh.write("\n") + Output.close(self) + +obj = NetflowOutput diff --git a/dshell/output/output.py b/dshell/output/output.py new file mode 100644 index 0000000..b543ab2 --- /dev/null +++ b/dshell/output/output.py @@ -0,0 +1,247 @@ +""" +Generic Dshell output class(es) + +Contains the base-level Output class that other modules inherit from. +""" + +import logging +import os +import re +import sys +from collections import defaultdict +from datetime import datetime + +class Output(): + """ + Base-level output class + + Arguments: + label : name to use for logging.getLogger(label) + format : 'format string' to override default formatstring for output class + timeformat : 'format string' for datetime representation + delim : set a delimiter for CSV or similar output + nobuffer : true/false to run flush() after every relevant write + noclobber : set to true to avoid overwriting existing files + fh : existing open file handle + file : filename to write to, assuming fh is not defined + mode : mode to open file, assuming fh is not defined (default 'w') + """ + _DEFAULT_FORMAT = "%(data)s\n" + _DEFAULT_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" + _DEFAULT_DELIM = ',' + _DESCRIPTION = "Base output class" + + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger(kwargs.get("label", "dshell")) + + self.format_fields = [] + self.timeformat = kwargs.get('timeformat', self._DEFAULT_TIME_FORMAT) + self.delim = kwargs.get('delim', self._DEFAULT_DELIM) + self.nobuffer = kwargs.get('nobuffer', False) + self.noclobber = kwargs.get('noclobber', False) + self.mode = kwargs.get('mode', 'w') + self.extra = kwargs.get('extra', False) + + self.set_format( kwargs.get('format', self._DEFAULT_FORMAT) ) + + # Set the filehandle for any output + f = None + if 'fh' in kwargs: + self.fh = kwargs['fh'] + return + elif 'file' in kwargs: + f = kwargs['file'] + elif len(args) > 0: + f = args[0] + if f: + if self.noclobber: + f = self.__incrementFilename(f) + self.fh = open(f, self.mode) + else: + self.fh = sys.stdout + + + def reset_fh(self, filename=None, fh=None, mode=None): + """ + Alter the module's open file handle without changing any of the other + settings. Must supply at least a filename or a filehandle (fh). + reset_fh(filename=None, fh=None, mode=None) + """ + if fh: + self.fh = fh + elif filename: + if self.noclobber: + filename = self.__incrementFilename(filename) + if mode: + self.mode = mode + self.fh = open(filename, mode) + else: + self.fh = open(filename, self.mode) + + def set_level(self, lvl): + "Set the logging level. Just a wrapper around logging.setLevel(lvl)." + self.logger.setLevel(lvl) + + def set_format(self, fmt): + "Set the output format to a new format string" + # Use a regular expression to identify all fields that the format will + # populate, based on limited printf-style formatting. + # https://docs.python.org/3/library/stdtypes.html#old-string-formatting + regexmatch = "%\((?P.*?)\)[diouxXeEfFgGcrs]" + self.format_fields = re.findall(regexmatch, fmt) + + self.format = fmt + + def __incrementFilename(self, filename): + """ + Used with the noclobber argument. + Creates a distinct filename by appending a sequence number. + """ + try: + while os.stat(filename): + p = filename.rsplit('-', 1) + try: + p, n = p[0], int(p[1]) + except ValueError: + n = 0 + filename = '-'.join(p + ['%04d' % (int(n) + 1)]) + except OSError: + pass # file not found + return filename + + def setup(self): + """ + Perform any additional setup outside of the standard __init__. + For example, printing header data to the outfile. + """ + pass + + def close(self): + "Close output file, assuming it's not stdout" + if self.fh not in (sys.stdout, sys.stdout.buffer): + self.fh.close() + + def log(self, msg, level=logging.INFO, *args, **kwargs): + """ + Write a message to the log + Passes all args and kwargs thru to logging, except for 'level' + """ + self.logger.log(level, msg, *args, **kwargs) + + def convert(self, *args, **kwargs): + """ + Attempts to convert the args/kwargs into the format defined in + self.format and self.timeformat + """ + # Have the keyword arguments default to empty strings, in the event + # of missing keys for string formatting + outdict = defaultdict(str, **kwargs) + outformat = self.format + extras = [] + + # Convert raw timestamps into a datetime object + if 'ts' in outdict: + try: + outdict['ts'] = datetime.fromtimestamp(float(outdict['ts'])) + outdict['ts'] = outdict['ts'].strftime(self.timeformat) + outdict['starttime'] = datetime.fromtimestamp(float(outdict['starttime'])) + outdict['starttime'] = outdict['starttime'].strftime(self.timeformat) + outdict['endtime'] = datetime.fromtimestamp(float(outdict['endtime'])) + outdict['endtime'] = outdict['endtime'].strftime(self.timeformat) + except TypeError: + pass + except KeyError: + pass + except ValueError: + pass + + # Create directional arrows + if 'dir_arrow' not in outdict: + if outdict.get('direction') == 'cs': + outdict['dir_arrow'] = '->' + elif outdict.get('direction') == 'sc': + outdict['dir_arrow'] = '<-' + else: + outdict['dir_arrow'] = '--' + + # Convert Nones into empty strings. + # If --extra flag used, generate string representing otherwise hidden + # fields. + for key, val in sorted(outdict.items()): + if val is None: + val = '' + outdict[key] = val + if self.extra: + if key not in self.format_fields: + extras.append("%s=%s" % (key, val)) + + # Dump the args into a 'data' field + outdict['data'] = self.delim.join(map(str, args)) + + # Create an optional 'extra' field + if self.extra: + if 'extra' not in self.format_fields: + outformat = outformat[:-1] + " [ %(extra)s ]\n" + outdict['extra'] = ', '.join(extras) + + # Convert the output dictionary into a string that is dumped to the + # output location. + output = outformat % outdict + return output + + def write(self, *args, **kwargs): + "Primary output function. Should be overwritten by subclasses." + line = self.convert(*args, **kwargs) + try: + self.fh.write(line) + if self.nobuffer: + self.fh.flush() + except BrokenPipeError: + pass + + def alert(self, *args, **kwargs): + """ + DEPRECATED + Use the write function of the AlertOutput class + """ + self.write(*args, **kwargs) + + def dump(self, *args, **kwargs): + """ + DEPRECATED + Use the write function of the PCAPOutput class + """ + self.write(*args, **kwargs) + + + +class QueueOutputWrapper(object): + """ + Wraps an instance of any other Output-like object to make its + write function more thread safe. + """ + + def __init__(self, oobject, oqueue): + self.__oobject = oobject + self.__owrite = oobject.write + self.queue = oqueue + self.id = str(self.__oobject) + + def true_write(self, *args, **kwargs): + "Calls the wrapped class's write function. Called from decode.py." + self.__owrite(*args, **kwargs) + + def write(self, *args, **kwargs): + """ + Adds a message to the queue indicating that this wrapper is ready to + run its write function + """ + self.queue.put((self.id, args, kwargs)) + + +############################################################################### + +# The "obj" variable is used in decode.py as a standard name for each output +# module's primary class. It technically imports this variable and uses it to +# construct an instance. +obj = Output diff --git a/dshell/output/pcapout.py b/dshell/output/pcapout.py new file mode 100644 index 0000000..96a7af3 --- /dev/null +++ b/dshell/output/pcapout.py @@ -0,0 +1,64 @@ +""" +This output module generates pcap output when given very specific arguments. +""" + +from dshell.output.output import Output +import struct +import sys + +# TODO get this module to work with ConnectionPlugins + +class PCAPOutput(Output): + "Writes data to a pcap file." + _DESCRIPTION = "Writes data to a pcap file (does not work with connection-based plugins)" + + def __init__(self, *args, **kwargs): + super().__init__(*args, mode='wb', **kwargs) + if self.fh == sys.stdout: + # Switch to a stdout that can handle byte output + self.fh = sys.stdout.buffer + # Since we have to wait until the link-layer type is set, we wait + # until the first write() operation before writing the pcap header + self.header_written = False + + def write(self, *args, **kwargs): + """ + Write a packet to the pcap file. + + Arguments: + pktlen : raw packet length + rawpkt : raw packet data string + ts : timestamp + link_layer_type : link-layer type (optional) (default: 1) + (e.g. 1 for Ethernet, 105 for 802.11, etc.) + """ + # The first time write() is called, the pcap header is written. + # This is to allow the plugin enough time to figure out what the + # link-layer type is for the data. + if not self.header_written: + link_layer_type = kwargs.get('link_layer_type', 1) + # write the header: + # magic_number, version_major, version_minor, thiszone, sigfigs, + # snaplen, link-layer type + self.fh.write( + struct.pack('IHHIIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, link_layer_type)) + self.header_written = True + + # Attempt to fetch the required fields + pktlen = kwargs.get('pktlen', None) + rawpkt = kwargs.get('rawpkt', None) + ts = kwargs.get('ts', None) + if pktlen is None or rawpkt is None or ts is None: + raise TypeError("PCAPOutput.write() requires at least these arguments to write packet data: pktlen, rawpkt, and ts.\n\tIt is possible this plugin is not configured to handle pcap output.") + + self.fh.write( + struct.pack('II', int(ts), int((ts - int(ts)) * 1000000))) + self.fh.write(struct.pack('II', len(rawpkt), pktlen)) + self.fh.write(rawpkt) + + def close(self): + if self.fh == sys.stdout.buffer: + self.fh = sys.stdout + super().close() + +obj = PCAPOutput diff --git a/dshell/plugins/__init__.py b/dshell/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/dhcp/__init__.py b/dshell/plugins/dhcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/dhcp/dhcp.py b/dshell/plugins/dhcp/dhcp.py new file mode 100644 index 0000000..3bb3755 --- /dev/null +++ b/dshell/plugins/dhcp/dhcp.py @@ -0,0 +1,102 @@ +""" +DHCP Plugin +""" + +import dshell.core +import dshell.util +from dshell.output.alertout import AlertOutput + +from pypacker.layer4 import udp +from pypacker.layer567 import dhcp + +from struct import unpack + +class DshellPlugin(dshell.core.PacketPlugin): + def __init__(self, **kwargs): + super().__init__(name='dhcp', + description='extract client information from DHCP messages', + longdescription=""" +The dhcp plugin will extract the Transaction ID, Hostname, and +Client ID (MAC address) from every UDP DHCP packet found in the given pcap +using port 67. DHCP uses BOOTP as its transport protocol. +BOOTP assigns port 67 for the 'BOOTP server' and port 68 for the 'BOOTP client'. +This filter pulls DHCP Inform packets. + +Examples: + + General usage: + + decode -d dhcp + + This will display the connection info including the timestamp, + the source IP : source port, destination IP : destination port, + Transaction ID, Client Hostname, and the Client MAC address + in a tabular format. + + + Malware Traffic Analysis Exercise Traffic from 2015-03-03 where a user was hit with an Angler exploit kit: + + We want to find out more about the infected machine, and some of this information can be pulled from DHCP traffic + + decode -d dhcp 2015-03-03-traffic-analysis-exercise.pcap + + OUTPUT: +[dhcp] 2015-03-03 14:05:10 172.16.101.196:68 -> 172.16.101.1:67 ** Transaction ID: 0xba5a2cfe Client ID (MAC): 38:2C:4A:3D:EF:01 Hostname: Gregory-PC ** +[dhcp] 2015-03-03 14:08:40 172.16.101.196:68 -> 255.255.255.255:67 ** Transaction ID: 0x6a482406 Client ID (MAC): 38:2C:4A:3D:EF:01 Hostname: Gregory-PC ** +[dhcp] 2015-03-03 14:10:11 172.16.101.196:68 -> 172.16.101.1:67 ** Transaction ID: 0xe74b17fe Client ID (MAC): 38:2C:4A:3D:EF:01 Hostname: Gregory-PC ** +[dhcp] 2015-03-03 14:12:50 172.16.101.196:68 -> 255.255.255.255:67 ** Transaction ID: 0xd62614a0 Client ID (MAC): 38:2C:4A:3D:EF:01 Hostname: Gregory-PC ** +""", + bpf='(udp and port 67)', + output=AlertOutput(label=__name__), + author='dek', + ) + self.mac_address = None + self.client_hostname = None + self.xid = None + + # A packetHandler is used to ensure that every DHCP packet in the traffic is parsed + def packet_handler(self, pkt): + + # iterate through the layers and find the DHCP layer + dhcp_packet = pkt.pkt.upper_layer + while not isinstance(dhcp_packet, dhcp.DHCP): + try: + dhcp_packet = dhcp_packet.upper_layer + except AttributeError: + # There doesn't appear to be a DHCP layer + return + + # Pull the transaction ID from the packet + self.xid = hex(dhcp_packet.xid) + + # if we have a DHCP INFORM PACKET + if dhcp_packet.op == dhcp.DHCP_OP_REQUEST: + for opt in list(dhcp_packet.opts): + try: + option_code = opt.type + msg_value = opt.body_bytes + except AttributeError: + continue + + # if opt is CLIENT_ID (61) + # unpack the msg_value and reformat the MAC address + if option_code == dhcp.DHCP_OPT_CLIENT_ID: + hardware_type, mac = unpack('B6s', msg_value) + mac = mac.hex().upper() + self.mac_address = ':'.join([mac[i:i+2] for i in range(0, len(mac), 2)]) + + # if opt is HOSTNAME (12) + elif option_code == dhcp.DHCP_OPT_HOSTNAME: + self.client_hostname = msg_value.decode('utf-8') + + # Allow for unknown hostnames + if not self.client_hostname: + self.client_hostname = "" + + if self.xid and self.mac_address: + self.write('Transaction ID: {0:<12} Client ID (MAC): {1:<20} Hostname: {2:<}'.format( + self.xid, self.mac_address, self.client_hostname), **pkt.info(), dir_arrow='->') + return pkt + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/dns/__init__.py b/dshell/plugins/dns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/dns/dns.py b/dshell/plugins/dns/dns.py new file mode 100644 index 0000000..afb9e0b --- /dev/null +++ b/dshell/plugins/dns/dns.py @@ -0,0 +1,174 @@ +""" +Extracts and summarizes DNS queries and responses. +""" + +import dshell.core +from dshell.plugins import dnsplugin +from dshell.output.alertout import AlertOutput + +from pypacker.pypacker import dns_name_decode +from pypacker.layer567 import dns + +import ipaddress + +RESPONSE_ERRORS = { + dns.DNS_RCODE_FORMERR: "FormErr", + dns.DNS_RCODE_SERVFAIL: "ServFail", + dns.DNS_RCODE_NXDOMAIN: "NXDOMAIN", + dns.DNS_RCODE_NOTIMP: "NotImp", + dns.DNS_RCODE_REFUSED: "Refused", + dns.DNS_RCODE_YXDOMAIN: "YXDp,aom", + dns.DNS_RCODE_YXRRSET: "YXRRSet", + dns.DNS_RCODE_NXRRSET: "NXRRSet", + dns.DNS_RCODE_NOTAUTH: "NotAuth", + dns.DNS_RCODE_NOTZONE: "NotZone", +} + +class DshellPlugin(dnsplugin.DNSPlugin): + + def __init__(self, *args, **kwargs): + super().__init__( + name="DNS", + description="Extract and summarize DNS queries/responses", + longdescription=""" +The DNS plugin extracts and summarizes DNS queries and their responses. If +possible, each query is paired with its response(s). + +Possible anomalies can be found using the --dns_show_noanswer, +--dns_only_noanswer, --dns_show_norequest, or --dns_only_norequest flags +(see --help). + +For example, looking for responses that did not come from a request: + decode -d dns --dns_only_norequest + +Additional information for responses can be seen with --dns_country and +--dns_asn to show country codes and ASNs, respectively. These results can be +piped to grep for filtering results. + +For example, to look for all traffic from Germany: + decode -d dns --dns_country |grep "country: DE" + +To look for non-US traffic, try: + decode -d dns --dns_country |grep "country:" |grep -v "country: US" +""", + author="bg/twp", + bpf="udp and port 53", + output=AlertOutput(label=__name__), + optiondict={'show_noanswer': {'action': 'store_true', 'help': 'report unanswered queries alongside other queries'}, + 'show_norequest': {'action': 'store_true', 'help': 'report unsolicited responses alongside other responses'}, + 'only_noanswer': {'action': 'store_true', 'help': 'report only unanswered queries'}, + 'only_norequest': {'action': 'store_true', 'help': 'report only unsolicited responses'}, + 'country': {'action': 'store_true', 'help': 'show country code for returned IP addresses'}, + 'asn': {'action': 'store_true', 'help': 'show ASN for returned IP addresses'}, + } + ) + + def premodule(self): + if self.only_norequest: + self.show_norequest = True + if self.only_noanswer: + self.show_noanswer = True + + + def dns_handler(self, conn, requests, responses): + if self.only_norequest and requests is not None: + return + if self.only_noanswer and responses is not None: + return + if not self.show_norequest and requests is None: + return + if not self.show_noanswer and responses is None: + return + + msg = [] + + # For simplicity, we focus only on the last request if there's more + # than one. + if requests: + request_pkt = requests[-1] + request = request_pkt.pkt.highest_layer + id = request.id + for query in request.queries: + if query.type == dns.DNS_A: + msg.append("A? {}".format(query.name_s)) + elif query.type == dns.DNS_AAAA: + msg.append("AAAA? {}".format(query.name_s)) + elif query.type == dns.DNS_CNAME: + msg.append("CNAME? {}".format(query.name_s)) + elif query.type == dns.DNS_LOC: + msg.append("LOC? {}".format(query.name_s)) + elif query.type == dns.DNS_MX: + msg.append("MX? {}".format(query.name_s)) + elif query.type == dns.DNS_PTR: + msg.append("PTR? {}".format(query.name_s)) + elif query.type == dns.DNS_SRV: + msg.append("SRV? {}".format(query.name_s)) + elif query.type == dns.DNS_TXT: + msg.append("TXT? {}".format(query.name_s)) + else: + request = None + + if responses: + response_pkt = responses[-1] + for response in responses: + rcode = response.rcode + response = response.pkt.highest_layer + id = response.id + # Check for errors in the response code + err = RESPONSE_ERRORS.get(rcode, None) + if err: + msg.append(err) + continue + # Get the response counts + msg.append("{}/{}/{}".format(response.answers_amount, response.authrr_amount, response.addrr_amount)) + # Parse the answers from the response + for answer in response.answers: + if answer.type == dns.DNS_A or answer.type == dns.DNS_AAAA: + msg_fields = {} + msg_format = "A: {ip} (ttl {ttl}s)" + answer_ip = ipaddress.ip_address(answer.address) + msg_fields['ip'] = str(answer_ip) + msg_fields['ttl'] = str(answer.ttl) + if self.country: + msg_fields['country'] = dshell.core.geoip.geoip_country_lookup(msg_fields['ip']) or '--' + msg_format += " (country: {country})" + if self.asn: + msg_fields['asn'] = dshell.core.geoip.geoip_asn_lookup(msg_fields['ip']) + msg_format += " (ASN: {asn})" + msg.append(msg_format.format(**msg_fields)) + # TODO pypacker doesn't really parse CNAMEs out. We try + # to get what we can manually, but keep checking if + # if it gets officially included in pypacker + elif answer.type == dns.DNS_CNAME: + if request: + cname = dnsplugin.basic_cname_decode(request.queries[0].name, answer.address) + else: + cname = dns_name_decode(answer.address) + msg.append('CNAME: {!r}'.format(cname)) + elif answer.type == dns.DNS_LOC: + msg.append("LOC: {!s}".format(answer.address)) + elif answer.type == dns.DNS_MX: + msg.append('MX: {!s}'.format(answer.address)) + elif answer.type == dns.DNS_NS: + msg.append('NS: {!s}'.format(answer.address)) + elif answer.type == dns.DNS_PTR: + ptr = dns_name_decode(answer.address) + msg.append('PTR: {!s}'.format(ptr)) + elif answer.type == dns.DNS_SRV: + msg.append('SRV: {!s}'.format(answer.address)) + elif answer.type == dns.DNS_TXT: + msg.append('TXT: {!s}'.format(answer.address)) + + else: + msg.append("No response") + + msg.insert(0, "ID: {}".format(id)) + msg = ", ".join(msg) + if request: + self.write(msg, **request_pkt.info()) + elif response: + self.write(msg, **response_pkt.info()) + else: + self.write(msg, **conn.info()) + + return conn, requests, responses diff --git a/dshell/plugins/dns/dnscc.py b/dshell/plugins/dns/dnscc.py new file mode 100644 index 0000000..a58a98f --- /dev/null +++ b/dshell/plugins/dns/dnscc.py @@ -0,0 +1,84 @@ +""" +Identifies DNS queries and finds the country code of the record response. +""" + +import dshell.core +from dshell.plugins import dnsplugin +from dshell.output.alertout import AlertOutput + +from pypacker.pypacker import dns_name_decode +from pypacker.layer567 import dns + +import ipaddress + +class DshellPlugin(dnsplugin.DNSPlugin): + + def __init__(self, *args, **kwargs): + super().__init__( + name="DNS Country Code", + description="identify country code of DNS A/AAAA record responses", + bpf="port 53", + author="bg", + output=AlertOutput(label=__name__), + optiondict={ + 'foreign': { + 'action': 'store_true', + 'help': 'report responses in non-US countries' + }, + 'code': { + 'type': str, + 'help': 'filter on a specific country code (ex. US, DE, JP, etc.)' + } + } + ) + + def dns_handler(self, conn, requests, responses): + "pull out the A/AAAA queries from the last DNS request in a connection" + queries = [] + if requests: + request = requests[-1].pkt.highest_layer + id = request.id + for query in request.queries: + if query.type == dns.DNS_A: + queries.append("A? {}".format(query.name_s)) + elif query.type == dns.DNS_AAAA: + queries.append("AAAA? {}".format(query.name_s)) + queries = ', '.join(queries) + + answers = [] + if responses: + for response in responses: + response = response.pkt.highest_layer + id = response.id + for answer in response.answers: + if answer.type == dns.DNS_A: + ip = ipaddress.ip_address(answer.address).compressed + cc = dshell.core.geoip.geoip_country_lookup(ip) or '--' + if self.foreign and (cc == 'US' or cc == '--'): + continue + elif self.code and cc != self.code: + continue + answers.append("A: {} ({}) (ttl: {}s)".format( + ip, cc, answer.ttl)) + elif answer.type == dns.DNS_AAAA: + ip = ipaddress.ip_address(answer.address).compressed + if ip == '::': + cc = '--' + else: + cc = dshell.core.geoip.geoip_country_lookup(ip) or '--' + if self.foreign and (cc == 'US' or cc == '--'): + continue + elif self.code and cc != self.code: + continue + answers.append("AAAA: {} ({}) (ttl: {}s)".format( + ip, cc, answer.ttl)) + answers = ', '.join(answers) + + if answers: + msg = "ID: {}, {} / {}".format(id, queries, answers) + self.write(msg, queries=queries, answers=answers, **conn.info()) + return conn, requests, responses + else: + return + + diff --git a/dshell/plugins/dns/innuendo-dns.py b/dshell/plugins/dns/innuendo-dns.py new file mode 100644 index 0000000..d7fb508 --- /dev/null +++ b/dshell/plugins/dns/innuendo-dns.py @@ -0,0 +1,85 @@ +""" +Proof-of-concept Dshell plugin to detect INNUENDO DNS Channel + +Based on the short marketing video (http://vimeo.com/115206626) the +INNUENDO DNS Channel relies on DNS to communicate with an authoritative +name server. The name server will respond with a base64 encoded TXT +answer. This plugin will analyze DNS TXT queries and responses to +determine if it matches the network traffic described in the video. +There are multiple assumptions (*very poor*) in this detection plugin +but serves as a proof-of-concept detector. This detector has not been +tested against authentic INNUENDO DNS Channel traffic. +""" + + +from dshell.plugins.dnsplugin import DNSPlugin +from dshell.output.alertout import AlertOutput + +from pypacker.layer567 import dns + +import base64 + +class DshellPlugin(DNSPlugin): + """ + Proof-of-concept Dshell plugin to detect INNUENDO DNS Channel + + Usage: decode -d innuendo *.pcap + """ + + def __init__(self): + super().__init__( + name="innuendo-dns", + description="proof-of-concept detector for INNUENDO DNS channel", + bpf="port 53", + author="primalsec", + output=AlertOutput(label=__name__), + ) + + def dns_handler(self, conn, requests, responses): + response = responses[-1] + + query = None + answers = [] + + if requests: + request = requests[-1].pkt.highest_layer + query = request.queries[-1] + # DNS Question, extract query name if it is a TXT record request + if query.type == dns.DNS_TXT: + query = query.name_s + + if responses: + for response in responses: + rcode = response.rcode + response = response.pkt.highest_layer + # DNS Answer with data and no errors + if rcode == dns.DNS_RCODE_NOERR and response.answers: + for answer in response.answers: + if answer.type == dns.DNS_TXT: + answers.append(answer.address) + + if query and answers: + # assumption: INNUENDO will use the lowest level domain for C2 + # example: AAAABBBBCCCC.foo.bar.com -> AAAABBBBCCCC is the INNUENDO + # data + subdomain = query.split('.', 1)[0] + + # weak test based on video observation *very poor assumption* + if subdomain.isupper(): + # check each answer in the TXT response + for answer in answers: + try: + # INNUENDO DNS channel base64 encodes the response, check to see if + # it contains a valid base64 string *poor assumption* + dummy = base64.b64decode(answer) + + self.write('INNUENDO DNS Channel', query, '/', answer, **conn.info()) + + # here would be a good place to decrypt the payload (if you have the keys) + # decrypt_payload( answer ) + except: + return None + return conn, requests, responses + + return None + diff --git a/dshell/plugins/dns/specialips.py b/dshell/plugins/dns/specialips.py new file mode 100644 index 0000000..a5fc4e0 --- /dev/null +++ b/dshell/plugins/dns/specialips.py @@ -0,0 +1,110 @@ +""" +Identifies DNS resolutions that fall into special IP spaces (i.e. private, +reserved, loopback, multicast, link-local, or unspecified). + +When found, it will print an alert for the request/response pair. The alert +will include the type of special IP in parentheses: + (loopback) + (private) + (reserved) + (multicast) + (link-local) + (unspecified) +""" + +from dshell.plugins import dnsplugin +from dshell.output.alertout import AlertOutput + +from pypacker.layer567 import dns + +import ipaddress + + +class DshellPlugin(dnsplugin.DNSPlugin): + + def __init__(self, *args, **kwargs): + super().__init__( + name="special-ips", + description="identify DNS resolutions that fall into special IP (IPv4 and IPv6) spaces (i.e. private, reserved, loopback, multicast, link-local, or unspecified)", + bpf="port 53", + author="dev195", + output=AlertOutput(label=__name__), + longdescription=""" +Identifies DNS resolutions that fall into special IP spaces (i.e. private, +reserved, loopback, multicast, link-local, or unspecified). + +When found, it will print an alert for the request/response pair. The alert +will include the type of special IP in parentheses: + (loopback) + (private) + (reserved) + (multicast) + (link-local) + (unspecified) + +For example, to look for responses with private IPs: + Dshell> decode -d specialips ~/pcap/SkypeIRC.cap |grep "(private)" + [special-ips] 2006-08-25 15:31:06 192.168.1.2:2128 -- 192.168.1.1:53 ** ID: 12579, A? voyager.home., A: 192.168.1.1 (private) (ttl 10000s) ** + +Finding can also be written to a separate pcap file by chaining: + Dshell> decode -d specialips+pcapwriter --pcapwriter_outfile="special-dns.pcap" ~/pcap/example.pcap +""", + ) + + + def dns_handler(self, conn, requests, responses): + """ + Stores the DNS request, then iterates over responses looking for + special IP addresses. If it finds one, it will print an alert for the + request/response pair. + """ + msg = [] + + if requests: + request_pkt = requests[-1] + request = request_pkt.pkt.highest_layer + id = request.id + for query in request.queries: + if query.type == dns.DNS_A: + msg.append("A? {}".format(query.name_s)) + elif query.type == dns.DNS_AAAA: + msg.append("AAAA? {}".format(query.name_s)) + + + if responses: + keep_responses = False + for response in responses: + response = response.pkt.highest_layer + for answer in response.answers: + if answer.type == dns.DNS_A or answer.type == dns.DNS_AAAA: + answer_ip = ipaddress.ip_address(answer.address) + msg_fields = {} + msg_format = "A: {ip} ({type}) (ttl {ttl}s)" + msg_fields['ip'] = str(answer_ip) + msg_fields['ttl'] = str(answer.ttl) + msg_fields['type'] = '' + if answer_ip.is_loopback: + msg_fields['type'] = 'loopback' + keep_responses = True + elif answer_ip.is_private: + msg_fields['type'] = 'private' + keep_responses = True + elif answer_ip.is_reserved: + msg_fields['type'] = 'reserved' + keep_responses = True + elif answer_ip.is_multicast: + msg_fields['type'] = 'multicast' + keep_responses = True + elif answer_ip.is_link_local: + msg_fields['type'] = 'link-local' + keep_responses = True + elif answer_ip.is_unspecified: + msg_fields['type'] = 'unspecified' + keep_responses = True + msg.append(msg_format.format(**msg_fields)) + if keep_responses: + msg.insert(0, "ID: {}".format(id)) + msg = ", ".join(msg) + self.write(msg, **conn.info()) + return conn, requests, responses + diff --git a/dshell/plugins/dnsplugin.py b/dshell/plugins/dnsplugin.py new file mode 100644 index 0000000..48da51b --- /dev/null +++ b/dshell/plugins/dnsplugin.py @@ -0,0 +1,124 @@ +""" +This is a base-level plugin intended to handle DNS lookups and responses + +It inherits from the base ConnectionPlugin and provides a new handler +function: dns_handler(conn, requests, responses) + +It automatically pairs request/response packets by ID and passes them to the +handler for a custom plugin, such as dns.py, to use. +""" + +import dshell.core as dshell + +from pypacker.pypacker import dns_name_decode +from pypacker.layer567 import dns + +import struct + +def basic_cname_decode(request, answer): + """ + DIRTY HACK ALERT + + This function exists to convert DNS CNAME responses into human-readable + strings. pypacker cannot currently convert these, so this one attempts + to do it. However, it is not complete and will only work for the most + common situations (i.e. no pointers, or pointers that only point to the + first request). + + Feed it the bytes (query.name) of the first request and the bytes for the + answer (answer.address) with a CNAME, and it will return the parsed string. + """ + + if b"\xc0" not in answer: + # short-circuit if there is no pointer + return dns_name_decode(answer) + # Get the offset into the question by grabbing the number after \xc0 + # Then, offset the offset by subtracting the query header length (12) + snip_index = answer[answer.index(b"\xc0") + 1] - 12 + # Grab the necessary piece from the request + snip = request[snip_index:] + # Reassemble and return + rebuilt = answer[:answer.index(b"\xc0")] + snip + return dns_name_decode(rebuilt) + +class DNSPlugin(dshell.ConnectionPlugin): + """ + A base-level plugin that overwrites the connection_handler in + ConnectionPlugin. It provides a new handler function: dns_handler. + """ + + def __init__(self, **kwargs): + dshell.ConnectionPlugin.__init__(self, **kwargs) + + def connection_handler(self, conn): + requests = {} + responses = {} + id_to_blob_map = {} + id_to_packets_map = {} + + for blob in conn.blobs: + for pkt in blob.all_packets: + packet = pkt.pkt + if not isinstance(packet.highest_layer, dns.DNS): + # First packet is not DNS, so we don't care + blob.hidden = True + break + + dnsp = packet.highest_layer + id_to_blob_map.setdefault(dnsp.id, []).append(blob) + id_to_packets_map.setdefault(dnsp.id, []).append(pkt) + qr_flag = dnsp.flags >> 15 + rcode = dnsp.flags & 15 + setattr(pkt, 'qr', qr_flag) + setattr(pkt, 'rcode', rcode) +# print("{0:016b}".format(dnsp.flags)) + if qr_flag == dns.DNS_Q: + requests.setdefault(dnsp.id, []).append(pkt) + elif qr_flag == dns.DNS_A: + responses.setdefault(dnsp.id, []).append(pkt) + + all_ids = set(list(requests.keys()) + list(responses.keys())) + keep_connection = False + for id in all_ids: + request_list = requests.get(id, None) + response_list = responses.get(id, None) + dns_handler_out = self.dns_handler(conn, requests=request_list, responses=response_list) + if not dns_handler_out: + # remove packets from connections that dns_handler did not like + for blob in id_to_blob_map[id]: + for pkt in id_to_packets_map[id]: + try: blob.all_packets.remove(pkt) + except ValueError: continue + else: + for blob in id_to_blob_map[id]: + blob.hidden = False + try: + if dns_handler_out and not isinstance(dns_handler_out[0], dshell.Connection): + self.warn("The output from {} dns_handler must be a list with a dshell.Connection as the first element! Chaining plugins from here may not be possible.".format(self.name)) + continue + except TypeError: + self.warn("The output from {} dns_handler must be a list with a dshell.Connection as the first element! Chaining plugins from here may not be possible.".format(self.name)) + continue + keep_connection = True + if keep_connection: + return conn + + def dns_handler(self, conn, requests, responses): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites + on DNS data. + + It takes in a Connection, a list of requests (or None), and a list of + responses (or None). The requests and responses are not intermixed; + the responses in the list correspond to the requests according to ID. + + It should return a list containing the same types of values that came + in as arguments (i.e. return (conn, requests, responses)). This is + mostly a consistency thing, as only the Connection is passed along to + other plugins. + """ + return (conn, requests, responses) + +DshellPlugin = None diff --git a/dshell/plugins/filter/__init__.py b/dshell/plugins/filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/filter/country.py b/dshell/plugins/filter/country.py new file mode 100644 index 0000000..47f90ec --- /dev/null +++ b/dshell/plugins/filter/country.py @@ -0,0 +1,102 @@ +""" +A filter for connections by IP address country code. Will generally be chained +with other plugins. +""" + +import dshell.core +from dshell.output.netflowout import NetflowOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + def __init__(self, *args, **kwargs): + super().__init__( + name="Country Filter", + bpf='ip or ip6', + description="filter connections by IP address country code", + longdescription=""" +country: filter connections on geolocation (country code) + +Mandatory option: + + --country_code: specify (2 character) country code to filter on + +Default behavior: + + If either the client or server IP address matches the specified country, + the stream will be included. + +Modifier options: + + --country_neither: Include only streams where neither the client nor the + server IP address matches the specified country. + + --country_both: Include only streams where both the client AND the server + IP addresses match the specified country. + + --country_notboth: Include streams where the specified country is NOT BOTH + the client and server IP. Streams where it is one or + the other may be included. + + --country_alerts: Show alerts for this plugin (default: false) + + +Example: + + decode -d country+pcapwriter traffic.pcap --pcapwriter_outfile=USonly.pcap --country_code US + decode -d country+followstream traffic.pcap --country_code US --country_notboth +""", + author="tp", + output=NetflowOutput(label=__name__), + optiondict={ + 'code': {'type': str, 'help': 'two-char country code', 'metavar':'CC'}, + 'neither': {'action': 'store_true', 'help': 'neither (client/server) is in specified country'}, + 'both': {'action': 'store_true', 'help': 'both (client/server) ARE in specified country'}, + 'notboth': {'action': 'store_true', 'help': 'specified country is not both client and server'}, + 'alerts': {'action': 'store_true', 'default':False, 'help':'have this filter show alerts for matches'} + }, + ) + + def premodule(self): + # Several of the args are mutually exclusive + # Check if more than one is set, and print a warning if so + if (self.neither + self.both + self.notboth) > 1: + self.warn("Can only use one of these args at a time: 'neither', 'both', or 'notboth'") + + def connection_handler(self, conn): + # If no country code specified, pass all traffic through + if not self.code: + return conn + + if self.neither: + if conn.clientcc != self.code and conn.servercc != self.code: + if self.alerts: self.write('neither', **conn.info()) + return conn + else: + return + + elif self.both: + if conn.clientcc == self.code and conn.servercc == self.code: + if self.alerts: self.write('both', **conn.info()) + return conn + else: + return + + elif self.notboth: + if ((conn.clientcc != self.code and conn.servercc == self.code) + or + (conn.clientcc == self.code and conn.servercc != self.code)): + if self.alerts: self.write('notboth', **conn.info()) + return conn + else: + return + + else: + if conn.clientcc == self.code or conn.servercc == self.code: + if self.alerts: self.write('match', **conn.info()) + return conn + + # no match + return None + + +if __name__ == "__main__": + print (DshellPlugin()) diff --git a/dshell/plugins/filter/track.py b/dshell/plugins/filter/track.py new file mode 100644 index 0000000..eeab990 --- /dev/null +++ b/dshell/plugins/filter/track.py @@ -0,0 +1,153 @@ +""" +Only follows connections that match user-provided IP addresses and ports. Is +generally chained with other plugins. +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + +import ipaddress +import sys + +class DshellPlugin(dshell.core.ConnectionPlugin): + def __init__(self, **kwargs): + super().__init__( + name="track", + author="twp,dev195", + description="Only follow connections that match user-provided IP addresses and ports", + longdescription="""Only follow connections that match user-provided IP addresses + +IP addresses can be specified with --track_source and --track_target. +Multiple IPs can be used with commas (e.g. --track_source=192.168.1.1,127.0.0.1). +Ports can be included with IP addresses by joining them with a 'p' (e.g. --track_target=192.168.1.1p80,127.0.0.1). +Ports can be used alone with just a 'p' (e.g. --track_target=p53). +CIDR notation is okay (e.g. --track_source=196.168.0.0/16). + +--track_source : used to limit connections by the IP that initiated the connection (usually the client) +--trace_target : used to limit connections by the IP that received the connection (usually the server) +--track_alerts : used to display optional alerts indicating when a connection starts/ends""", + bpf="ip or ip6", + output=AlertOutput(label=__name__), + optiondict={ + "target": { + "default": [], + "action": "append", + "metavar": "IPpPORT"}, + "source": { + "default": [], + "action": "append", + "metavar": "IPpPORT"}, + "alerts": { + "action": "store_true"} + } + ) + self.sources = [] + self.targets = [] + + def __split_ips(self, input): + """ + Used to split --track_target and --track_source arguments into + list-of-lists used in the connection handler + """ + return_val = [] + for piece in input.split(','): + if 'p' in piece: + ip, port = piece.split('p', 1) + try: + port = int(port) + except ValueError as e: + self.error("Could not parse port number in {!r} - {!s}".format(piece, e)) + sys.exit(1) + if 0 < port > 65535: + self.error("Could not parse port number in {!r} - must be in valid port range".format(piece)) + sys.exit(1) + else: + ip, port = piece, None + if '/' in ip: + try: + ip = ipaddress.ip_network(ip) + except ValueError as e: + self.error("Could not parse CIDR netrange - {!s}".format(e)) + sys.exit(1) + elif ip: + try: + ip = ipaddress.ip_address(ip) + except ValueError as e: + self.error("Could not parse IP address - {!s}".format(e)) + sys.exit(1) + else: + ip = None + return_val.append((ip, port)) + return return_val + + def __check_ips(self, masterip, masterport, checkip, checkport): + "Checks IPs and ports for matches against the user-selected values" + # masterip, masterport are the values selected by the user + # checkip, checkport are the values to be checked against masters + ip_okay = False + port_okay = False + + if masterip is None: + ip_okay = True + elif (isinstance(masterip, (ipaddress.IPv4Network, ipaddress.IPv6Network)) + and checkip in masterip): + ip_okay = True + elif (isinstance(masterip, (ipaddress.IPv4Address, ipaddress.IPv6Address)) + and masterip == checkip): + ip_okay = True + + if masterport is None: + port_okay = True + elif masterport == checkport: + port_okay = True + + if port_okay and ip_okay: + return True + else: + return False + + + def premodule(self): + if self.target: + for tstr in self.target: + self.targets.extend(self.__split_ips(tstr)) + if self.source: + for sstr in self.source: + self.sources.extend(self.__split_ips(sstr)) + self.debug("targets: {!s}".format(self.targets)) + self.debug("sources: {!s}".format(self.sources)) + + def connection_handler(self, conn): + if self.targets: + conn_okay = False + for target in self.targets: + targetip = target[0] + targetport = target[1] + serverip = ipaddress.ip_address(conn.serverip) + serverport = conn.serverport + if self.__check_ips(targetip, targetport, serverip, serverport): + conn_okay = True + break + if not conn_okay: + return + + if self.sources: + conn_okay = False + for source in self.sources: + sourceip = source[0] + sourceport = source[1] + clientip = ipaddress.ip_address(conn.clientip) + clientport = conn.clientport + if self.__check_ips(sourceip, sourceport, clientip, clientport): + conn_okay = True + break + if not conn_okay: + return + + if self.alerts: + self.write("matching connection", **conn.info()) + + return conn + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/flows/__init__.py b/dshell/plugins/flows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/flows/largeflows.py b/dshell/plugins/flows/largeflows.py new file mode 100644 index 0000000..ee40046 --- /dev/null +++ b/dshell/plugins/flows/largeflows.py @@ -0,0 +1,38 @@ +""" +Displays netflows that have at least 1MB transferred, by default. +Megabyte threshold can be updated by the user. +""" + +import dshell.core +from dshell.output.netflowout import NetflowOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="large-flows", + description="Display netflows that have at least 1MB transferred", + author="bg", + output=NetflowOutput(label=__name__), + optiondict={ + 'size': { + 'type': float, + 'default': 1, + 'metavar': 'SIZE', + 'help': 'number of megabytes transferred (default: 1)'} + } + ) + + def premodule(self): + if self.size <= 0: + self.warn("Cannot have a size that's less than or equal to zero (size: {}). Setting to 1.".format(self.size)) + self.size = 1 + self.min = 1048576 * self.size + self.debug("Input: {}, Final size: {} bytes".format(self.size, self.min)) + + def connection_handler(self, conn): + if conn.clientbytes + conn.serverbytes >= self.min: + self.write(**conn.info()) + return conn + + diff --git a/dshell/plugins/flows/longflows.py b/dshell/plugins/flows/longflows.py new file mode 100644 index 0000000..3bccfc9 --- /dev/null +++ b/dshell/plugins/flows/longflows.py @@ -0,0 +1,38 @@ +""" +Displays netflows that have a duration of at least 5 minutes. +Minute threshold can be updated by the user. +""" + +import dshell.core +from dshell.output.netflowout import NetflowOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="long-flows", + description="Display netflows that have a duration of at least 5 minutes", + author="bg", + output=NetflowOutput(label=__name__), + optiondict={ + "len": { + "type": float, + "default": 5, + "help": "set minimum connection time to MIN minutes (default: 5)", + "metavar": "MIN", + } + } + ) + + def premodule(self): + if self.len <= 0: + self.warn("Cannot have a time that's less than or equal to zero (size: {}). Setting to 5.".format(self.len)) + self.len = 5 + self.secs = 60 * self.len + + def connection_handler(self, conn): + tdelta = (conn.endtime - conn.starttime).total_seconds() + if tdelta >= self.secs: + self.write(**conn.info()) + return conn + diff --git a/dshell/plugins/flows/netflow.py b/dshell/plugins/flows/netflow.py new file mode 100644 index 0000000..89a30e9 --- /dev/null +++ b/dshell/plugins/flows/netflow.py @@ -0,0 +1,20 @@ +""" +Collects and displays statistics about connections (a.k.a. flow data) +""" + +import dshell.core +from dshell.output.netflowout import NetflowOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + def __init__(self, *args, **kwargs): + super().__init__( + name="Netflow", + description="Collects and displays statistics about connections", + author="dev195", + bpf="ip or ip6", + output=NetflowOutput(label=__name__), + ) + + def connection_handler(self, conn): + self.write(**conn.info()) + return conn diff --git a/dshell/plugins/flows/reverseflows.py b/dshell/plugins/flows/reverseflows.py new file mode 100644 index 0000000..921458a --- /dev/null +++ b/dshell/plugins/flows/reverseflows.py @@ -0,0 +1,84 @@ +""" +Generate an alert when a client transmits more data than the server. + +Additionally, the user can specify a threshold. This means that an alert +will be generated if the client transmits more than three times as much data +as the server. + +The default threshold value is 3.0, meaning that any client transmits +more than three times as much data as the server will generate an alert. + +Examples: +1) decode -d reverse-flow + Generates an alert for client transmissions that are three times + greater than the server transmission. + +2) decode -d reverse-flow --reverse-flow_threshold 61 + Generates an alert for all client transmissions that are 61 times + greater than the server transmission + +3) decode -d reverse-flow --reverse-flow_threshold 61 --reverse-flow_zero + Generates an alert for all client transmissions that are 61 times greater + than the server transmission. +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="reverse-flows", + description="Generate an alert if the client transmits more data than the server", + author="me", + bpf="tcp or udp", + output=AlertOutput(label=__name__), + optiondict={ + 'threshold': {'type':float, 'default':3.0, + 'help':'Alerts if client transmits more than threshold times the data of the server'}, + 'minimum': {'type':int, 'default':0, + 'help':'alert on client transmissions larger than min bytes [default: 0]'}, + 'zero': {'action':'store_true', 'default':False, + 'help':'alert if the server transmits zero bytes [default: false]'}, + }, + longdescription=""" +Generate an alert when a client transmits more data than the server. + +Additionally, the user can specify a threshold. This means that an alert +will be generated if the client transmits more than three times as much data +as the server. + +The default threshold value is 3.0, meaning that any client transmits +more than three times as much data as the server will generate an alert. + +Examples: +1) decode -d reverse-flow + Generates an alert for client transmissions that are three times + greater than the server transmission. + +2) decode -d reverse-flow --reverse-flow_threshold 61 + Generates an alert for all client transmissions that are 61 times + greater than the server transmission + +3) decode -d reverse-flow --reverse-flow_threshold 61 --reverse-flow_zero + Generates an alert for all client transmissions that are 61 times greater + than the server transmission. + """, + ) + + def premodule(self): + if self.threshold < 0: + self.warn("Cannot have a negative threshold. Defaulting to 3.0. (threshold: {0})".format(self.threshold)) + self.threshold = 3.0 + elif not self.threshold: + self.warn("Threshold not set. Displaying all client-server transmissions (threshold: {0})".format(self.threshold)) + + def connection_handler(self, conn): + if conn.clientbytes < self.minimum: + return + + if self.zero or (conn.serverbytes and float(conn.clientbytes)/conn.serverbytes > self.threshold): + self.write('client sent {:>6.2f} more than the server'.format(conn.clientbytes/float(conn.serverbytes)), **conn.info(), dir_arrow="->") + return conn + diff --git a/dshell/plugins/flows/toptalkers.py b/dshell/plugins/flows/toptalkers.py new file mode 100644 index 0000000..b26f2df --- /dev/null +++ b/dshell/plugins/flows/toptalkers.py @@ -0,0 +1,83 @@ +""" +Finds the top-talkers in a file or on an interface based on byte count. +""" + +import dshell.core +from dshell.output.alertout import AlertOutput +from dshell.util import human_readable_filesize + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self, *args, **kwargs): + super().__init__( + name="Top Talkers", + description="Find top-talkers based on byte count", + author="dev195", + bpf="tcp or udp", + output=AlertOutput(label=__name__), + optiondict={ + "top_x": { + "type": int, + "default": 20, + "help": "Only display the top X results (default: 20)", + "metavar": "X" + }, + "total": { + "action": "store_true", + "help": "Sum byte counts from both directions instead of separate entries for individual directions" + }, + "h": { + "action": "store_true", + "help": "Print byte counts in human-readable format" + } + }, + longdescription=""" +Finds top 20 connections with largest transferred byte count. + +Can be configured to display an arbitrary Top X list with arguments. + +Does not pass connections down plugin chain. +""" + ) + + def premodule(self): + """ + Initialize a list to hold the top X talkers + Format of each entry: + (bytes, direction, Connection object) + """ + self.top_talkers = [(0, '---', None)] + + def connection_handler(self, conn): + if self.total: + # total up the client and server bytes + self.__process_bytes(conn.clientbytes + conn.serverbytes, '<->', conn) + else: + # otherwise, treat client and server bytes separately + self.__process_bytes(conn.clientbytes, '-->', conn) + self.__process_bytes(conn.serverbytes, '<--', conn) + + def postmodule(self): + "Iterate over the entries in top_talkers list and print them" + for bytecount, direction, conn in self.top_talkers: + if conn is None: + break + if self.h: + byte_display = human_readable_filesize(bytecount) + else: + byte_display = "{} B".format(bytecount) + msg = "client {} server {}".format(direction, byte_display) + self.write(msg, **conn.info(), dir_arrow="->") + + def __process_bytes(self, bytecount, direction, conn): + """ + Check if the bytecount for a connection belongs in top_talkers + If so, insert it into the list and pop off the lowest entry + """ + for i, oldbytecount in enumerate(self.top_talkers): + if bytecount >= oldbytecount[0]: + self.top_talkers.insert(i, (bytecount, direction, conn)) + break + + while len(self.top_talkers) > self.top_x: + self.top_talkers.pop(-1) diff --git a/dshell/plugins/ftp/__init__.py b/dshell/plugins/ftp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/ftp/ftp.py b/dshell/plugins/ftp/ftp.py new file mode 100644 index 0000000..29328f2 --- /dev/null +++ b/dshell/plugins/ftp/ftp.py @@ -0,0 +1,352 @@ +""" +Goes through TCP connections and tries to find FTP control channels and +associated data channels. Optionally, it will write out any file data it +sees into a separate directory. + +If a data connection is seen, it prints a message indicating the user, pass, +and file requested. If the --ftp_dump flag is set, it also dumps the file into the +--ftp_outdir directory. +""" + +import dshell.core +import dshell.util +from dshell.output.alertout import AlertOutput + +import os +import re +import sys + +# constants for channel type +CTRL_CONN = 0 +DATA_CONN = 1 + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="ftp", + description="alerts on FTP traffic and, optionally, rips files", + longdescription=""" +Goes through TCP connections and tries to find FTP control channels and +associated data channels. Optionally, it will write out any file data it +sees into a separate directory. + +If a data connection is seen, it prints a message indicating the user, pass, +and file requested. If the --ftp_dump flag is set, it also dumps the file into the +--ftp_outdir directory. +""", + author="amm,dev195", + bpf="tcp", + output=AlertOutput(label=__name__), + optiondict={ + "ports": { + 'help': 'comma-separated list of ports to watch for control connections (default: 21)', + 'metavar': 'PORT,PORT,PORT,[...]', + 'default': '21'}, + "dump": { + 'action': 'store_true', + 'help': 'dump files from stream'}, + "outdir": { + 'help': 'directory to write output files (default: "ftpout")', + 'metavar': 'DIRECTORY', + 'default': 'ftpout'} + } + ) + + def __update_bpf(self): + """ + Dynamically change the BPF to allow processing of data transfer + channels. + """ + dynfilters = [] + for conn, metadata in self.conns.items(): + try: + dynfilters += ["(host %s and host %s)" % metadata["tempippair"]] + except (KeyError, TypeError): + continue + for a, p in self.data_channel_map.keys(): + dynfilters += ["(host %s and port %d)" % (a, p)] + self.bpf = "(%s) and ((%s)%s)" % ( + self.original_bpf, + " or ".join( "port %d" % p for p in self.control_ports ), + " or " + " or ".join(dynfilters) if dynfilters else "" + ) + self.recompile_bpf() + + def premodule(self): + # dictionary containing metadata for connections + self.conns = {} + # dictionary mapping data channels (host, port) to their control channels + self.data_channel_map = {} + # ports used for control channels + self.control_ports = set() + # Original BPF without manipulation + self.original_bpf = self.bpf + # set control ports using user-provided info + for p in self.ports.split(','): + try: + self.control_ports.add(int(p)) + except ValueError as e: + self.error("{!r} is not a valid port. Skipping.".format(p)) + if not self.control_ports: + self.error("Could not find any control ports. At least one must be set for this plugin.") + sys.exit(1) + + # create output directory + # break if it cannot be created + if self.dump and not os.path.exists(self.outdir): + try: + os.makedirs(self.outdir) + except (IOError, OSError) as e: + self.error("Could not create output directory: {!r}: {!s}" + .format(self.outdir, e)) + sys.exit(1) + + def connection_init_handler(self, conn): + # Create metadata containers for any new connections + if conn.serverport in self.control_ports: + self.conns[conn.addr] = { + 'mode': CTRL_CONN, + 'user': '', + 'pass': '', + 'path': [], + 'datachan': None, + 'lastcommand': '', + 'tempippair': None, + 'filedata': None, + 'file': ['', '', ''] + } + elif self.dump and (conn.clientip, conn.clientport) in self.data_channel_map: + self.conns[conn.addr] = { + 'mode': DATA_CONN, + 'ctrlchan': self.data_channel_map[(conn.clientip, conn.clientport)], + 'filedata': None + } + elif self.dump and (conn.serverip, conn.serverport) in self.data_channel_map: + self.conns[conn.addr] = { + 'mode': DATA_CONN, + 'ctrlchan': self.data_channel_map[(conn.serverip, conn.serverport)], + 'filedata': None + } + elif self.dump: + # This is a data connection with an unknown control connection. It + # may be a passive mode transfer without known port info, yet. + self.conns[conn.addr] = { + 'mode': DATA_CONN, + 'ctrlchan': None, + 'filedata': None + } + + def connection_close_handler(self, conn): + # After data channel closes, store file content in control channel's + # 'filedata' field. + # Control channel will write it to disk after it determines the + # filename. + try: + info = self.conns[conn.addr] + except KeyError: + return + + if self.dump and info['mode'] == DATA_CONN: + # find the associated control channel + if info['ctrlchan'] == None: + if (conn.clientip, conn.clientport) in self.data_channel_map: + info['ctrlchan'] = self.data_channel_map[(conn.clientip, conn.clientport)] + if (conn.serverip, conn.serverport) in self.data_channel_map: + info['ctrlchan'] = self.data_channel_map[(conn.serverip, conn.serverport)] + try: + ctrlchan = self.conns[info['ctrlchan']] + except KeyError: + return + # add data to control channel dictionary + for blob in conn.blobs: + if ctrlchan['filedata']: + ctrlchan['filedata'] += blob.data + else: + ctrlchan['filedata'] = blob.data + # update port list and data channel knowledge + if (conn.serverip, conn.serverport) == ctrlchan['datachan']: + del self.data_channel_map[ctrlchan['datachan']] + ctrlchan['datachan'] = None + self.__update_bpf() + if (conn.clientip, conn.clientport) == ctrlchan['datachan']: + del self.data_channel_map[ctrlchan['datachan']] + ctrlchan['datachan'] = None + self.__update_bpf() + del self.conns[conn.addr] + + elif info['mode'] == CTRL_CONN: + # clear control channels if they've been alerted on + if info['file'] == None: + del self.conns[conn.addr] + + def postmodule(self): + for addr, info in self.conns.items(): + if self.dump and 'filedata' in info and info['filedata']: + origname = info['file'][0] + '_' + os.path.join(*info['file'][1:3]) + outname = dshell.util.gen_local_filename(self.outdir, origname) + with open(outname, 'wb') as fh: + fh.write(info['filedata']) + numbytes = len(info['filedata']) + info['filedata'] = None + info['outfile'] = outname + msg = 'User: %s, Pass: %s, %s File: %s (Incomplete: %d bytes written to %s)' % (info['user'], info['pass'], info['file'][0], os.path.join(*info['file'][1:3]), numbytes, os.path.basename(outname)) + self.write(msg, **info) + + + def blob_handler(self, conn, blob): + try: + info = self.conns[conn.addr] + except KeyError: + # connection was not initialized correctly + # set the blob to hidden and move on + blob.hidden = True + return + + if info['mode'] == DATA_CONN: + return conn, blob + + try: + data = blob.data + data = data.decode('ascii') + except UnicodeDecodeError as e: + # Could not convert command data to readable ASCII + blob.hidden = True + return + + if blob.direction == 'cs': + # client-to-server: try and get the command issued + if ' ' not in data.rstrip(): + command = data.rstrip() + param = '' + else: + command, param = data.rstrip().split(' ', 1) + command = command.upper() + info['lastcommand'] = command + + if command == 'USER': + info['user'] = param + + elif command == 'PASS': + info['pass'] = param + + elif command == 'CWD': + info['path'].append(param) + + elif command == 'PASV' or command == 'EPSV': + if self.dump: + # Temporarily store the pair of IP addresses + # to open up the BPF filter until blob_handler processes + # the response with the full IP/Port information. + # Note: Due to the way blob processing works, we don't + # get this information until after the data channel is + # established. + info['tempippair'] = tuple( + sorted((conn.clientip, conn.serverip)) + ) + self.__update_bpf() + + # For file transfers (including LIST), store tuple + # (Direction, Path, Filename) in info['file'] + elif command == 'LIST': + if param == '': + info['file'] = ( + 'RETR', os.path.normpath(os.path.join(*info['path'])) + if len(info['path']) + else '', 'LIST' + ) + else: + info['file'] = ( + 'RETR', os.path.normpath(os.path.join(os.path.join(*info['path']), param)) + if len(info['path']) + else '', 'LIST' + ) + elif command == 'RETR': + info['file'] = ( + 'RETR', os.path.normpath(os.path.join(*info['path'])) + if len(info['path']) + else '', param + ) + elif command == 'STOR': + info['file'] = ( + 'STOR', os.path.normpath(os.path.join(*info['path'])) + if len(info['path']) + else '', param + ) + + # Responses + else: + # Rollback directory change unless 2xx response + if info['lastcommand'] == 'CWD' and data[0] != '2': + info['path'].pop() + # Write out files upon resonse to transfer commands + if info['lastcommand'] in ('LIST', 'RETR', 'STOR'): + if self.dump and info['filedata']: + origname = info['file'][0] + '_' + os.path.join(*info['file'][1:3]) + outname = dshell.util.gen_local_filename(self.outdir, origname) + with open(outname, 'wb') as fh: + fh.write(info['filedata']) + numbytes = len(info['filedata']) + info['filedata'] = None + info['outfile'] = outname + info.update(conn.info()) + msg = 'User: "{}", Pass: "{}", {} File: {} ({:,} bytes written to {})'.format( + info['user'], + info['pass'], + info['file'][0], + os.path.join(*info['file'][1:3]), + numbytes, + os.path.basename(outname) + ) + else: + info.update(conn.info()) + msg = 'User: "{}", Pass: "{}", {} File: {}'.format( + info['user'], + info['pass'], + info['file'][0], + os.path.join(*info['file'][1:3]) + ) + if data[0] not in ('1','2'): + msg += ' ({})'.format(data.rstrip()) + info['ts'] = blob.ts + if (blob.sip == conn.sip): + self.write(msg, **info, dir_arrow="->") + else: + self.write(msg, **info, dir_arrow="<-") + info['file'] = None + + # Handle EPSV mode port setting + if info['lastcommand'] == 'EPSV' and data[0] == '2': + ret = re.findall('\(\|\|\|\d+\|\)', data) + # TODO delimiters other than pipes + if ret: + tport = int(ret[0].split('|')[3]) + info['datachan'] = (conn.serverip, tport) + if self.dump: + self.data_channel_map[(conn.serverip, tport)] = conn.addr + info['tempippair'] = None + self.__update_bpf() + + # Look for ip/port information, assuming PSV response + ret = re.findall('\d+,\d+,\d+,\d+,\d+\,\d+', data) + if len(ret) == 1: + tip, tport = self.calculateTransfer(ret[0]) # transfer ip, transfer port + info['datachan'] = (tip, tport) # Update this control channel's knowledge of currently working data channel + if self.dump: + self.data_channel_map[(tip,tport)] = conn.addr # Update plugin's global datachan knowledge + info['tempippair'] = None + self.__update_bpf() + + return conn, blob + + + def calculateTransfer(self, val): + # calculate passive FTP data port + tmp = val.split(',') + ip = '.'.join(tmp[:4]) + port = int(tmp[4])*256 + int(tmp[5]) + return ip, port + + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/http/__init__.py b/dshell/plugins/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/http/httpdump.py b/dshell/plugins/http/httpdump.py new file mode 100644 index 0000000..1aeeb4a --- /dev/null +++ b/dshell/plugins/http/httpdump.py @@ -0,0 +1,168 @@ +""" +Presents useful information points for HTTP sessions +""" + +import dshell.core +import dshell.util +from dshell.plugins.httpplugin import HTTPPlugin +from dshell.output.colorout import ColorOutput + +from urllib.parse import parse_qs +from http import cookies + +class DshellPlugin(HTTPPlugin): + def __init__(self): + super().__init__( + name="httpdump", + description="Dump useful information about HTTP sessions", + bpf="tcp and (port 80 or port 8080 or port 8000)", + author="amm", + output=ColorOutput(label=__name__), + optiondict={ + "maxurilen": { + "type": int, + "default": 30, + "metavar": "LENGTH", + "help": "Truncate URLs longer than LENGTH (default: 30). Set to 0 for no truncating."}, + "maxpost": { + "type": int, + "default": 1000, + "metavar": "LENGTH", + "help": "Truncate POST bodies longer than LENGTH characters (default: 1000). Set to 0 for no truncating."}, + "maxcontent": { + "type": int, + "default": 0, + "metavar": "LENGTH", + "help": "Truncate response bodies longer than LENGTH characters (default: no truncating). Set to 0 for no truncating."}, + "showcontent": { + "action": "store_true", + "help": "Display response body"}, + "showhtml": { + "action": "store_true", + "help": "Display only HTML results"}, + "urlfilter": { + "type": str, + "default": None, + "metavar": "REGEX", + "help": "Filter to URLs matching this regular expression"} + } + ) + + def premodule(self): + if self.urlfilter: + import re + self.urlfilter = re.compile(self.urlfilter) + + def http_handler(self, conn, request, response): + host = request.headers.get('host', conn.serverip) + url = host + request.uri + pretty_url = url + + # separate URL-encoded data from the location + if '?' in request.uri: + uri_location, uri_data = request.uri.split('?', 1) + pretty_url = host + uri_location + else: + uri_location, uri_data = request.uri, "" + + # Check if the URL matches a user-defined filter + if self.urlfilter and not self.urlfilter.search(pretty_url): + return + + if self.maxurilen > 0 and len(uri_location) > self.maxurilen: + uri_location = "{}[truncated]".format(uri_location[:self.maxurilen]) + pretty_url = host + uri_location + + # Set the first line of the alert to show some basic metadata + if response == None: + msg = ["{} (NO RESPONSE) {}".format(request.method, pretty_url)] + else: + msg = ["{} ({}) {} ({})".format(request.method, response.status, pretty_url, response.headers.get("content-type", "[no content-type]"))] + + # Determine if there is any POST data from the client and parse + if request and request.method == "POST": + try: + post_params = parse_qs(request.body.decode("utf-8"), keep_blank_values=True) + # If parse_qs only returns a single element with a null + # value, it's probably an eroneous evaluation. Most likely + # base64 encoded payload ending in an '=' character. + if len(post_params) == 1 and list(post_params.values()) == [["\x00"]]: + post_params = request.body + except UnicodeDecodeError: + post_params = request.body + else: + post_params = {} + + # Get some additional useful data + url_params = parse_qs(uri_data, keep_blank_values=True) + referer = request.headers.get("referer", None) + client_cookie = cookies.SimpleCookie(request.headers.get("cookie", "")) + server_cookie = cookies.SimpleCookie(response.headers.get("cookie", "")) + + # Piece together the alert message + if referer: + msg.append("Referer: {}".format(referer)) + + if client_cookie: + msg.append("Client Transmitted Cookies:") + for k, v in client_cookie.items(): + msg.append("\t{} -> {}".format(k, v.value)) + + if server_cookie: + msg.append("Server Set Cookies:") + for k, v in server_cookie.items(): + msg.append("\t{} -> {}".format(k, v.value)) + + if url_params: + msg.append("URL Parameters:") + for k, v in url_params.items(): + msg.append("\t{} -> {}".format(k, v)) + + if post_params: + if isinstance(post_params, dict): + msg.append("POST Parameters:") + for k, v in post_params.items(): + msg.append("\t{} -> {}".format(k, v)) + else: + msg.append("POST Data:") + msg.append(dshell.util.printable_text(str(post_params))) + elif request.body: + msg.append("POST Body:") + request_body = dshell.util.printable_text(request_body) + if self.maxpost > 0 and len(request.body) > self.maxpost: + msg.append("{}[truncated]".format(request_body[:self.maxpost])) + else: + msg.append(request_body) + + if self.showcontent or self.showhtml: + if self.showhtml and 'html' not in response.headers.get('content-type', ''): + return + if 'gzip' in response.headers.get('content-encoding', ''): + # TODO gunzipping + content = '(gzip encoded)\n{}'.format(response.body) + else: + content = response.body + content = dshell.util.printable_text(content) + if self.maxcontent and len(content) > self.maxcontent: + content = "{}[truncated]".format(content[:self.maxcontent]) + msg.append("Body Content:") + msg.append(content) + + # Display the start and end times based on Blob instead of Connection + kwargs = conn.info() + if request: + kwargs['starttime'] = request.blob.starttime + kwargs['clientbytes'] = len(request.blob.data) + else: + kwargs['starttime'] = None + kwargs['clientbytes'] = 0 + if response: + kwargs['endtime'] = response.blob.endtime + kwargs['serverbytes'] = len(response.blob.data) + else: + kwargs['endtime'] = None + kwargs['serverbytes'] = 0 + + self.write('\n'.join(msg), **kwargs) + + return conn, request, response diff --git a/dshell/plugins/http/joomla.py b/dshell/plugins/http/joomla.py new file mode 100644 index 0000000..e0a8225 --- /dev/null +++ b/dshell/plugins/http/joomla.py @@ -0,0 +1,86 @@ +""" +Detect and dissect malformed HTTP headers targeting Joomla + +https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-8562 +""" + +from dshell.plugins.httpplugin import HTTPPlugin +from dshell.output.alertout import AlertOutput + +import re + +class DshellPlugin(HTTPPlugin): + def __init__(self): + super().__init__( + name="Joomla CVE-2015-8562", + author="bg", + description='detect attempts to enumerate MS15-034 vulnerable IIS servers', + bpf='tcp and (port 80 or port 8080 or port 8000)', + output=AlertOutput(label=__name__), + optiondict={ + "raw_payload": { + "action": "store_true", + "help": "return the raw payload (do not attempt to decode chr encoding)", + } + }, + longdescription=''' +Detect and dissect malformed HTTP headers targeting Joomla + +https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-8562 + +Usage Examples: +--------------- + +Dshell> decode -d joomla *.pcap +[Joomla CVE-2015-8562] 2015-12-15 20:17:18 192.168.1.119:43865 <- 192.168.1.139:80 ** x-forwarded-for -> system('touch /tmp/2'); ** + +The module assumes the cmd payload is encoded using chr. To turn this off run: + +Dshell> decode -d joomla --joomla_raw_payload *.pcap +[Joomla CVE-2015-8562] 2015-12-15 20:17:18 192.168.1.119:43865 <- 192.168.1.139:80 ** x-forwarded-for -> "eval(chr(115).chr(121).chr(115).chr(116).chr(101).chr(109).chr(40).chr(39).chr(116).chr(111).chr(117).chr(99).chr(104).chr(32).chr(47).chr(116).chr(109).chr(112).chr(47).chr(50).chr(39).chr(41).chr(59)); ** +''', + ) + + # Indicator of (potential) compromise + self.ioc = "JFactory::getConfig();exit" + self.ioc_bytes = bytes(self.ioc, "ascii") + + def attempt_decode(self, cmd): + ptext = '' + for c in re.findall('\d+', cmd): + ptext += chr(int(c)) + return ptext + + def parse_cmd(self, data): + start = data.find('"feed_url";')+11 + end = data.find(self.ioc) + chunk = data[start:end] + + try: + cmd = chunk.split(':')[-1] + if self.raw_payload: + return cmd + + plaintext_cmd = self.attempt_decode(cmd) + return plaintext_cmd + except: + return None + + def http_handler(self, conn, request, response): + if not request: + return + + if self.ioc_bytes not in request.blob.data: + # indicator of (potential) compromise is not here + return + + # there is an attempt to exploit Joomla! + + # The Joomla exploit could be sent any HTTP header field + for hdr, val in request.headers.items(): + if self.ioc in val: + cmd = self.parse_cmd(val) + if cmd: + self.alert('{} -> {}'.format(hdr, cmd), **conn.info()) + return conn, request, response + diff --git a/dshell/plugins/http/ms15-034.py b/dshell/plugins/http/ms15-034.py new file mode 100644 index 0000000..9210603 --- /dev/null +++ b/dshell/plugins/http/ms15-034.py @@ -0,0 +1,64 @@ +""" +Proof-of-concept code to detect attempts to enumerate MS15-034 vulnerable +IIS servers and/or cause a denial of service. Each event will generate an +alert that prints out the HTTP Request method and the range value contained +with the HTTP stream. +""" + +from dshell.plugins.httpplugin import HTTPPlugin +from dshell.output.alertout import AlertOutput + +class DshellPlugin(HTTPPlugin): + def __init__(self): + super().__init__( + name="ms15-034", + author="bg", + description='detect attempts to enumerate MS15-034 vulnerable IIS servers', + bpf='tcp and (port 80 or port 8080 or port 8000)', + output=AlertOutput(label=__name__), + longdescription=''' +Proof-of-concept code to detect attempts to enumerate MS15-034 vulnerable +IIS servers and/or cause a denial of service. Each event will generate an +alert that prints out the HTTP Request method and the range value contained +with the HTTP stream. + +Usage: +decode -d ms15-034 -q *.pcap +decode -d ms15-034 -i -q + +References: +https://technet.microsoft.com/library/security/ms15-034 +https://ma.ttias.be/remote-code-execution-via-http-request-in-iis-on-windows/ +''', + ) + + + def http_handler(self, conn, request, response): + if response == None: + # Denial of Service (no server response) + try: + rangestr = request.headers.get("range", '') + # check range value to reduce false positive rate + if not rangestr.endswith('18446744073709551615'): + return + except: + return + self.write('MS15-034 DoS [Request Method: "{0}" URI: "{1}" Range: "{2}"]'.format(request.method, request.uri, rangestr), conn.info()) + return conn, request, response + + else: + # probing for vulnerable server + try: + rangestr = request.headers.get("range", '') + if not rangestr.endswith('18446744073709551615'): + return + except: + return + + # indication of vulnerable server + if rangestr and (response.status == '416' or \ + response.reason == 'Requested Range Not Satisfiable'): + self.write('MS15-034 Vulnerable Server [Request Method: "{0}" Range: "{1}"]'.format(request.method,rangestr), conn.info()) + return conn, request, response + + diff --git a/dshell/plugins/http/riphttp.py b/dshell/plugins/http/riphttp.py new file mode 100644 index 0000000..d5b5ccf --- /dev/null +++ b/dshell/plugins/http/riphttp.py @@ -0,0 +1,205 @@ +""" +Identifies HTTP traffic and reassembles file transfers before writing them to +files. +""" + +from dshell.plugins.httpplugin import HTTPPlugin +from dshell.output.alertout import AlertOutput + +import os +import sys +import re + +class DshellPlugin(HTTPPlugin): + def __init__(self): + super().__init__( + name="rip-http", + author="bg,twp", + bpf="tcp and (port 80 or port 8080 or port 8000)", + description="Rips files from HTTP traffic", + output=AlertOutput(label=__name__), + optiondict={'append_conn': + {'action': 'store_true', + 'help': 'append sourceip-destip to filename'}, + 'append_ts': + {'action': 'store_true', + 'help': 'append timestamp to filename'}, + 'direction': + {'help': 'cs=only capture client POST, sc=only capture server GET response', + 'metavar': '"cs" OR "sc"', + 'default': None}, + 'outdir': + {'help': 'directory to write output files (Default: current directory)', + 'metavar': 'DIRECTORY', + 'default': '.'}, + 'content_filter': + {'help': 'regex MIME type filter for files to save', + 'metavar': 'REGEX'}, + 'name_filter': + {'help': 'regex filename filter for files to save', + 'metavar': 'REGEX'} + } + ) + + def premodule(self): + if self.direction not in ('cs', 'sc', None): + self.error("Invalid value for direction: {!r}. Argument must be either 'sc' for server-to-client or 'cs' for client-to-server.".format(self.direction)) + sys.exit(1) + + if self.content_filter: + self.content_filter = re.compile(self.content_filter) + if self.name_filter: + self.name_filter = re.compile(self.name_filter) + + self.openfiles = {} + + if not os.path.exists(self.outdir): + try: + os.makedirs(self.outdir) + except (IOError, OSError) as e: + self.error("Could not create output directory: {!r}: {!s}" + .format(self.outdir, e)) + sys.exit(1) + + def http_handler(self, conn, request, response): + if (not self.direction or self.direction == 'cs') and request and request.method == "POST" and request.body: + if not self.content_filter or self.content_filter.search(request.headers.get('content-type', '')): + payload = request + elif (not self.direction or self.direction == 'sc') and response and response.status[0] == '2': + if not self.content_filter or self.content_filter.search(response.headers.get('content-type', '')): + payload = response + else: + payload = None + + if not payload: + # Connection did not match any filters, so get rid of it + return + + host = request.headers.get('host', conn.serverip) + url = host + request.uri + + if url in self.openfiles: + # File is already open, so just insert the new data + s, e = self.openfiles[url].handleresponse(response) + self.debug("{0!r} --> Range: {1} - {2}".format(url, s, e)) + else: + # A new file! + filename = request.uri.split('?', 1)[0].split('/')[-1] + if self.name_filter and self.name_filter.search(filename): + # Filename did not match filter, so get rid of it + return + if not filename: + # Assume index.html if there is no filename + filename = "index.html" + if self.append_conn: + filename += "_{0}-{1}".format(conn.serverip, conn.clientip) + if self.append_ts: + filename += "_{}".format(conn.ts) + while os.path.exists(os.path.join(self.outdir, filename)): + filename += "_" + self.write("New file {} ({})".format(filename, url), **conn.info(), dir_arrow="<-") + self.openfiles[url] = HTTPFile(os.path.join(self.outdir, filename), self) + s, e = self.openfiles[url].handleresponse(payload) + self.debug("{0!r} --> Range: {1} - {2}".format(url, s, e)) + if self.openfiles[url].done(): + self.write("File done {} ({})".format(filename, url), **conn.info(), dir_arrow="<-") + del self.openfiles[url] + + return conn, request, response + +class HTTPFile(object): + """ + An internal class used to hold metadata for open HTTP files. + Used mostly to reassemble fragmented transfers. + """ + + def __init__(self, filename, plugin_instance): + self.complete = False + # Expected size in bytes of full file transfer + self.size = 0 + # List of tuples indicating byte chunks already received and written to + # disk + self.ranges = [] + self.plugin = plugin_instance + self.filename = filename + try: + self.fh = open(filename, 'wb') + except IOError as e: + self.plugin.error( + "Could not create file {!r}: {!s}".format(filename, e)) + self.fh = None + + def __del__(self): + if self.fh is None: + return + self.fh.close() + if not self.done(): + self.plugin.warning("Incomplete file: {!r}".format(self.filename)) + try: + os.rename(self.filename, self.filename + "_INCOMPLETE") + except: + pass + ls = 0 + le = 0 + for s, e in self.ranges: + if s > le + 1: + self.plugin.warning( + "Missing bytes between {0} and {1}".format(le, s)) + ls, le = s, e + + def handleresponse(self, response): + # Check for Content Range + range_start = 0 + range_end = len(response.body) - 1 + if 'content-range' in response.headers: + m = re.search( + 'bytes (\d+)-(\d+)/(\d+|\*)', response.headers['content-range']) + if m: + range_start = int(m.group(1)) + range_end = int(m.group(2)) + if len(response.body) < (range_end - range_start + 1): + range_end = range_start + len(response.body) - 1 + try: + if int(m.group(3)) > self.size: + self.size = int(m.group(3)) + except: + pass + elif 'content-length' in response.headers: + try: + if int(response.headers['content-length']) > self.size: + self.size = int(response.headers['content-length']) + except: + pass + # Update range tracking + self.ranges.append((range_start, range_end)) + # Write part of file + if self.fh is not None: + self.fh.seek(range_start) + self.fh.write(response.body) + return (range_start, range_end) + + def done(self): + self.checkranges() + return self.complete + + def checkranges(self): + self.ranges.sort() + current_start = 0 + current_end = 0 + foundgap = False + # print self.ranges + for s, e in self.ranges: + if s <= current_end + 1: + current_end = e + else: + foundgap = True + current_start = s + current_end = e + if not foundgap: + if (current_end + 1) >= self.size: + self.complete = True + return foundgap + + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/http/web.py b/dshell/plugins/http/web.py new file mode 100644 index 0000000..7d4d3cd --- /dev/null +++ b/dshell/plugins/http/web.py @@ -0,0 +1,67 @@ +""" +Displays basic information for web requests/responses in a connection. +""" + +from dshell.plugins.httpplugin import HTTPPlugin +from dshell.output.alertout import AlertOutput + +from hashlib import md5 + +class DshellPlugin(HTTPPlugin): + def __init__(self): + super().__init__( + name="web", + author="bg,twp", + description="Displays basic information for web requests/responses in a connection", + bpf="tcp and (port 80 or port 8080 or port 8000)", + output=AlertOutput(label=__name__), + optiondict={ + "md5": {"action": "store_true", + "help": "Calculate MD5 for each response."} + }, + ) + + def http_handler(self, conn, request, response): + if request: + # Collect basics about the request, if available + method = request.method + host = request.headers.get("host", "") + print(request.headers) + uri = request.uri +# useragent = request.headers.get("user-agent", None) +# referer = request.headers.get("referer", None) + version = request.version + else: + method = "(no request)" + host = "" + uri = "" + version = "" + + if response: + # Collect basics about the response, if available + status = response.status + reason = response.reason + if self.md5: + hash = "(md5: {})".format(md5(response.body).hexdigest()) + else: + hash = "" + else: + status = "(no response)" + reason = "" + hash = "" + + data = "{} {}{} HTTP/{} {} {} {}".format(method, + host, + uri, + version, + status, + reason, + hash) + if not request: + self.write(data, method=method, host=host, uri=uri, version=version, status=status, reason=reason, hash=hash, **response.blob.info()) + else: + self.write(data, method=method, uri=uri, version=version, status=status, reason=reason, hash=hash, **request.headers, **request.blob.info()) + return conn, request, response + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/httpplugin.py b/dshell/plugins/httpplugin.py new file mode 100644 index 0000000..99fe472 --- /dev/null +++ b/dshell/plugins/httpplugin.py @@ -0,0 +1,255 @@ +""" +This is a base-level plugin inteded to handle HTTP connections. + +It inherits from the base ConnectionPlugin and provides a new handler +function: http_handler(conn, request, response). + +It automatically pairs requests/responses, parses headers, reassembles bodies, +and collects them into HTTPRequest and HTTPResponse objects that are passed +to the http_handler. +""" + +import dshell.core + +from pypacker.layer567 import http + +import gzip +import io + +def parse_headers(obj, f): + """Return dict of HTTP headers parsed from a file object.""" + # Logic lifted mostly from dpkt's http module + d = {} + while 1: + line = f.readline() + line = line.decode('utf-8') + line = line.strip() + if not line: + break + l = line.split(None, 1) + if not l[0].endswith(':'): + obj.errors.append(dshell.core.DataError("Invalid header {!r}".format(line))) + k = l[0][:-1].lower() + v = len(l) != 1 and l[1] or '' + if k in d: + if not type(d[k]) is list: + d[k] = [d[k]] + d[k].append(v) + else: + d[k] = v + return d + +def parse_body(obj, f, headers): + """Return HTTP body parsed from a file object, given HTTP header dict.""" + # Logic lifted mostly from dpkt's http module + if headers.get('transfer-encoding', '').lower() == 'chunked': + l = [] + found_end = False + while 1: + try: + sz = f.readline().split(None, 1)[0] + except IndexError: + obj.errors.append(dshell.core.DataError('missing chunk size')) + n = int(sz, 16) + if n == 0: + found_end = True + buf = f.read(n) + if f.readline().strip(): + break + if n and len(buf) == n: + l.append(buf) + else: + break + if not found_end: + obj.errors.append(dshell.core.DataError('premature end of chunked body')) + body = b''.join(l) + elif 'content-length' in headers: + n = int(headers['content-length']) + body = f.read(n) + if len(body) != n: + obj.errors.append(dshell.core.DataError('short body (missing {} bytes)'.format(n - len(body)))) + elif 'content-type' in headers: + body = f.read() + else: + # XXX - need to handle HTTP/0.9 + body = b'' + return body + +class HTTPRequest(object): + """ + A class for HTTP requests + + Attributes: + blob : the Blob instance of the request + errors : a list of caught exceptions from parsing + method : the method of the request (e.g. GET, PUT, POST, etc.) + uri : the URI being requested (host not included) + version : the HTTP version (e.g. "1.1" for "HTTP/1.1") + headers : a dictionary containing the headers and values + body : bytestring of the reassembled body, after the headers + """ + _methods = ( + 'GET', 'PUT', 'ICY', + 'COPY', 'HEAD', 'LOCK', 'MOVE', 'POLL', 'POST', + 'BCOPY', 'BMOVE', 'MKCOL', 'TRACE', 'LABEL', 'MERGE', + 'DELETE', 'SEARCH', 'UNLOCK', 'REPORT', 'UPDATE', 'NOTIFY', + 'BDELETE', 'CONNECT', 'OPTIONS', 'CHECKIN', + 'PROPFIND', 'CHECKOUT', 'CCM_POST', + 'SUBSCRIBE', 'PROPPATCH', 'BPROPFIND', + 'BPROPPATCH', 'UNCHECKOUT', 'MKACTIVITY', + 'MKWORKSPACE', 'UNSUBSCRIBE', 'RPC_CONNECT', + 'VERSION-CONTROL', + 'BASELINE-CONTROL' + ) + + def __init__(self, blob): + self.errors = [] + self.headers = {} + self.body = b'' + self.blob = blob + data = io.BytesIO(blob.data) + rawline = data.readline() + try: + line = rawline.decode('utf-8') + except UnicodeDecodeError: + line = '' + l = line.strip().split() + if len(l) != 3 or l[0] not in self._methods or not l[2].startswith('HTTP'): + self.errors.append(dshell.core.DataError('invalid HTTP request: {!r}'.format(rawline))) + self.method = '' + self.uri = '' + self.version = '' + return + else: + self.method = l[0] + self.uri = l[1] + self.version = l[2][5:] + self.headers = parse_headers(self, data) + self.body = parse_body(self, data, self.headers) + +class HTTPResponse(object): + """ + A class for HTTP responses + + Attributes: + blob : the Blob instance of the request + errors : a list of caught exceptions from parsing + version : the HTTP version (e.g. "1.1" for "HTTP/1.1") + status : the status code of the response (e.g. "200" or "304") + reason : the status text of the response (e.g. "OK" or "Not Modified") + headers : a dictionary containing the headers and values + body : bytestring of the reassembled body, after the headers + """ + def __init__(self, blob): + self.errors = [] + self.headers = {} + self.body = b'' + self.blob = blob + data = io.BytesIO(blob.data) + rawline = data.readline() + try: + line = rawline.decode('utf-8') + except UnicodeDecodeError: + line = '' + l = line.strip().split(None, 2) + if len(l) < 2 or not l[0].startswith("HTTP") or not l[1].isdigit(): + self.errors.append(dshell.core.DataError('invalid HTTP response: {!r}'.format(rawline))) + self.version = '' + self.status = '' + self.reason = '' + return + else: + self.version = l[0][5:] + self.status = l[1] + self.reason = l[2] + self.headers = parse_headers(self, data) + self.body = parse_body(self, data, self.headers) + + def decompress_gzip_content(self): + """ + If this response has Content-Encoding set to something with "gzip", + this function will decompress it and store it in the body. + """ + if "gzip" in self.headers.get("content-encoding", ""): + try: + iobody = io.BytesIO(self.body) + except TypeError as e: + self.errors.append(dshell.core.DataError("Body was not a byte string ({!s}). Could not decompress.".format(type(self.body)))) + return + try: + self.body = gzip.GzipFile(fileobj=iobody).read() + except OSError as e: + self.errors.append(OSError("Could not gunzip body. {!s}".format(e))) + return + + +class HTTPPlugin(dshell.core.ConnectionPlugin): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # Use "gunzip" argument to automatically decompress gzipped responses + self.gunzip = kwargs.get("gunzip", False) + + def connection_handler(self, conn): + """ + Goes through each Blob in a Connection, assuming they appear in pairs + of requests and responses, and builds HTTPRequest and HTTPResponse + objects. + + After a response (or only a request at the end of a connection), + http_handler is called. If it returns nothing, the respective blobs + are marked as hidden so they won't be passed to additional plugins. + """ + request = None + response = None + for blob in conn.blobs: + blob.reassemble(allow_overlap=True, allow_padding=True) + if not blob.data: + continue + if blob.direction == 'cs': + # client-to-server request + request = HTTPRequest(blob) + for req_error in request.errors: + self.debug("Request Error: {!r}".format(req_error)) + elif blob.direction == 'sc': + # server-to-client response + response = HTTPResponse(blob) + for rep_error in response.errors: + self.debug("Response Error: {!r}".format(rep_error)) + if self.gunzip: + response.decompress_gzip_content() + http_handler_out = self.http_handler(conn=conn, request=request, response=response) + if not http_handler_out: + if request: + request.blob.hidden = True + if response: + response.blob.hidden = True + request = None + response = None + if request and not response: + http_handler_out = self.http_handler(conn=conn, request=request, response=None) + if not http_handler_out: + blob.hidden = True + return conn + + def http_handler(self, conn, request, response): + """ + A placeholder. + + Plugins will be able to overwrite this to perform custom activites + on HTTP data. + + It SHOULD return a list containing the sames types of values that came + in as arguments (i.e. return (conn, request, response)) or None. This + is mostly a consistency thing. Realistically, it only needs to return + some value that evaluates to True to pass the Blobs along to additional + plugins. + + Arguments: + conn: a Connection object + request: a HTTPRequest object + response: a HTTPResponse object + """ + return conn, request, response + +DshellPlugin = None diff --git a/dshell/plugins/malware/__init__.py b/dshell/plugins/malware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/malware/sweetorange.py b/dshell/plugins/malware/sweetorange.py new file mode 100644 index 0000000..f45e328 --- /dev/null +++ b/dshell/plugins/malware/sweetorange.py @@ -0,0 +1,82 @@ +""" +2015 Feb 13 + +Sometimes, attackers will try to obfuscate links to the Sweet Orange exploit +kit. This plugin is an attempt to decode that sort of traffic. + +It will use a regular expression to try and detect certain variable names that +can be contained in JavaScript code. It will then take the value assigned to +it and decode the domain address hidden inside the value. + +Samples: +http://malware-traffic-analysis.net/2014/10/27/index2.html +http://malware-traffic-analysis.net/2014/10/03/index.html +http://malware-traffic-analysis.net/2014/09/25/index.html +""" + +from dshell.output.alertout import AlertOutput +from dshell.plugins.httpplugin import HTTPPlugin + +import re + + +class DshellPlugin(HTTPPlugin): + + def __init__(self): + super().__init__( + name="sweetorange", + longdescription="Used to decode certain variants of the Sweet Orange exploit kit redirect traffic. Looks for telltale Javascript variable names (e.g. 'ajax_data_source' and 'main_request_data_content') and automatically decodes the exploit landing page contained.", + description="Used to decode certain variants of the Sweet Orange exploit kit redirect traffic", + bpf="tcp and (port 80 or port 8080 or port 8000)", + output=AlertOutput(label=__name__), + author="dev195", + gunzip=True, + optiondict={ + "variable": { + "type": str, + "action": "append", + "help": 'Variable names to search for. Default ("ajax_data_source", "main_request_data_content")', + "default": ["ajax_data_source", "main_request_data_content"] + }, + "color": { + "action": "store_true", + "help": "Display encoded/decoded lines in different TTY colors.", + "default": False + }, + } + ) + + + def premodule(self): + self.sig_regex = re.compile( + r"var (" + '|'.join(map(re.escape, self.variable)) + ")='(.*?)';") + self.hexregex = re.compile(r'[^a-fA-F0-9]') + self.debug('Variable regex: "%s"' % self.sig_regex.pattern) + + def http_handler(self, conn, request, response): + try: + response_body = response.body.decode("ascii") + except UnicodeError: + return + except AttributeError: + return + + if response and any([v in response_body for v in self.variable]): + # Take the variable's value, extract the hex characters, and + # convert to ASCII + matches = self.sig_regex.search(response_body) + try: + hidden = matches.groups()[1] + match = bytes.fromhex(self.hexregex.sub('', hidden)) + match = match.decode('utf-8') + except: + return + if self.color: + # If desired, add TTY colors to the alerts for differentiation + # between encoded/decoded strings + hidden = "\x1b[37;2m%s\x1b[0m" % hidden + match = "\x1b[32m%s\x1b[0m" % match + self.log(hidden) + self.write(match, **conn.info()) + return (conn, request, response) + diff --git a/dshell/plugins/misc/__init__.py b/dshell/plugins/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/misc/followstream.py b/dshell/plugins/misc/followstream.py new file mode 100644 index 0000000..5e73d57 --- /dev/null +++ b/dshell/plugins/misc/followstream.py @@ -0,0 +1,25 @@ +""" +Generates color-coded Screen/HTML output similar to Wireshark Follow Stream +""" + +import dshell.core +from dshell.output.colorout import ColorOutput + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="Followstream", + author="amm/dev195", + description="Generates color-coded Screen/HTML output similar to Wireshark Follow Stream. Empty connections will be skipped.", + bpf="tcp", + output=ColorOutput(label=__name__), + ) + + def connection_handler(self, conn): + if (conn.clientbytes + conn.serverbytes > 0): + self.write(conn, **conn.info()) + return conn + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/misc/pcapwriter.py b/dshell/plugins/misc/pcapwriter.py new file mode 100644 index 0000000..2407bc9 --- /dev/null +++ b/dshell/plugins/misc/pcapwriter.py @@ -0,0 +1,64 @@ +""" +Generates pcap output + +Can be used alone or chained at the end of plugins for a kind of filter. + +Use --pcapwriter_outfile to separate its output from that of other plugins. + +Example uses include: + - merging multiple pcap files into one + (decode -d pcapwriter ~/pcap/* >merged.pcap) + - saving relevant traffic by chaining with another plugin + (decode -d track+pcapwriter --track_source=192.168.1.1 --pcapwriter_outfile=merged.pcap ~/pcap/*) + - getting pcap output from plugins that can't use pcapout + (decode -d web+pcapwriter ~/pcap/*) +""" + +import dshell.core +from dshell.output.pcapout import PCAPOutput + +import sys + +class DshellPlugin(dshell.core.PacketPlugin): + + def __init__(self, *args, **kwargs): + super().__init__( + name="pcap writer", + description="Used to generate pcap output for plugins that can't use -o pcapout", + longdescription="""Generates pcap output + +Can be used alone or chained at the end of plugins for a kind of filter. + +Use --pcapwriter_outfile to separate its output from that of other plugins. + +Example uses include: + - merging multiple pcap files into one (decode -d pcapwriter ~/pcap/* >merged.pcap) + - saving relevant traffic by chaining with another plugin (decode -d track+pcapwriter --track_source=192.168.1.1 --pcapwriter_outfile=merged.pcap ~/pcap/*) + - getting pcap output from plugins that can't use pcapout (decode -d web+pcapwriter ~/pcap/*) +""", + author="dev195", + output=PCAPOutput(label=__name__), + optiondict={ + "outfile": { + "type": str, + "help": "Write to FILE instead of stdout", + "metavar": "FILE", + } + } + ) + + def premodule(self): + if self.outfile: + try: + self.out.reset_fh(filename=self.outfile, mode='wb') + except OSError as e: + self.error(str(e)) + sys.exit(1) + + def raw_handler(self, pktlen, pkt, ts): + rawpkt = pkt.header_bytes + pkt.body_bytes + self.write(pktlen=pktlen, rawpkt=rawpkt, ts=ts, link_layer_type=self.link_layer_type) + return pktlen, pkt, ts + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/misc/search.py b/dshell/plugins/misc/search.py new file mode 100644 index 0000000..d4d99a6 --- /dev/null +++ b/dshell/plugins/misc/search.py @@ -0,0 +1,92 @@ +import dshell.core +from dshell.util import printable_text +from dshell.output.alertout import AlertOutput + +import re +import sys + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="search", + author="dev195", + bpf="tcp or udp", + description="Search for patterns in connections", + longdescription=""" +Reconstructs streams and searches the content for a user-provided regular +expression. Requires definition of the --search_expression argument. Additional +options can be provided to alter behavior. + """, + output=AlertOutput(label=__name__), + optiondict={ + "expression": { + "help": "Search expression", + "type": str, + "metavar": "REGEX"}, + "ignorecase": { + "help": "Ignore case when searching", + "action": "store_true"}, + "invert": { + "help": "Return connections that DO NOT match expression", + "action": "store_true"}, + "quiet": { + "help": "Do not display matches from this plugin. Useful when chaining plugins.", + "action": "store_true"} + }) + + + + def premodule(self): + # make sure the user actually provided an expression to search for + if not self.expression: + self.error("Must define an expression to search for using --search_expression") + sys.exit(1) + + # define the regex flags, based on arguments + re_flags = 0 + if self.ignorecase: + re_flags = re_flags | re.IGNORECASE + + # Create the regular expression + try: + # convert expression to bytes so it can accurately compare to + # the connection data (which is also of type bytes) + byte_expression = bytes(self.expression, 'utf-8') + self.regex = re.compile(byte_expression, re_flags) + except Exception as e: + self.error("Could not compile regex ({0})".format(e)) + sys.exit(1) + + + + def connection_handler(self, conn): + """ + Go through the data of each connection. + If anything is a hit, return the entire connection. + """ + + match_found = False + for blob in conn.blobs: + for line in blob.data.splitlines(): + match = self.regex.search(line) + if match and self.invert: + return None + elif match and not self.invert: + match_found = True + if not self.quiet: + if blob.sip == conn.sip: + self.write(printable_text(line, False), **conn.info(), dir_arrow="->") + else: + self.write(printable_text(line, False), **conn.info(), dir_arrow="<-") + elif self.invert and not match: + if not self.quiet: + self.write(**conn.info()) + return conn + if match_found: + return conn + + + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/misc/sslalerts.py b/dshell/plugins/misc/sslalerts.py new file mode 100644 index 0000000..d5ddc3f --- /dev/null +++ b/dshell/plugins/misc/sslalerts.py @@ -0,0 +1,107 @@ +""" +Looks for SSL alert messages +""" + +# handy reference: +# http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session + +import dshell.core +from dshell.output.alertout import AlertOutput + +import hashlib +import io +import struct +from pprint import pprint + +# SSLv3/TLS version +SSL3_VERSION = 0x0300 +TLS1_VERSION = 0x0301 +TLS1_1_VERSION = 0x0302 +TLS1_2_VERSION = 0x0303 + +# Record type +SSL3_RT_CHANGE_CIPHER_SPEC = 20 +SSL3_RT_ALERT = 21 +SSL3_RT_HANDSHAKE = 22 +SSL3_RT_APPLICATION_DATA = 23 + +# Handshake message type +SSL3_MT_HELLO_REQUEST = 0 +SSL3_MT_CLIENT_HELLO = 1 +SSL3_MT_SERVER_HELLO = 2 +SSL3_MT_CERTIFICATE = 11 +SSL3_MT_SERVER_KEY_EXCHANGE = 12 +SSL3_MT_CERTIFICATE_REQUEST = 13 +SSL3_MT_SERVER_DONE = 14 +SSL3_MT_CERTIFICATE_VERIFY = 15 +SSL3_MT_CLIENT_KEY_EXCHANGE = 16 +SSL3_MT_FINISHED = 20 + +alert_types = { + 0x00: "CLOSE_NOTIFY", + 0x0a: "UNEXPECTED_MESSAGE", + 0x14: "BAD_RECORD_MAC", + 0x15: "DECRYPTION_FAILED", + 0x16: "RECORD_OVERFLOW", + 0x1e: "DECOMPRESSION_FAILURE", + 0x28: "HANDSHAKE_FAILURE", + 0x29: "NO_CERTIFICATE", + 0x2a: "BAD_CERTIFICATE", + 0x2b: "UNSUPPORTED_CERTIFICATE", + 0x2c: "CERTIFICATE_REVOKED", + 0x2d: "CERTIFICATE_EXPIRED", + 0x2e: "CERTIFICATE_UNKNOWN", + 0x2f: "ILLEGAL_PARAMETER", + 0x30: "UNKNOWN_CA", + 0x31: "ACCESS_DENIED", + 0x32: "DECODE_ERROR", + 0x33: "DECRYPT_ERROR", + 0x3c: "EXPORT_RESTRICTION", + 0x46: "PROTOCOL_VERSION", + 0x47: "INSUFFICIENT_SECURITY", + 0x50: "INTERNAL_ERROR", + 0x5a: "USER_CANCELLED", + 0x64: "NO_RENEGOTIATION", +} + +alert_severities = { + 0x01: "warning", + 0x02: "fatal", +} + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="sslalerts", + author="dev195", + bpf="tcp and (port 443 or port 993 or port 1443 or port 8531)", + description="Looks for SSL alert messages", + output=AlertOutput(label=__name__), + ) + + def blob_handler(self, conn, blob): + data = io.BytesIO(blob.data) + alert_seen = False + # Iterate over each layer of the connection, paying special attention to the certificate + while True: + try: + content_type, proto_version, record_len = struct.unpack("!BHH", data.read(5)) + except struct.error: + break + if proto_version not in (SSL3_VERSION, TLS1_VERSION, TLS1_1_VERSION, TLS1_2_VERSION): + return None + if content_type == SSL3_RT_ALERT: + handshake_len = struct.unpack("!I", data.read(4))[0] +# assert handshake_len == 2 # TODO remove when live + severity = struct.unpack("!B", data.read(1))[0] + if severity not in alert_severities: + continue + severity_msg = alert_severities.get(severity, severity) + alert_type = struct.unpack("!B", data.read(1))[0] + alert_msg = alert_types.get(alert_type, str(alert_type)) + self.write("SSL alert: ({}) {}".format(severity_msg, alert_msg), **conn.info()) + alert_seen = True + + if alert_seen: + return conn, blob diff --git a/dshell/plugins/misc/synrst.py b/dshell/plugins/misc/synrst.py new file mode 100644 index 0000000..ccf9227 --- /dev/null +++ b/dshell/plugins/misc/synrst.py @@ -0,0 +1,53 @@ +""" +Detects failed attempts to connect (SYN followed by RST/ACK) +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + +from pypacker.layer4 import tcp + +class DshellPlugin(dshell.core.PacketPlugin): + + def __init__(self): + super().__init__( + name="SYN/RST", + description="Detects failed attempts to connect (SYN followed by RST/ACK)", + author="bg", + bpf="(ip and (tcp[13]=2 or tcp[13]=20)) or (ip6 and tcp)", + output=AlertOutput(label=__name__) + ) + + def premodule(self): + # Cache to hold SYNs waiting to pair with RST/ACKs + self.tracker = {} + + def packet_handler(self, pkt): + # Check if SYN or RST/ACK. Discard non-matches. + if pkt.tcp_flags not in (tcp.TH_SYN, tcp.TH_RST|tcp.TH_ACK): + return + + # Try to find the TCP layer + tcpp = pkt.pkt.upper_layer + while not isinstance(tcpp, tcp.TCP): + try: + tcpp = tcpp.upper_layer + except AttributeError: + # There doesn't appear to be a TCP layer, for some reason + return + + if tcpp.flags == tcp.TH_SYN: + seqnum = tcpp.seq + key = "{}|{}|{}|{}|{}".format( + pkt.sip, pkt.sport, seqnum, pkt.dip, pkt.dport) + self.tracker[key] = pkt + elif tcpp.flags == tcp.TH_RST|tcp.TH_ACK: + acknum = tcpp.ack - 1 + tmpkey = "{}|{}|{}|{}|{}".format( + pkt.dip, pkt.dport, acknum, pkt.sip, pkt.sport) + if tmpkey in self.tracker: + msg = "Failed connection [initiated by {}]".format(pkt.dip) + self.write(msg, **pkt.info()) + oldpkt = self.tracker[tmpkey] + del self.tracker[tmpkey] + return [oldpkt, pkt] diff --git a/dshell/plugins/misc/xor.py b/dshell/plugins/misc/xor.py new file mode 100644 index 0000000..8e98385 --- /dev/null +++ b/dshell/plugins/misc/xor.py @@ -0,0 +1,101 @@ +""" +XOR the data in every packet with a user-provided key. Multiple keys can be used +for different data directions. +""" + +import dshell.core +import dshell.util +from dshell.output.output import Output + +import struct + +class DshellPlugin(dshell.core.ConnectionPlugin): + def __init__(self): + super().__init__( + name="xor", + description="XOR every packet with a given key", + output=Output(label=__name__), + bpf="tcp", + author="twp,dev195", + optiondict={ + "key": { + "type": str, + "default": "0xff", + "help": "xor key in hex format (default: 0xff)", + "metavar": "0xHH" + }, + "cskey": { + "type": str, + "default": None, + "help": "xor key to use for client-to-server data (default: None)", + "metavar": "0xHH" + }, + "sckey": { + "type": str, + "default": None, + "help": "xor key to use for server-to-client data (default: None)", + "metavar": "0xHH" + }, + "resync": { + "action": "store_true", + "help": "resync the key index if the key is seen in the data" + } + } + ) + + def __make_key(self, key): + "Convert a user-provided key into a standard format plugin can use." + if key.startswith("0x") or key.startswith("\\x"): + # Convert a hex key + oldkey = key[2:] + newkey = b'' + for i in range(0, len(oldkey), 2): + try: + newkey += struct.pack('B', int(oldkey[i:i + 2], 16)) + except ValueError as e: + self.warn("Error converting hex. Will treat as raw string. - {!s}".format(e)) + newkey = key.encode('ascii') + break + else: + try: + # See if it's a numeric key + newkey = int(key) + newkey = struct.pack('I', newkey) + except ValueError: + # otherwise, convert string key to bytes as it is + newkey = key.encode('ascii') + self.debug("__make_key: {!r} -> {!r}".format(key, newkey)) + return newkey + + def premodule(self): + self.key = self.__make_key(self.key) + if self.cskey: + self.cskey = self.__make_key(self.cskey) + if self.sckey: + self.sckey = self.__make_key(self.sckey) + + def connection_handler(self, conn): + for blob in conn.blobs: + key_index = 0 + if self.sckey and blob.direction == 'sc': + key = self.sckey + elif self.cskey and blob.direction == 'cs': + key = self.cskey + else: + key = self.key + for pkt in blob.all_packets: + # grab the data from the TCP layer and down + data = pkt.pkt.upper_layer.upper_layer.body_bytes + self.debug("Original:\n{}".format(dshell.util.hex_plus_ascii(data))) + # XOR the data and store it in new_data + new_data = b'' + for i in range(len(data)): + if self.resync and data[i:i + len(key)] == key: + key_index = 0 + x = data[i] ^ key[key_index] + new_data += struct.pack('B', x) + key_index = (key_index + 1) % len(key) + # rebuild the packet by adding together each of the layers + pkt.rawpkt = pkt.pkt.header_bytes + pkt.pkt.upper_layer.header_bytes + pkt.pkt.upper_layer.upper_layer.header_bytes + new_data + self.debug("New:\n{}".format(dshell.util.hex_plus_ascii(new_data))) + return conn diff --git a/dshell/plugins/nbns/__init__.py b/dshell/plugins/nbns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/nbns/nbns.py b/dshell/plugins/nbns/nbns.py new file mode 100644 index 0000000..413c3ff --- /dev/null +++ b/dshell/plugins/nbns/nbns.py @@ -0,0 +1,160 @@ +""" +NBNS plugin +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + +from struct import unpack + + +# A few common NBNS Protocol Info Opcodes +# Due to a typo in RFC 1002, 0x9 is also acceptable, but rarely used +# for 'NetBios Refresh' +# 'NetBios Multi-Homed Name Regsitration' (0xf) was added after the RFC +nbns_op = { 0: 'NB_NAME_QUERY', + 5: 'NB_REGISTRATION', + 6: 'NB_RELEASE', + 7: 'NB_WACK', + 8: 'NB_REFRESH', + 9: 'NB_REFRESH', + 15: 'NB_MULTI_HOME_REG' } + + +class DshellPlugin(dshell.core.PacketPlugin): + def __init__(self): + super().__init__( name='nbns', + description='Extract client information from NBNS traffic', + longdescription=""" +The nbns (NetBIOS Name Service) plugin will extract the Transaction ID, Protocol Info, +Client Hostname, and Client MAC address from every UDP NBNS packet found in the given +pcap using port 137. UDP is the standard transport protocol for NBNS traffic. +This filter pulls pertinent information from NBNS packets. + +Examples: + + General usage: + + decode -d nbns + + This will display the connection info including the timestamp, + the source IP, destination IP, Transaction ID, Protocol Info, + Client Hostname, and the Client MAC address in a tabular format. + + + Malware Traffic Analysis Exercise Traffic from 2014-12-08 where a user was hit with a Fiesta exploit kit: + + We want to find out more about the infected machine, and some of this information can be pulled from NBNS traffic + + decode -d nbns 2014-12-08-traffic-analysis-exercise.pcap + + OUTPUT (first few packets): + [nbns] 2014-12-08 18:19:13 192.168.204.137:137 -> 192.168.204.2:137 ** + Transaction ID: 0xb480 + Info: NB_NAME_QUERY + Client Hostname: WPAD + Client MAC: 00:0C:29:9D:B8:6D + ** + [nbns] 2014-12-08 18:19:14 192.168.204.137:137 -> 192.168.204.2:137 ** + Transaction ID: 0xb480 + Info: NB_NAME_QUERY + Client Hostname: WPAD + Client MAC: 00:0C:29:9D:B8:6D + ** + [nbns] 2014-12-08 18:19:16 192.168.204.137:137 -> 192.168.204.2:137 ** + Transaction ID: 0xb480 + Info: NB_NAME_QUERY + Client Hostname: WPAD + Client MAC: 00:0C:29:9D:B8:6D + ** + [nbns] 2014-12-08 18:19:17 192.168.204.137:137 -> 192.168.204.255:137 ** + Transaction ID: 0xb480 + Info: NB_NAME_QUERY + Client Hostname: WPAD + Client MAC: 00:0C:29:9D:B8:6D + ** + """, + bpf='(udp and port 137)', + output=AlertOutput(label=__name__), + author='dek', + ) + self.mac_address = None + self.client_hostname = None + self.xid = None + self.prot_info = None + + + def packet_handler(self, pkt): + + # iterate through the layers and find the NBNS layer + nbns_packet = pkt.pkt.upper_layer + try: + nbns_packet = nbns_packet.upper_layer + except (IndexError) as e: + self.out.log('{}: could not parse session data \ + (NBNS packet not found)'.format(str(e))) + # pypacker may throw an Exception here; could use + # further testing + return + + + # Extract the Client hostname from the connection data + # It is represented as 32-bytes half-ASCII + try: + nbns_name = unpack('32s', pkt.data[13:45])[0] + except error as e: + self.out.log('{}: (NBNS packet not found)'.format(str(e))) + return + + + # Decode the 32-byte half-ASCII name to its 16 byte NetBIOS name + try: + if len(nbns_name) == 32: + decoded = [] + for i in range(0,32,2): + nibl = hex(ord(chr(nbns_name[i])) - ord('A'))[2:] + nibh = hex(ord(chr(nbns_name[i+1])) - ord('A'))[2:] + decoded.append(chr(int(''.join((nibl, nibh)), 16))) + + # For uniformity, strip excess byte and space chars + self.client_hostname = ''.join(decoded)[0:-1].strip() + else: + self.client_hostname = str(nbns_name) + + except ValueError as e: + self.out.log('{}: Hostname in improper format \ + (NBNS packet not found)'.format(str(e))) + return + + + # Extract the Transaction ID from the NBNS packet + xid = unpack('2s', pkt.data[0:2])[0] + self.xid = "0x{}".format(xid.hex()) + + # Extract the opcode info from the NBNS Packet + op = unpack('2s', pkt.data[2:4])[0] + op_hex = op.hex() + op = int(op_hex, 16) + # Remove excess bits + op = (op >> 11) & 15 + + # Decode protocol info if it was present in the payload + try: + self.prot_info = nbns_op[op] + except: + self.prot_info = "0x{}".format(op_hex) + + # Extract the MAC address from the ethernet layer of the packet + self.mac_address = pkt.smac + + # Allow for unknown hostnames + if not self.client_hostname: + self.client_hostname = "" + + if self.xid and self.prot_info and self.client_hostname and self.mac_address: + self.write('\n\tTransaction ID:\t\t{:<8} \n\tInfo:\t\t\t{:<16} \n\tClient Hostname:\t{:<16} \n\tClient MAC:\t\t{:<18}\n'.format( + self.xid, self.prot_info, self.client_hostname, self.mac_address), **pkt.info(), dir_arrow='->') + return pkt + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/portscan/__init__.py b/dshell/plugins/portscan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/portscan/indegree.py b/dshell/plugins/portscan/indegree.py new file mode 100644 index 0000000..4bdc338 --- /dev/null +++ b/dshell/plugins/portscan/indegree.py @@ -0,0 +1,35 @@ +""" +Parse traffic to detect scanners based on connection to IPs that are rarely touched by others +""" + +import dshell.core + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name='parse indegree', + description='Parse traffic to detect scanners based on connection to IPs that are rarely touched by others', + bpf='(tcp or udp)', + author='dev195', + ) + self.client_conns = {} + self.server_conns = {} + self.minhits = 3 + + def connection_handler(self, conn): + self.client_conns.setdefault(conn.clientip, set()) + self.server_conns.setdefault(conn.serverip, set()) + + self.client_conns[conn.clientip].add(conn.serverip) + self.server_conns[conn.serverip].add(conn.clientip) + + def postfile(self): + for clientip, serverips in self.client_conns.items(): + target_count = len(serverips) + S = min((len(self.server_conns[serverip]) for serverip in serverips)) + if S > 2 or target_count < 5: + continue + # TODO implement whitelist + self.write("Scanning IP: {} / S score: {:.1f} / Number of records: {}".format(clientip, S, target_count)) + diff --git a/dshell/plugins/portscan/trw.py b/dshell/plugins/portscan/trw.py new file mode 100644 index 0000000..e2f638b --- /dev/null +++ b/dshell/plugins/portscan/trw.py @@ -0,0 +1,93 @@ +""" +Uses the Threshold Random Walk algorithm described in this paper: + +Limitations to threshold random walk scan detection and mitigating enhancements +Written by: Mell, P.; Harang, R. +http://ieeexplore.ieee.org/xpls/icp.jsp?arnumber=6682723 +""" + +import dshell.core +from dshell.output.output import Output + +from pypacker.layer4 import tcp + +from collections import defaultdict + +o0 = 0.8 # probability IP is benign given successful connection +o1 = 0.2 # probability IP is a scanner given successful connection +is_success = o0/o1 +is_failure = o1/o0 + +max_fp_prob = 0.01 +min_detect_prob = 0.99 +hi_threshold = min_detect_prob / max_fp_prob +lo_threshold = max_fp_prob / min_detect_prob + +OUTPUT_FORMAT = "(%(plugin)s) %(data)s\n" + +class DshellPlugin(dshell.core.PacketPlugin): + def __init__(self, *args, **kwargs): + super().__init__( + name="trw", + author="dev195", + bpf="tcp", + output=Output(label=__name__, format=OUTPUT_FORMAT), + description="Uses Threshold Random Walk to detect network scanners", + optiondict={ + "mark_benigns": { + "action": "store_true", + "help": "Use an upper threshold to mark IPs as benign, thus removing them from consideration as scanners" + } + } + ) + self.synners = set() + self.ip_scores = defaultdict(lambda: 1) + self.classified_ips = set() + + def check_score(self, ip, score): + if self.mark_benigns and score >= hi_threshold: + self.write("IP {} is benign (score: {})".format(ip, score)) + self.classified_ips.add(ip) + elif score <= lo_threshold: + self.write("IP {} IS A SCANNER! (score: {})".format(ip, score)) + self.classified_ips.add(ip) + + def packet_handler(self, pkt): + if not pkt.tcp_flags: + return + + # If we have a SYN, store it in a set and wait for some kind of + # response or the end of pcap + if pkt.tcp_flags == tcp.TH_SYN and pkt.sip not in self.classified_ips: + self.synners.add(pkt.addr) + return pkt + + # If we get the SYN/ACK, score the destination IP with a success + elif pkt.tcp_flags == (tcp.TH_SYN | tcp.TH_ACK) and pkt.dip not in self.classified_ips: + alt_addr = ((pkt.dip, pkt.dport), (pkt.sip, pkt.sport)) + if alt_addr in self.synners: + self.ip_scores[pkt.dip] *= is_success + self.check_score(pkt.dip, self.ip_scores[pkt.dip]) + self.synners.remove(alt_addr) + return pkt + + # If we get a RST, assume the connection was refused and score the + # destination IP with a failure + elif pkt.tcp_flags & tcp.TH_RST and pkt.dip not in self.classified_ips: + alt_addr = ((pkt.dip, pkt.dport), (pkt.sip, pkt.sport)) + if alt_addr in self.synners: + self.ip_scores[pkt.dip] *= is_failure + self.check_score(pkt.dip, self.ip_scores[pkt.dip]) + self.synners.remove(alt_addr) + return pkt + + + def postfile(self): + # Go through any SYNs that didn't get a response and assume they failed + for addr in self.synners: + ip = addr[0][0] + if ip in self.classified_ips: + continue + self.ip_scores[ip] *= is_failure + self.check_score(ip, self.ip_scores[ip]) + diff --git a/dshell/plugins/protocol/__init__.py b/dshell/plugins/protocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decoders/protocol/bitcoin.py b/dshell/plugins/protocol/bitcoin.py similarity index 64% rename from decoders/protocol/bitcoin.py rename to dshell/plugins/protocol/bitcoin.py index 37035bc..015be19 100644 --- a/decoders/protocol/bitcoin.py +++ b/dshell/plugins/protocol/bitcoin.py @@ -1,9 +1,14 @@ -import dshell -import util -import binascii +""" +Bitcoin plugin +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + import json from struct import unpack + # Magic values used to determine Bitcoin Network Type # Bitcoin Testnet is an alternative blockchain used for testing MAGIC_VALS = {'F9 BE B4 D9': 'BITCOIN-MAIN', @@ -11,20 +16,25 @@ '0B 11 09 07': 'BITCOIN-TESTNET3'} -class DshellDecoder(dshell.TCPDecoder): +class DshellPlugin(dshell.core.ConnectionPlugin): def __init__(self): - dshell.TCPDecoder.__init__(self, - name='bitcoin', + super().__init__( name='bitcoin', description='Extract Bitcoin traffic, including Stratum mining protocol (pooled) traffic', longdescription=''' -The bitcoin decoder will extract any Bitcoin traffic attempting to find and output: +The bitcoin plugin will extract any Bitcoin traffic attempting to find and output: Client/server IP addresses, src/dst port numbers, MAC addresses of the machines used in the Bitcoin communication/transactions, timestamps of the packets, packet payload sizes in KB, and the Network type - ('Bitcoin Main' if Bitcoin data traffic) + ('Bitcoin Main' if Bitcoin data traffic). -Additionally for Stratum mining, it will attempt to extract: +Connection tuples are cached when BITCOIN-MAIN traffic is detected, such that following this, +any blobs in a cached connection that do not contain BITCOIN-MAIN magic bytes, are labeled +as part of a connection containing Bitcoin traffic. + +Any traffic on BITCOIN-MAIN's designated port will be labeled as potential Bitcoin traffic. + +Additionally for Stratum mining, the plugin will attempt to extract: Bitcoin miner being used, transaction methods used in each connection (mining.notify, mining.authorize, mining.get_transaction, mining.submit, etc.), User ID (Auth ID) used to access the Bitcoin mining pool, and possibly the password @@ -35,13 +45,13 @@ def __init__(self): generation transaction (part 2), merkle tree branches (hashes), block version, and the hash difficulty (n-bits) (The generation transactions and merkle tree branches are only optionally outputted - to a file: See Example (3) below) + to a file: See Example (2) below) Note (1): The first time that all of this Stratum mining information is collected (per connection), all of the packets decoded after this point from within the same connection (same exact sip, dip, sport, dport) will continue to output the same collection of information since it - will be the same. + will be the same, and is cumulative per connection. Note (2): The gen_tx1 and gen_tx2 fields enable the miner to build the coinbase transaction for the block by concatentating gen_tx1, the extranonce1 @@ -68,20 +78,17 @@ def __init__(self): Examples: - + (1) Basic usage: decode -d bitcoin - (2) If pcap starts in middle of connection (then we need to ignore TCP handshake): - - decode -d bitcoin --bitcoin_ignore_handshake - (3) Saving Generation Transaction Data and Merkle Branches to a specified file: + (2) Saving Generation Transaction Data and Merkle Branches to a specified file: decode -d bitcoin --bitcoin_gentx='foo.txt.' ''', - filter='''tcp and port (3332 or 3333 or 3334 or 3335 or + bpf='''(tcp and port (3332 or 3333 or 3334 or 3335 or 4008 or 4012 or 4016 or 4024 or 4032 or 4048 or 4064 or 4096 or 4128 or 4256 or 5050 or 7033 or @@ -89,12 +96,17 @@ def __init__(self): 8333 or 8334 or 8336 or 8337 or 8344 or 8347 or 8361 or 8888 or 9332 or 9337 or 9999 or 11111 or - 12222 or 17777 or 18333)''', + 12222 or 17777 or 18333))''', + output=AlertOutput(label=__name__), author='dek', optiondict={ - 'gentx' : {'type' : 'str', 'default' : '', 'help' : 'The name of the file to output the fields used to generate the block transaction (gen_tx1, gen_tx2, merkle_branches).'}, + 'gentx': { + 'type': str, + 'default': None, + 'help': 'The name of the file to output the fields used to generate the block transaction (gen_tx1, gen_tx2, merkle_branches) (default: None)' }, } - ) + ) + self.auth_ids = {} self.notify_params = {} self.methods = {} @@ -104,24 +116,26 @@ def __init__(self): self.dmac = None self.bc_net = None self.size = 0 + self.bcm_cache = set() self.JSON = False self.NOTIFY = False - # blobHandler to reassemble the packets in the traffic # Bitcoin traffic uses TCP - def blobHandler(self, conn, blob): - + def blob_handler(self, conn, blob): try: - data = blob.data() + data = blob.data + data_str = ''.join(chr(x) for x in data) + data_len = len(data) except: - self.warn('could not parse session data') + self.out.log('could not parse session data') return - + # Only continue if the packet contains data if not data: return + # Default mining.notify fields to None job_id = None prev_blk_hash = None @@ -132,13 +146,13 @@ def blobHandler(self, conn, blob): difficulty = None curr_time = None clean_jobs = None - + # If the payload contains JSON - if data.startswith('{"'): + if data_str.startswith('{"'): self.JSON = True try: # split JSON objects by newline - for rawjs in data.split("\n"): + for rawjs in data_str.split("\n"): if rawjs: js = json.loads(rawjs) try: @@ -149,7 +163,7 @@ def blobHandler(self, conn, blob): if js["method"] == "mining.subscribe": self.miners[conn.addr] = js["params"][0] - + if js["method"] == "mining.authorize": if "params" in js and js['params'][0]: # Grab the Bitcoin User ID (sometimes a wallet id) @@ -159,7 +173,7 @@ def blobHandler(self, conn, blob): if js['params'][1]: self.auth_ids[conn.addr] = "".join(( self.auth_ids[conn.addr], " / ", str(js['params'][1]) )) - + if js["method"] == "mining.notify": self.NOTIFY = True if "params" in js and js['params']: @@ -171,34 +185,47 @@ def blobHandler(self, conn, blob): clean_jobs] except KeyError as e: - self.warn("{} - Error extracting auth ID".format(str(e))) + self.out.log("{} - Error extracting auth ID".format(str(e))) except ValueError as e: - self.warn('{} - json data not found'.format(str(e))) + self.out.log('{} - json data not found'.format(str(e))) return - + + # Grab the first 4 bytes of the payload to search for the magic values # used to determine which Bitcoin network is being accessed # Additionally, reformat bytes try: - magic_val = binascii.hexlify(data[0:4]).upper() + magic_val = data[0:4].hex().upper() magic_val = ' '.join([magic_val[i:i+2] for i in range(0, len(magic_val), 2)]) except: - self.warn('could not parse session data') + self.out.log('could not parse session data') return - # Attempt to translate first 4 bytes of payload - # into a Bitcoin (bc) network type + # Attempt to translate first 4 bytes of payload into a Bitcoin (bc) + # network type, and determine if blob is part of connection which + # contained BITCOIN-MAIN traffic, or if using BITCOIN-MAIN port, or + # if part of Stratum mining try: self.bc_net = str(MAGIC_VALS[magic_val]) + if self.bc_net == 'BITCOIN-MAIN': + self.bcm_cache.add(conn.addr) except: - self.bc_net = 'N/A' + if conn.addr in self.bcm_cache: + self.bc_net = 'Potential BITCOIN-MAIN (part of connection which detected BITCOIN-MAIN traffic)' + elif (blob.sport == 8333 or blob.dport == 8333): + self.bc_net = 'Potential BITCOIN-MAIN traffic (using designated port)' + # Stratum mining methods have been detected + elif (self.methods): + self.bc_net = 'STRATUM MINING' + else: + self.bc_net = 'N/A (Likely just traffic over a known Bitcoin/Stratum mining port)' # Pull pertinent information from packet's contents - self.size = '{0:.2f}'.format((len(blob.data())/1024.0)) - self.smac = conn.smac - self.dmac = conn.dmac - + self.size = '{0:.2f}'.format(data_len/1024.0) + # Pull source MAC and dest MAC from first packet in each connection + self.smac = blob.smac + self.dmac = blob.dmac # Truncate the list Job IDs per connection for printing purposes if JSON # data was found in the blob @@ -216,55 +243,57 @@ def blobHandler(self, conn, blob): self.JSON = False self.NOTIFY = False + # If able to pull the Bitcoin Pool User ID (sometimes a wallet ID) - # Also if the transcation is mining.notify (seen in Stratum mining) + # Also if the transcation is mining.notify (seen in Stratum mining) or Stratum mining + # detected via keywords # then output the current Block information if (self.size and self.smac and self.dmac and self.miners.get(conn.addr, None) and self.methods.get(conn.addr, None) and self.auth_ids.get(conn.addr, None) and self.notify_params.get(conn.addr, None) and not self.gentx): - self.alert("\n\tSRC_MAC: \t{0:<20} \n\tDST_MAC: \t{1:<20} \n\tSIZE: \t\t{2:>3}KB\n\t" - "MINER: \t\t{3:<20} \n\tMETHODS: \t{4:<25} \n\tUSER ID/PW: \t{5:<50}\n\t" - "JOB IDs: \t{6:<20} \n\tPREV BLK HASH: \t{7:<65} \n\tBLOCK VER: \t{8:<15}\n\t" - "HASH DIFF: \t{9:<10}\n\n".format( - self.smac, self.dmac, self.size, - self.miners[conn.addr], self.methods[conn.addr], self.auth_ids[conn.addr], - self.notify_params[conn.addr][0][conn.addr], self.notify_params[conn.addr][1], - self.notify_params[conn.addr][5], self.notify_params[conn.addr][6]), - ts=blob.starttime, sip=conn.sip, dip=conn.dip, sport=conn.sport, - dport=conn.dport, direction=blob.direction) + self.write("\n\tNETWORK: \t{0:<15} \n\tSRC_MAC: \t{1:<20} \n\tDST_MAC: \t{2:<20} \n\tSIZE: \t\t{3:>3}KB\n\t" + "MINER: \t\t{4:<20} \n\tMETHODS: \t{5:<25} \n\tUSER ID/PW: \t{6:<50}\n\t" + "JOB IDs: \t{7:<20} \n\tPREV BLK HASH: \t{8:<65} \n\tBLOCK VER: \t{9:<15}\n\t" + "HASH DIFF: \t{10:<10}\n\n".format( + self.bc_net, self.smac, self.dmac, self.size, + self.miners[conn.addr], ', '.join(self.methods[conn.addr]), self.auth_ids[conn.addr], + ', '.join(self.notify_params[conn.addr][0][conn.addr]), self.notify_params[conn.addr][1], + self.notify_params[conn.addr][5], self.notify_params[conn.addr][6]), + ts=blob.starttime, sip=conn.sip, dip=conn.dip, sport=conn.sport, + dport=conn.dport, direction=blob.direction) # If able to pull the Bitcoin Pool User ID (sometimes a wallet ID) - # Also if the transcation is mining.notify (seen in Stratum mining) and the user - # specifies that they want to save the fields used to generate the block transaction - # (gen_tx1, gen_tx2 (hashes with scriptPubKeys), merkle tree branches), then - # output all information possible, and write the gentx information to the specified file + # Also if the transcation is mining.notify (seen in Stratum mining) or Stratum mining + # detected via keywords and the user specifies that they want to save the fields used + # to generate the block transaction (gen_tx1, gen_tx2 (hashes with scriptPubKeys), merkle tree branches), + # then output all information possible, and write the gentx information to the specified file elif (self.size and self.smac and self.dmac and self.miners.get(conn.addr, None) and self.methods.get(conn.addr, None) and self.auth_ids.get(conn.addr, None) and self.notify_params.get(conn.addr, None) and self.gentx): - self.alert("\n\tSRC_MAC: \t{0:<20} \n\tDST_MAC: \t{1:<20} \n\tSIZE: \t\t{2:>3}KB\n\t" - "MINER: \t\t{3:<20} \n\tMETHODS: \t{4:<25} \n\tUSER ID/PW: \t{5:<50}\n\t" - "JOB IDs: \t{6:<20} \n\tPREV BLK HASH: \t{7:<65} \n\tBLOCK VER: \t{8:<15}\n\t" - "HASH DIFF: \t{9:<10}\n\n".format( - self.smac, self.dmac, self.size, - self.miners[conn.addr], self.methods[conn.addr], self.auth_ids[conn.addr], - self.notify_params[conn.addr][0][conn.addr], self.notify_params[conn.addr][1], - self.notify_params[conn.addr][5], self.notify_params[conn.addr][6]), - ts=blob.starttime, sip=conn.sip, dip=conn.dip, sport=conn.sport, - dport=conn.dport, direction=blob.direction) + self.write("\n\tNETWORK: \t{0:<15} \n\tSRC_MAC: \t{1:<20} \n\tDST_MAC: \t{2:<20} \n\tSIZE: \t\t{3:>3}KB\n\t" + "MINER: \t\t{4:<20} \n\tMETHODS: \t{5:<25} \n\tUSER ID/PW: \t{6:<50}\n\t" + "JOB IDs: \t{7:<20} \n\tPREV BLK HASH: \t{8:<65} \n\tBLOCK VER: \t{9:<15}\n\t" + "HASH DIFF: \t{10:<10}\n\n".format( + self.bc_net, self.smac, self.dmac, self.size, + self.miners[conn.addr], ', '.join(self.methods[conn.addr]), self.auth_ids[conn.addr], + ', '.join(self.notify_params[conn.addr][0][conn.addr]), self.notify_params[conn.addr][1], + self.notify_params[conn.addr][5], self.notify_params[conn.addr][6]), + ts=blob.starttime, sip=conn.sip, dip=conn.dip, sport=conn.sport, + dport=conn.dport, direction=blob.direction) # Write the verbose block information (gen tx1/2, merkle branches) gathered # from mining.notify payloads to the command-line specified output file # The extra information (JOB ID, BLOCK VER, etc.) will be useful in matching the # information outputted by the alerts to the payload containing the # generation transaction info and merkle branches - fout = open(self.gentx, "a+") + fout = open(self.gentx, "a+") fout.write(("\nJOB IDs: \t\t{0:<20} \nPREV BLK HASH: \t{1:<65} \n\nGEN TX1: \t\t{2:<20}" "\n\nGEN TX2: \t\t{3:<20} \n\nMERKLE BRANCHES: {4:<20} \n\nBLOCK VER: \t\t{5:<20}" "\nHASH DIFF: \t\t{6:<10}\n").format( - self.notify_params[conn.addr][0][conn.addr], self.notify_params[conn.addr][1], + ', '.join(self.notify_params[conn.addr][0][conn.addr]), self.notify_params[conn.addr][1], self.notify_params[conn.addr][2], self.notify_params[conn.addr][3], - self.notify_params[conn.addr][4], self.notify_params[conn.addr][5], + ', '.join(self.notify_params[conn.addr][4]), self.notify_params[conn.addr][5], self.notify_params[conn.addr][6])) fout.write(("\n" + "-"*100)*2) @@ -272,13 +301,15 @@ def blobHandler(self, conn, blob): # Else if we dont have Bitcoin User IDs, or Block information # and the user doesn't want verbose block information (gentx) elif (self.size and self.smac and self.dmac and self.bc_net): - self.alert("\n\tNETWORK: \t{0:<15} \n\tSRC_MAC: \t{1:<20} \n\tDST_MAC: \t{2:<20} \n\tSIZE: \t\t{3:>3}KB\n\n".format( + self.write("\n\tNETWORK: \t{0:<15} \n\tSRC_MAC: \t{1:<20} \n\tDST_MAC: \t{2:<20} \n\tSIZE: \t\t{3:>3}KB\n\n".format( self.bc_net, self.smac, self.dmac, self.size), ts=blob.starttime, sip=conn.sip, dip=conn.dip, sport=conn.sport, dport=conn.dport, direction=blob.direction) - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() + + return conn, blob + + + +if __name__ == "__main__": + print(DshellPlugin()) + diff --git a/dshell/plugins/protocol/ether.py b/dshell/plugins/protocol/ether.py new file mode 100644 index 0000000..b6e5826 --- /dev/null +++ b/dshell/plugins/protocol/ether.py @@ -0,0 +1,66 @@ +""" +Shows MAC address information and optionally filters by it. It is highly +recommended that oui.txt be included in the share/ directory (see README). +""" + +import os +import dshell.core +from dshell.output.output import Output +from dshell.util import get_data_path + +class DshellPlugin(dshell.core.PacketPlugin): + OUTPUT_FORMAT = "[%(plugin)s] %(dt)s %(sip)-15s %(smac)-18s %(smac_org)-35s -> %(dip)-15s %(dmac)-18s %(dmac_org)-35s %(byte_count)d\n" + + def __init__(self, *args, **kwargs): + super().__init__( + name="Ethernet", + description="Show MAC address information and optionally filter by it", + author="dev195", + output=Output(label=__name__, format=self.OUTPUT_FORMAT), + optiondict={ + "org": {"default":[], "action":"append", "metavar":"ORGANIZATION", "help":"Organizations owning MAC address to inclusively filter on (exact match only). Can be used multiple times to look for multiple organizations."}, + "org_exclusive": {"default":False, "action":"store_true", "help":"Set organization filter to be exclusive"}, + 'quiet': {'action': 'store_true', 'default':False, 'help':'disable alerts for this plugin'} + } + ) + self.oui_map = {} + + def premodule(self): + # Create a mapping of MAC address prefix to organization + # http://standards-oui.ieee.org/oui.txt + ouifilepath = os.path.join(get_data_path(), 'oui.txt') + try: + with open(ouifilepath, encoding="utf-8") as ouifile: + for line in ouifile: + if "(hex)" not in line: + continue + line = line.strip().split(None, 2) + prefix = line[0].replace('-', ':') + org = line[2] + self.oui_map[prefix] = org + except FileNotFoundError: + # user probably did not download it + # print warning and continue + self.warn("Could not find {} (see README). Will not be able to determine MAC organizations.".format(ouifilepath)) + + def packet_handler(self, pkt): + if not pkt.smac or not pkt.dmac: + return + smac_prefix = pkt.smac[:8].upper() + smac_org = self.oui_map.get(smac_prefix, '???') + dmac_prefix = pkt.dmac[:8].upper() + dmac_org = self.oui_map.get(dmac_prefix, '???') + + # Filter out any packets that do not match organization filter + if self.org: + if self.org_exclusive and (smac_org in self.org or dmac_org in self.org): + return + elif not self.org_exclusive and not (smac_org in self.org or dmac_org in self.org): + return + + if not self.quiet: + self.write("", smac_org=smac_org, dmac_org=dmac_org, **pkt.info()) + return pkt + +if __name__ == "__main__": + print(DshellPlugin()) diff --git a/dshell/plugins/protocol/ip.py b/dshell/plugins/protocol/ip.py new file mode 100644 index 0000000..607c2ed --- /dev/null +++ b/dshell/plugins/protocol/ip.py @@ -0,0 +1,24 @@ +""" +Outputs all IPv4/IPv6 traffic, and hex plus ascii with verbose flag +""" + +import dshell.core +import dshell.util +from dshell.output.alertout import AlertOutput + +class DshellPlugin(dshell.core.PacketPlugin): + + def __init__(self): + super().__init__( + name='ip', + description='IPv4/IPv6 plugin', + bpf='ip or ip6', + author='twp', + output=AlertOutput(label=__name__), + ) + + def packet_handler(self, packet): + self.write(**packet.info(), dir_arrow='->') + # If verbose flag set, outputs packet contents in hex and ascii alongside packet info + self.out.log("\n" + dshell.util.hex_plus_ascii(packet.rawpkt)) + return packet diff --git a/dshell/plugins/protocol/protocol.py b/dshell/plugins/protocol/protocol.py new file mode 100644 index 0000000..d8c175b --- /dev/null +++ b/dshell/plugins/protocol/protocol.py @@ -0,0 +1,22 @@ +""" +Tries to find traffic that does not belong to the following protocols: +TCP, UDP, or ICMP +""" + +import dshell.core +from dshell.output.alertout import AlertOutput + +class DshellPlugin(dshell.core.PacketPlugin): + + def __init__(self): + super().__init__( + name="Uncommon Protocols", + description="Finds uncommon (i.e. not tcp, udp, or icmp) protocols in IP traffic", + bpf="(ip or ip6) and not tcp and not udp and not icmp and not icmp6", + author="bg", + output=AlertOutput(label=__name__), + ) + + def packet_handler(self, packet): + self.write("PROTOCOL: {} ({})".format(packet.protocol, packet.protocol_num), **packet.info(), dir_arrow="->") + return packet diff --git a/dshell/plugins/ssl/__init__.py b/dshell/plugins/ssl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/ssl/sslblacklist.py b/dshell/plugins/ssl/sslblacklist.py new file mode 100644 index 0000000..668bcea --- /dev/null +++ b/dshell/plugins/ssl/sslblacklist.py @@ -0,0 +1,124 @@ +""" +Looks for certificates in SSL/TLS traffic and tries to find any hashes that +match those in the abuse.ch blacklist. +(https://sslbl.abuse.ch/blacklist/) +""" + +# handy reference: +# http://blog.fourthbit.com/2014/12/23/traffic-analysis-of-an-ssl-slash-tls-session + +import dshell.core +from dshell.output.alertout import AlertOutput + +import hashlib +import io +import struct + +# SSLv3/TLS version +SSL3_VERSION = 0x0300 +TLS1_VERSION = 0x0301 +TLS1_1_VERSION = 0x0302 +TLS1_2_VERSION = 0x0303 + +# Record type +SSL3_RT_CHANGE_CIPHER_SPEC = 20 +SSL3_RT_ALERT = 21 +SSL3_RT_HANDSHAKE = 22 +SSL3_RT_APPLICATION_DATA = 23 + +# Handshake message type +SSL3_MT_HELLO_REQUEST = 0 +SSL3_MT_CLIENT_HELLO = 1 +SSL3_MT_SERVER_HELLO = 2 +SSL3_MT_CERTIFICATE = 11 +SSL3_MT_SERVER_KEY_EXCHANGE = 12 +SSL3_MT_CERTIFICATE_REQUEST = 13 +SSL3_MT_SERVER_DONE = 14 +SSL3_MT_CERTIFICATE_VERIFY = 15 +SSL3_MT_CLIENT_KEY_EXCHANGE = 16 +SSL3_MT_FINISHED = 20 + + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name="sslblacklist", + author="dev195", + bpf="tcp and (port 443 or port 993 or port 1443 or port 8531)", + description="Looks for certificate SHA1 matches in the abuse.ch blacklist", + longdescription=""" + Looks for certificates in SSL/TLS traffic and tries to find any hashes that + match those in the abuse.ch blacklist. + + Requires downloading the blacklist CSV from abuse.ch: + https://sslbl.abuse.ch/blacklist/ + + If the CSV is not in the current directory, use the --sslblacklist_csv + argument to provide a file path. +""", + output=AlertOutput(label=__name__), + optiondict={ + "csv": { + "help": "filepath to the sslblacklist.csv file", + "default": "./sslblacklist.csv", + "metavar": "FILEPATH" + }, + } + ) + + def premodule(self): + self.parse_blacklist_csv(self.csv) + + def parse_blacklist_csv(self, filepath): + "parses the SSL blacklist CSV, given the 'filepath'" + # Python's standard csv module doesn't seem to handle it properly + self.hashes = {} + with open(filepath, 'r') as csv: + for line in csv: + line = line.split('#')[0] # ignore comments + line = line.strip() + try: + timestamp, sha1, reason = line.split(',', 3) + self.hashes[sha1] = reason + except ValueError: + continue + + def blob_handler(self, conn, blob): + if blob.direction == 'cs': + return None + + data = io.BytesIO(blob.data) + + # Iterate over each layer of the connection, paying special attention to the certificate + while True: + try: + content_type, proto_version, record_len = struct.unpack("!BHH", data.read(5)) + except struct.error: + break + if proto_version not in (SSL3_VERSION, TLS1_VERSION, TLS1_1_VERSION, TLS1_2_VERSION): + return None + if content_type == SSL3_RT_HANDSHAKE: + handshake_type = struct.unpack("!B", data.read(1))[0] + handshake_len = struct.unpack("!I", b"\x00"+data.read(3))[0] + if handshake_type == SSL3_MT_CERTIFICATE: + # Process the certificate itself + cert_chain_len = struct.unpack("!I", b"\x00"+data.read(3))[0] + bytes_processed = 0 + while (bytes_processed < cert_chain_len): + try: + cert_data_len = struct.unpack("!I", b"\x00"+data.read(3))[0] + cert_data = data.read(cert_data_len) + bytes_processed = 3 + cert_data_len + sha1 = hashlib.sha1(cert_data).hexdigest() + if sha1 in self.hashes: + bad_guy = self.hashes[sha1] + self.write("Certificate hash match: {}".format(bad_guy), **conn.info()) + except struct.error as e: + break + else: + # Ignore any layers that are not a certificate + data.read(handshake_len) + continue + + return conn, blob diff --git a/dshell/plugins/tftp/__init__.py b/dshell/plugins/tftp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decoders/tftp/tftp.py b/dshell/plugins/tftp/tftp.py similarity index 58% rename from decoders/tftp/tftp.py rename to dshell/plugins/tftp/tftp.py index f5a8c96..de5e9dd 100644 --- a/decoders/tftp/tftp.py +++ b/dshell/plugins/tftp/tftp.py @@ -1,5 +1,5 @@ """ -TFTP Decoder +TFTP Plugin In short: Goes through UDP traffic, packet by packet, and ties together TFTP file streams. If the command line argument is set (--tftp_rip), it will dump the @@ -7,8 +7,8 @@ In long: Goes through each UDP packet and parses out the TFTP opcode. For read or - write requests, it sets a placeholder in unsetReadStreams or unsetWriteStreams, - respectively. These placeholders are moved to openStreams when we first see + write requests, it sets a placeholder in unset_read_streams or unset_write_streams, + respectively. These placeholders are moved to open_streams when we first see data for the read request or an ACK code for a write request. The reason for these placeholders is to allow the server to set the ephemeral port during data transfer. @@ -19,34 +19,37 @@ later. When we consider a stream finished (either the DATA packet is too short or there are no more packets), we rebuild the file data, print information about the stream, dump the file (optional), and move the information from - openStreams to closedStreams. + open_streams to closed_streams. Example: Running on sample pcap available here: https://wiki.wireshark.org/TFTP With default values, it will display transfers performed - Dshell> decode -d tftp ~/pcap/tftp_*.pcap + Dshell> decode -d tftp ~/pcap/tftp_*.pcap tftp 2013-05-01 08:24:11 192.168.0.253:50618 -- 192.168.0.10:3445 ** read rfc1350.txt (24599 bytes) ** tftp 2013-04-27 05:07:59 192.168.0.1:57509 -- 192.168.0.13:2087 ** write rfc1350.txt (24599 bytes) ** - With the --tftp_rip flag, it will generate the same output while reassembling + With the --tftp_rip flag, it will generate the same output while reassembling the files and saving them in a defined directory (./tftp_out by default) - Dshell> decode -d tftp --tftp_rip --tftp_outdir=./MyTFTP ~/pcap/tftp_*.pcap + Dshell> decode -d tftp --tftp_rip --tftp_outdir=./MyTFTP ~/pcap/tftp_*.pcap tftp 2013-05-01 08:24:11 192.168.0.253:50618 -- 192.168.0.10:3445 ** read rfc1350.txt (24599 bytes) ** tftp 2013-04-27 05:07:59 192.168.0.1:57509 -- 192.168.0.13:2087 ** write rfc1350.txt (24599 bytes) ** Dshell> ls ./MyTFTP/ rfc1350.txt rfc1350.txt_01 - Note: The two files have the same name in the traffic, but have incremented + Note: The two files have the same name in the traffic, but have incremented filenames when saved - """ -import dshell -import dpkt -import struct +import dshell.core +import dshell.util +from dshell.output.alertout import AlertOutput + +from pypacker.layer4 import udp + import os +import struct -class DshellDecoder(dshell.IPDecoder): - "Primary decoder class" +class DshellPlugin(dshell.core.PacketPlugin): + "Primary plugin class" # packet opcodes (http://www.networksorcery.com/enp/default1101.htm) RRQ = 1 # read request WRQ = 2 # write request @@ -56,19 +59,26 @@ class DshellDecoder(dshell.IPDecoder): OACK = 6 # option acknowledgment def __init__(self, **kwargs): - dshell.IPDecoder.__init__(self, - name="tftp", - filter="udp", - description="Find TFTP streams and, optionally, extract the files", - longdescription="Find TFTP streams and, optionally, extract the files", - author="dev195", - optiondict={ - "rip": {"action": "store_true", "help": "Rip files from traffic (default: off)", "default": False}, - "outdir": {"help": "Directory to place files when using --rip", "default": "./tftp_out", "metavar": "DIRECTORY"}} - ) + super().__init__( + name="tftp", + bpf="udp", + description="Find TFTP streams and, optionally, extract the files", + author="dev195", + output=AlertOutput(label=__name__), + optiondict={ + "rip": { + "action": "store_true", + "help": "Rip files from traffic (default: off)", + "default": False}, + "outdir": { + "help": "Directory to place files when using --rip (default: tftp_out)", + "default": "./tftp_out", + "metavar": "DIRECTORY"} + } + ) # default information for streams we didn't see the start for - self.defaultStream = { + self.default_stream = { 'filename': '', 'mode': '', 'readwrite': '', @@ -78,41 +88,45 @@ def __init__(self, **kwargs): } # containers for various states of streams - self.openStreams = {} - self.closedStreams = [] + self.open_streams = {} + self.closed_streams = [] # These two are holders while waiting for the server to decide on which # ephemeral port to use - self.unsetWriteStreams = {} - self.unsetReadStreams = {} + self.unset_write_streams = {} + self.unset_read_streams = {} - def preModule(self): + def premodule(self): "if needed, create the directory for file output" if self.rip and not os.path.exists(self.outdir): try: os.makedirs(self.outdir) except OSError: - self.error( - "Could not create directory '%s'. Files will not be dumped." % self.outdir) + self.error("Could not create directory {!r}. Files will not be dumped.".format(self.outdir)) self.rip = False - def postModule(self): + def postmodule(self): "cleanup any unfinished streams" - self.debug("Unset Read Streams: %s" % self.unsetReadStreams) - self.debug("Unset Write Streams: %s" % self.unsetWriteStreams) - while(len(self.openStreams) > 0): - k = self.openStreams.keys()[0] + self.debug("Unset Read Streams: {!s}".format(self.unset_read_streams)) + self.debug("Unset Write Streams: {!s}".format(self.unset_write_streams)) + while(len(self.open_streams) > 0): + k = list(self.open_streams)[0] self.__closeStream(k, "POSSIBLY INCOMPLETE") - def packetHandler(self, ip=None): + def packet_handler(self, pkt): """ Handles each UDP packet. It checks the TFTP opcode and parses accordingly. """ - try: - udp = dpkt.ip.IP(ip.pkt).data - except dpkt.UnpackError: - return - data = udp.data + udpp = pkt.pkt.upper_layer + while not isinstance(udpp, udp.UDP): + try: + udpp = udpp.upper_layer + except AttributeError: + # There doesn't appear to be a UDP layer, for some reason + return + + data = udpp.body_bytes + try: flag = struct.unpack("!H", data[:2])[0] except struct.error: @@ -121,50 +135,50 @@ def packetHandler(self, ip=None): if flag == self.RRQ: # this packet is requesting to read a file from the server try: - filename, mode = data.split("\x00")[0:2] + filename, mode = data.split(b"\x00")[0:2] except ValueError: return # probably not TFTP - clientIP, clientPort, serverIP, serverPort = ip.sip, udp.sport, ip.dip, udp.dport - self.unsetReadStreams[(clientIP, clientPort, serverIP)] = { + clientIP, clientPort, serverIP, serverPort = pkt.sip, udpp.sport, pkt.dip, udpp.dport + self.unset_read_streams[(clientIP, clientPort, serverIP)] = { 'filename': filename, 'mode': mode, 'readwrite': 'read', 'closed_connection': False, 'filedata': {}, - 'timestamp': ip.ts + 'timestamp': pkt.ts } elif flag == self.WRQ: # this packet is requesting to write a file to the server try: - filename, mode = data.split("\x00")[0:2] + filename, mode = data.split(b"\x00")[0:2] except ValueError: return # probably not TFTP # in this case, we are writing to the "server" - clientIP, clientPort, serverIP, serverPort = ip.sip, udp.sport, ip.dip, udp.dport - self.unsetWriteStreams[(clientIP, clientPort, serverIP)] = { + clientIP, clientPort, serverIP, serverPort = pkt.sip, udpp.sport, pkt.dip, udpp.dport + self.unset_write_streams[(clientIP, clientPort, serverIP)] = { 'filename': filename, 'mode': mode, 'readwrite': 'write', 'closed_connection': False, 'filedata': {}, - 'timestamp': ip.ts + 'timestamp': pkt.ts } elif flag == self.DATA: # this packet is sending a chunk of data - clientIP, clientPort, serverIP, serverPort = ip.sip, udp.sport, ip.dip, udp.dport + clientIP, clientPort, serverIP, serverPort = pkt.sip, udpp.sport, pkt.dip, udpp.dport key = (clientIP, clientPort, serverIP, serverPort) - if key not in self.openStreams: + if key not in self.open_streams: # this is probably an unset read stream; there is no # acknowledgement, it just starts sending data - if (serverIP, serverPort, clientIP) in self.unsetReadStreams: - self.openStreams[key] = self.unsetReadStreams[ + if (serverIP, serverPort, clientIP) in self.unset_read_streams: + self.open_streams[key] = self.unset_read_streams[ (serverIP, serverPort, clientIP)] - del(self.unsetReadStreams[ + del(self.unset_read_streams[ (serverIP, serverPort, clientIP)]) else: - self.openStreams[key] = self.defaultStream + self.open_streams[key] = self.default_stream blockNum = struct.unpack("!H", data[:2])[0] data = data[2:] if len(data) < 512: @@ -173,30 +187,30 @@ def packetHandler(self, ip=None): closedConn = True else: closedConn = False - self.openStreams[key]['filedata'][blockNum] = data - self.openStreams[key]['closed_connection'] = closedConn + self.open_streams[key]['filedata'][blockNum] = data + self.open_streams[key]['closed_connection'] = closedConn elif flag == self.ACK: # this packet has acknowledged the receipt of a data chunk or # allows a write process to begin blockNum = struct.unpack("!H", data[:2])[0] - clientIP, clientPort, serverIP, serverPort = ip.sip, udp.sport, ip.dip, udp.dport + clientIP, clientPort, serverIP, serverPort = pkt.sip, udpp.sport, pkt.dip, udpp.dport # special case: this is acknowledging a write operation and sets # the port for receiving if blockNum == 0: - clientIP, clientPort, serverIP, serverPort = ip.dip, udp.dport, ip.sip, udp.sport + clientIP, clientPort, serverIP, serverPort = pkt.dip, udpp.dport, pkt.sip, udpp.sport i = (clientIP, clientPort, serverIP) - if i in self.unsetWriteStreams: - self.openStreams[ - (clientIP, clientPort, serverIP, serverPort)] = self.unsetWriteStreams[i] - del(self.unsetWriteStreams[i]) + if i in self.unset_write_streams: + self.open_streams[ + (clientIP, clientPort, serverIP, serverPort)] = self.unset_write_streams[i] + del(self.unset_write_streams[i]) # otherwise, check if this is the confirmation for the end of a # connection - elif (clientIP, clientPort, serverIP, serverPort) in self.openStreams and self.openStreams[(clientIP, clientPort, serverIP, serverPort)]['closed_connection']: + elif (clientIP, clientPort, serverIP, serverPort) in self.open_streams and self.open_streams[(clientIP, clientPort, serverIP, serverPort)]['closed_connection']: self.__closeStream( (clientIP, clientPort, serverIP, serverPort)) - elif (serverIP, serverPort, clientIP, clientPort) in self.openStreams and self.openStreams[(serverIP, serverPort, clientIP, clientPort)]['closed_connection']: + elif (serverIP, serverPort, clientIP, clientPort) in self.open_streams and self.open_streams[(serverIP, serverPort, clientIP, clientPort)]['closed_connection']: self.__closeStream( (serverIP, serverPort, clientIP, clientPort)) @@ -206,29 +220,33 @@ def packetHandler(self, ip=None): errCode = struct.unpack("!H", data[:2])[0] errMessage = data[2:].strip() if errCode == 1: # File not found - clientIP, clientPort, serverIP, serverPort = ip.dip, udp.dport, ip.sip, udp.sport + clientIP, clientPort, serverIP, serverPort = pkt.dip, udpp.dport, pkt.sip, udpp.sport i = (clientIP, clientPort, serverIP) - if i in self.unsetReadStreams: - self.openStreams[ - (serverIP, serverPort, clientIP, clientPort)] = self.unsetReadStreams[i] - del(self.unsetReadStreams[i]) + if i in self.unset_read_streams: + self.open_streams[ + (serverIP, serverPort, clientIP, clientPort)] = self.unset_read_streams[i] + del(self.unset_read_streams[i]) self.__closeStream( (serverIP, serverPort, clientIP, clientPort), errMessage) elif flag == self.OACK: pass # TODO handle options + return pkt + def __closeStream(self, key, message=''): """ Called when a stream is finished. It moves the stream from - openStreams to closedStreams, prints output, and dumps the file + open_streams to closed_streams, prints output, and dumps the file """ - theStream = self.openStreams[key] + theStream = self.open_streams[key] if not theStream['filename']: message = "INCOMPLETE -- missing filename" + else: + theStream['filename'] = theStream['filename'].decode('utf-8', "backslashreplace") # Rebuild the file from the individual blocks - rebuiltFile = '' + rebuiltFile = b'' for i in sorted(theStream['filedata'].keys()): rebuiltFile += theStream['filedata'][i] @@ -240,51 +258,26 @@ def __closeStream(self, key, message=''): ipsNports = key # print out information about the stream - msg = "%s %s (%s bytes) %s" % ( - theStream['readwrite'], theStream['filename'], len(rebuiltFile), message) - self.alert(msg, ts=theStream['timestamp'], sip=ipsNports[0], - sport=ipsNports[1], dip=ipsNports[2], dport=ipsNports[3]) + msg = "{:5} {} ({} bytes) {}".format( + theStream['readwrite'], + theStream['filename'], + len(rebuiltFile), + message) + self.write(msg, ts=theStream['timestamp'], sip=ipsNports[0], + sport=ipsNports[1], dip=ipsNports[2], dport=ipsNports[3], + readwrite=theStream['readwrite'], filename=theStream['filename']) # dump the file, if that's what the user wants if self.rip and len(rebuiltFile) > 0: - outpath = self.__localfilename( - self.outdir, theStream['filename']) + outpath = dshell.util.gen_local_filename(self.outdir, theStream['filename']) outfile = open(outpath, 'wb') outfile.write(rebuiltFile) outfile.close() # remove the stream from the list of open streams - self.closedStreams.append(( + self.closed_streams.append(( key, - self.openStreams[key]['closed_connection'] + self.open_streams[key]['closed_connection'] )) - del(self.openStreams[key]) - - def __localfilename(self, path, origname): - """ - Generates a local file name based on the original - Taken from FTP decoder - """ - tmp = origname.replace("\\", "_") - tmp = tmp.replace("/", "_") - tmp = tmp.replace(":", "_") - localname = '' - for c in tmp: - if ord(c) > 32 and ord(c) < 127: - localname += c - else: - localname += "%%%02X" % ord(c) - localname = path + '/' + localname - postfix = '' - i = 0 - while os.path.exists(localname + postfix): - i += 1 - postfix = "_%02d" % i - return localname + postfix - + del(self.open_streams[key]) -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() diff --git a/dshell/plugins/visual/__init__.py b/dshell/plugins/visual/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/visual/piecharts.py b/dshell/plugins/visual/piecharts.py new file mode 100644 index 0000000..404fc26 --- /dev/null +++ b/dshell/plugins/visual/piecharts.py @@ -0,0 +1,314 @@ +""" +Plugin that generates HTML+JavaScript pie charts for flow information +""" + +import dshell.core +from dshell.output.output import Output + +import operator +from collections import defaultdict + +class VisualizationOutput(Output): + """ + Special output class intended to only be used for this specific plugin. + """ + + _DEFAULT_FORMAT='{"value":%(data)s, "datatype":"%(datatype)s", "label":"%(label)s"},' + + _HTML_HEADER = """ + + + + Dshell - Pie Chart Output + + + + + +
+
+ + + + + + + + + + + + + + +
+

Source Countries

+
+
+

Destination Countries

+
+
+

Source ASNs

+
+
+

Destination ASNs

+
+
+

Source Ports

+
+
+

Destination Ports

+
+
+

Protocols

+
+
+
+ + +
+
+ + + +""" + + def setup(self): + Output.setup(self) + self.fh.write(self._HTML_HEADER) + + def close(self): + self.fh.write(self._HTML_FOOTER) + Output.close(self) + +class DshellPlugin(dshell.core.ConnectionPlugin): + + def __init__(self): + super().__init__( + name='Pie Charts', + author='dev195', + bpf="ip", + description='Generates visualizations based on connections', + longdescription=""" +Generates HTML+JavaScript pie chart visualizations based on connections. + +Output should be redirected to a file and placed in a directory that has the d3.js JavaScript library. Library is available for download at https://d3js.org/ +""", + output=VisualizationOutput(label=__name__), + ) + + self.top_x = 10 + + def premodule(self): + "Set each of the counter dictionaries as defaultdict(int)" + # source + self.s_country_count = defaultdict(int) + self.s_asn_count = defaultdict(int) + self.s_port_count = defaultdict(int) + self.s_ip_count = defaultdict(int) + # dest + self.d_country_count = defaultdict(int) + self.d_asn_count = defaultdict(int) + self.d_port_count = defaultdict(int) + self.d_ip_count = defaultdict(int) + # protocol + self.proto = defaultdict(int) + + + def postmodule(self): + "Write the top X results for each type of data we're counting" + t = self.top_x + 1 + for i in sorted(self.proto.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0]: + self.write(int(i[1]), datatype="protocol", label=i[0]) + for i in sorted(self.s_country_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0] and i[0] != '--': + self.write(int(i[1]), datatype="source_country", label=i[0]) + for i in sorted(self.d_country_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0] and i[0] != '--': + self.write(int(i[1]), datatype="dest_country", label=i[0]) + for i in sorted(self.s_asn_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0] and i[0] != '--': + self.write(int(i[1]), datatype="source_asn", label=i[0]) + for i in sorted(self.d_asn_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0] and i[0] != '--': + self.write(int(i[1]), datatype="dest_asn", label=i[0]) + for i in sorted(self.s_port_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0]: + self.write(int(i[1]), datatype="source_port", label=i[0]) + for i in sorted(self.d_port_count.items(), reverse=True, key=operator.itemgetter(1))[:t]: + if i[0]: + self.write(int(i[1]), datatype="dest_port", label=i[0]) + + def connection_handler(self, conn): + "For each conn, increment the counts for the relevant dictionary keys" + self.proto[conn.protocol] += 1 + self.s_country_count[conn.sipcc] += 1 + self.s_asn_count[conn.sipasn] += 1 + self.s_port_count[conn.sport] += 1 + self.s_ip_count[conn.sip] += 1 + self.d_country_count[conn.dipcc] += 1 + self.d_asn_count[conn.dipasn] += 1 + self.d_port_count[conn.dport] += 1 + self.d_ip_count[conn.dip] += 1 + return conn + diff --git a/dshell/plugins/voip/__init__.py b/dshell/plugins/voip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/decoders/voip/rtp.py b/dshell/plugins/voip/rtp.py similarity index 62% rename from decoders/voip/rtp.py rename to dshell/plugins/voip/rtp.py index a4f242e..7481af5 100644 --- a/decoders/voip/rtp.py +++ b/dshell/plugins/voip/rtp.py @@ -1,28 +1,25 @@ -# -# Author: MM - https://github.com/1modm -# -# RTP provides end-to-end network transport functions suitable for applications transmitting real-time data, -# such as audio, video or simulation data, over multicast or unicast network services. -# -# RFC: https://www.ietf.org/rfc/rfc3550.txt -# -# RTP Payload: -# https://tools.ietf.org/html/rfc2198 -# https://tools.ietf.org/html/rfc4855 -# https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml - -import dshell -import dpkt +""" +Real-time transport protocol (RTP) capture plugin +""" + import datetime -class DshellDecoder(dshell.UDPDecoder): +import dshell.core +from dshell.output.alertout import AlertOutput + +from pypacker.layer4 import udp +from pypacker.layer567 import rtp + +class DshellPlugin(dshell.core.PacketPlugin): def __init__(self): - dshell.UDPDecoder.__init__(self, - name='rtp', - description='Real-time transport protocol (RTP) capture decoder', - longdescription=""" -The real-time transport protocol (RTP) decoder will extract the Hosts, Payload Type, Synchronization source, + super().__init__( + name="RTP", + author="mm/dev195", + bpf="udp", + description="Real-time transport protocol (RTP) capture plugin", + longdescription=""" +The real-time transport protocol (RTP) plugin will extract the Hosts, Payload Type, Synchronization source, Sequence Number, Padding, Marker and Client MAC address from every RTP packet found in the given pcap. General usage: @@ -58,11 +55,10 @@ def __init__(self): Contributing source (32 bits): 0, Padding (1 bit): 0, Extension (1 bit): 0, Marker (1 bit): 0 ** """, - filter='udp', - author='mm' - ) + output=AlertOutput(label=__name__) + ) - def preModule(self): + def premodule(self): self.payload_type = {0: "PCMU - Audio - 8000 Hz - 1 Channel", 1: "Reserved", 2: "Reserved", 3: "GSM - Audio - 8000 Hz - 1 Channel", 4: "G723 - Audio - 8000 Hz - 1 Channel", 5: "DVI4 - Audio - 8000 Hz - 1 Channel", 6: "DVI4 - Audio - 16000 Hz - 1 Channel", 7: "LPC - Audio - 8000 Hz - 1 Channel", 8: "PCMA - Audio - 8000 Hz - 1 Channel", 9: "G722 - Audio - 8000 Hz - 1 Channel", @@ -82,22 +78,28 @@ def preModule(self): for i in range(96,128): self.payload_type[i] = "Dynamic" - def packetHandler(self, udp, data): - try: - if dpkt.rtp.RTP(data): - rtppkt = dpkt.rtp.RTP(data) - pt = self.payload_type.get(rtppkt.pt) - - self.alert('\n\tFrom: {0} ({1}) to {2} ({3}) \n\tPayload Type (7 bits): {4}\n\tSequence Number (16 bits): {5}\n\tTimestamp (32 bits): {6} \n\tSynchronization source (32 bits): {7}\n\tArrival Time: {8} --> {9}\n\tContributing source (32 bits): {10}, Padding (1 bit): {11}, Extension (1 bit): {12}, Marker (1 bit): {13}\n'.format( - udp.sip, udp.smac, udp.dip, udp.dmac, pt, rtppkt.seq, rtppkt.ts, rtppkt.ssrc, - udp.ts, datetime.datetime.utcfromtimestamp(udp.ts), - rtppkt.cc, rtppkt.p, rtppkt.x, rtppkt.m), **udp.info()) - - except dpkt.UnpackError, e: - pass - -if __name__ == '__main__': - dObj = DshellDecoder() - print dObj -else: - dObj = DshellDecoder() \ No newline at end of file + def packet_handler(self, pkt): + # Scrape out the UDP layer of the packet + udpp = pkt.pkt.upper_layer + while not isinstance(udpp, udp.UDP): + try: + udpp = udpp.upper_layer + except AttributeError: + # There doesn't appear to be an UDP layer + return + + # Parse the RTP protocol from above the UDP layer + rtpp = rtp.RTP(udpp.body_bytes) + + if rtpp.version != 2: + # RTP should always be version 2 + return + + pt = self.payload_type.get(rtpp.pt, "??") + + self.write("\n\tFrom: {0} ({1}) to {2} ({3}) \n\tPayload Type (7 bits): {4}\n\tSequence Number (16 bits): {5}\n\tTimestamp (32 bits): {6} \n\tSynchronization source (32 bits): {7}\n\tArrival Time: {8} --> {9}\n\tContributing source (32 bits): {10}, Padding (1 bit): {11}, Extension (1 bit): {12}, Marker (1 bit): {13}\n".format( + pkt.sip, pkt.smac, pkt.dip, pkt.dmac, pt, rtpp.seq, rtpp.ts, + rtpp.ssrc, pkt.ts, datetime.datetime.utcfromtimestamp(pkt.ts), + rtpp.cc, rtpp.p, rtpp.x, rtpp.m), **pkt.info()) + + return pkt diff --git a/dshell/plugins/voip/sip.py b/dshell/plugins/voip/sip.py new file mode 100644 index 0000000..52cf7b5 --- /dev/null +++ b/dshell/plugins/voip/sip.py @@ -0,0 +1,174 @@ +""" + Author: MM - https://github.com/1modm + + The Session Initiation Protocol (SIP) is the IETF protocol for VOIP and other + text and multimedia sessions and is a communications protocol for signaling + and controlling. + SIP is independent from the underlying transport protocol. It runs on the + Transmission Control Protocol (TCP), the User Datagram Protocol (UDP) or the + Stream Control Transmission Protocol (SCTP) + + Rate and codec calculation thanks to https://git.ucd.ie/volte-and-of/voip-pcapy + + RFC: https://www.ietf.org/rfc/rfc3261.txt + + SIP is a text-based protocol with syntax similar to that of HTTP. + There are two different types of SIP messages: requests and responses. + - Requests initiate a SIP transaction between two SIP entities for + establishing, controlling, and terminating sessions. + - Responses are send by the user agent server indicating the result of a + received request. + + - SIP session setup example: + + Alice's . . . . . . . . . . . . . . . . . . . . Bob's + softphone SIP Phone + | | | | + | INVITE F1 | | | + |--------------->| INVITE F2 | | + | 100 Trying F3 |--------------->| INVITE F4 | + |<---------------| 100 Trying F5 |--------------->| + | |<-------------- | 180 Ringing F6 | + | | 180 Ringing F7 |<---------------| + | 180 Ringing F8 |<---------------| 200 OK F9 | + |<---------------| 200 OK F10 |<---------------| + | 200 OK F11 |<---------------| | + |<---------------| | | + | ACK F12 | + |------------------------------------------------->| + | Media Session | + |<================================================>| + | BYE F13 | + |<-------------------------------------------------| + | 200 OK F14 | + |------------------------------------------------->| + | | + +""" + +import dshell.core +from dshell.output.colorout import ColorOutput + +from pypacker.layer4 import udp +from pypacker.layer567 import sip + +class DshellPlugin(dshell.core.PacketPlugin): + + def __init__(self): + super().__init__( + name="SIP", + author="mm/dev195", + output=ColorOutput(label=__name__), + bpf="udp", + description="(UNFINISHED) Session Initiation Protocol (SIP) capture plugin", + longdescription=""" +The Session Initiation Protocol (SIP) plugin will extract the Call ID, User agent, Codec, Method, +SIP call, Host, and Client MAC address from every SIP request or response packet found in the given pcap. + +General usage: + decode -d sip + +Detailed usage: + decode -d sip --sip_showpkt + +Layer2 sll usage: + decode -d sip --no-vlan --layer2=sll.SLL + +SIP over TCP: + decode -d sip --bpf 'tcp' + +SIP is a text-based protocol with syntax similar to that of HTTP, so you can use followstream plugin: + decode -d followstream --ebpf 'port 5060' --bpf 'udp' + +Examples: + + https://wiki.wireshark.org/SampleCaptures#SIP_and_RTP + http://vignette3.wikia.nocookie.net/networker/images/f/fb/Sample_SIP_call_with_RTP_in_G711.pcap/revision/latest?cb=20140723121754 + + decode -d sip metasploit-sip-invite-spoof.pcap + decode -d sip Sample_SIP_call_with_RTP_in_G711.pcap + +Output: + + <-- SIP Request --> + Timestamp: 2016-09-21 22:44:28.220185 UTC - Protocol: UDP - Size: 435 bytes + Sequence and Method: 1 ACK + From: 10.5.1.8:5060 (00:20:80:a1:13:db) to 10.5.1.7:5060 (15:2a:01:b4:0f:47) + Via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK940bdac4-8a13-1410-9e58-08002772a6e9;rport + SIP call: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 --> "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce + Call ID: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC + + --> SIP Response <-- + Timestamp: 2016-09-21 22:44:27.849761 UTC - Protocol: UDP - Size: 919 bytes + Sequence and Method: 1 INVITE + From: 10.5.1.7:5060 (02:0a:40:12:30:23) to 10.5.1.8:5060 (d5:02:03:94:31:1b) + Via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 + SIP call: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 --> "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce + Call ID: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC + Codec selected: PCMU + Rate selected: 8000 + +Detailed Output: + + --> SIP Response <-- + Timestamp: 2016-09-21 22:44:25.360974 UTC - Protocol: UDP - Size: 349 bytes + From: 10.5.1.7:5060 (15:2a:01:b4:0f:47) to 10.5.1.8:5060 (00:20:80:a1:13:db) + SIP/2.0 100 Trying + content-length: 0 + via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 + from: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 + to: + cseq: 1 INVITE + call-id: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC + + --> SIP Response <-- + Timestamp: 2016-09-21 22:44:25.387780 UTC - Protocol: UDP - Size: 585 bytes + From: 10.5.1.7:5060 (15:2a:01:b4:0f:47) to 10.5.1.8:5060 (00:20:80:a1:13:db) + SIP/2.0 180 Ringing + content-length: 0 + via: SIP/2.0/UDP 10.5.1.8:5060;branch=z9hG4bK26a8d5c4-8a13-1910-9d58-08002772a6e9;rport=5060;received=10.5.1.8 + from: "M" ;tag=0ba2d5c4-8a13-1910-9d56-08002772a6e9 + require: 100rel + rseq: 694867676 + user-agent: Ekiga/4.0.1 + to: "miguel" ;tag=84538c9d-ba7e-e611-937f-68a3c4f0d6ce + contact: "miguel" + cseq: 1 INVITE + allow: INVITE,ACK,OPTIONS,BYE,CANCEL,SUBSCRIBE,NOTIFY,REFER,MESSAGE,INFO,PING,PRACK + call-id: 0ba2d5c4-8a13-1910-9d57-08002772a6e9@M-PC +""", + optiondict={ + "showpkt": { + "action": "store_true", + "default": False, + "help": "Display the full SIP response or request body" + } + } + ) + + self.rate = None + self.codec = None + self.direction = None + + def packet_handler(self, pkt): + self.rate = str() + self.codec = str() + self.direction = str() + + # Scrape out the UDP layer of the packet + udpp = pkt.pkt.upper_layer + while not isinstance(udpp, udp.UDP): + try: + udpp = udpp.upper_layer + except AttributeError: + # There doesn't appear to be an UDP layer + return + + # Check if exists SIP Request + if sip.SIP(udpp.body_bytes): + siptxt = "<-- SIP Request -->" + sippkt = sip.SIP(udpp.body_bytes) + self.direction = "sc" + self.output = True + + # TODO finish SIP plugin (pypacker needs to finish SIP, too) diff --git a/dshell/plugins/wifi/__init__.py b/dshell/plugins/wifi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dshell/plugins/wifi/wifi80211.py b/dshell/plugins/wifi/wifi80211.py new file mode 100644 index 0000000..5dc7db2 --- /dev/null +++ b/dshell/plugins/wifi/wifi80211.py @@ -0,0 +1,88 @@ +""" +Shows 802.11 information for individual packets. +""" + +import dshell.core +from dshell.output.output import Output + +from pypacker.layer12 import ieee80211 + +# Create a dictionary of string representations of frame types +TYPE_KEYS = { + ieee80211.MGMT_TYPE: "MGMT", + ieee80211.CTL_TYPE: "CTRL", + ieee80211.DATA_TYPE: "DATA" +} + +# Create a dictionary of subtype keys from constants defined in ieee80211 +# Its keys will be tuple pairs of (TYPE, SUBTYPE) +SUBTYPE_KEYS = dict() +# Management frame subtypes +SUBTYPE_KEYS.update(dict(((ieee80211.MGMT_TYPE, v), k[2:]) for k, v in ieee80211.__dict__.items() if type(v) == int and k.startswith("M_"))) +# Control frame subtypes +SUBTYPE_KEYS.update(dict(((ieee80211.CTL_TYPE, v), k[2:]) for k, v in ieee80211.__dict__.items() if type(v) == int and k.startswith("C_"))) +# Data frame subtypes +SUBTYPE_KEYS.update(dict(((ieee80211.DATA_TYPE, v), k[2:]) for k, v in ieee80211.__dict__.items() if type(v) == int and k.startswith("D_"))) + +class DshellPlugin(dshell.core.PacketPlugin): + + OUTPUT_FORMAT = "[%(plugin)s] %(dt)s [%(ftype)s] [%(encrypted)s] [%(fsubtype)s] %(bodybytes)r %(retry)s\n" + + def __init__(self, *args, **kwargs): + super().__init__( + name="802.11", + description="Show 802.11 packet information", + author="dev195", + bpf="wlan type mgt or wlan type ctl or wlan type data", + output=Output(label=__name__, format=self.OUTPUT_FORMAT), + optiondict={ + "ignore_mgt": {"action": "store_true", "help": "Ignore management frames"}, + "ignore_ctl": {"action": "store_true", "help": "Ignore control frames"}, + "ignore_data": {"action": "store_true", "help": "Ignore data frames"}, + "ignore_beacon": {"action": "store_true", "help": "Ignore beacons"}, + }, + longdescription=""" +Shows basic information for 802.11 packets, including: + - Frame type + - Encryption + - Frame subtype + - Data sample +""" + ) + + def handle_plugin_options(self): + "Update the BPF based on 'ignore' flags" + # NOTE: This function is naturally called in decode.py + bpf_pieces = [] + if not self.ignore_mgt: + if self.ignore_beacon: + bpf_pieces.append("(wlan type mgt and not wlan type mgt subtype beacon)") + else: + bpf_pieces.append("wlan type mgt") + if not self.ignore_ctl: + bpf_pieces.append("wlan type ctl") + if not self.ignore_data: + bpf_pieces.append("wlan type data") + self.bpf = " or ".join(bpf_pieces) + + def packet_handler(self, pkt): + try: + frame = pkt.pkt.ieee80211 + except AttributeError: + frame = pkt.pkt + encrypted = "encrypted" if frame.protected else " " + frame_type = TYPE_KEYS.get(frame.type, '----') + frame_subtype = SUBTYPE_KEYS.get((frame.type, frame.subtype), "") + retry = "[resent]" if frame.retry else "" + bodybytes = frame.body_bytes[:50] + + self.write( + encrypted=encrypted, + ftype=frame_type, + fsubtype=frame_subtype, + retry=retry, + bodybytes=bodybytes, + **pkt.info() + ) + + return pkt diff --git a/dshell/plugins/wifi/wifibeacon.py b/dshell/plugins/wifi/wifibeacon.py new file mode 100644 index 0000000..9ead90b --- /dev/null +++ b/dshell/plugins/wifi/wifibeacon.py @@ -0,0 +1,66 @@ +""" +Shows 802.11 wireless beacons and related information +""" + +from collections import defaultdict +from datetime import datetime + +import dshell.core +from dshell.output.output import Output + +class DshellPlugin(dshell.core.PacketPlugin): + + OUTPUT_FORMAT = "[%(plugin)s]\t%(dt)s\tInterval: %(interval)s TU,\tSSID: %(ssid)s\t%(count)s\n" + + def __init__(self, *args, **kwargs): + super().__init__( + name="Wi-fi Beacons", + description="Show SSIDs of 802.11 wireless beacons", + author="dev195", + bpf="wlan type mgt subtype beacon", + output=Output(label=__name__, format=self.OUTPUT_FORMAT), + optiondict={ + "group": {"action": "store_true", "help": "Group beacons together with counts"}, + } + ) + self.group_counts = defaultdict(int) + self.group_times = defaultdict(datetime.now) + + def packet_handler(self, pkt): + # Extract 802.11 frame from packet + try: + frame = pkt.pkt.ieee80211 + except AttributeError: + frame = pkt.pkt + + # Confirm that packet is, in fact, a beacon + if not frame.is_beacon(): + return + + # Extract SSID from frame + beacon = frame.beacon + ssid = "" + try: + for param in beacon.params: + # Find the SSID parameter + if param.id == 0: + ssid = param.body_bytes.decode("utf-8") + break + except IndexError: + # Sometimes pypacker fails to parse a packet + return + + if self.group: + self.group_counts[(ssid, beacon.interval)] += 1 + self.group_times[(ssid, beacon.interval)] = pkt.ts + else: + self.write(ssid=ssid, interval=beacon.interval, **pkt.info()) + + return pkt + + def postfile(self): + if self.group: + for key, val in self.group_counts.items(): + ssid, interval = key + dt = self.group_times[key] + self.write(ssid=ssid, interval=interval, plugin=self.name, dt=dt, count=val) diff --git a/dshell/util.py b/dshell/util.py new file mode 100644 index 0000000..37669bc --- /dev/null +++ b/dshell/util.py @@ -0,0 +1,151 @@ +""" +A collection of useful utilities used in several plugins and libraries. +""" + +import os +import string + +def xor(xinput, key): + """ + Xor an input string with a given character key. + + Arguments: + input: plain text input string + key: xor key + """ + output = ''.join([chr(ord(c) ^ key) for c in xinput]) + return output + + +def get_data_path(): + dpath = os.path.dirname(__file__) + return os.path.sep.join( (dpath, 'data') ) + +def get_plugin_path(): + dpath = os.path.dirname(__file__) + return os.path.sep.join( (dpath, 'plugins') ) + +def get_output_path(): + dpath = os.path.dirname(__file__) + return os.path.sep.join( (dpath, 'output') ) + +def decode_base64(intext, alphabet='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', padchar='='): + """ + Decodes a base64-encoded string, optionally using a custom alphabet. + + Arguments: + intext: input plaintext string + alphabet: base64 alphabet to use + padchar: padding character + """ + # Build dictionary from alphabet + alphabet_index = {} + for i, c in enumerate(alphabet): + if c in alphabet_index: + raise ValueError("'{}' used more than once in alphabet".format(c)) + alphabet_index[c] = i + alphabet_index[padchar] = 0 + + alphabet += padchar + + outtext = '' + intext = intext.rstrip('\n') + + i = 0 + while i < len(intext) - 3: + if intext[i] not in alphabet or intext[i + 1] not in alphabet or intext[i + 2] not in alphabet or intext[i + 3] not in alphabet: + raise KeyError("Non-alphabet character in encoded text.") + val = alphabet_index[intext[i]] * 262144 + val += alphabet_index[intext[i + 1]] * 4096 + val += alphabet_index[intext[i + 2]] * 64 + val += alphabet_index[intext[i + 3]] + i += 4 + for factor in [65536, 256, 1]: + outtext += chr(int(val / factor)) + val = val % factor + + return outtext + + +def printable_text(intext, include_whitespace=True): + """ + Replaces non-printable characters with dots. + + Arguments: + intext: input plaintext string + include_whitespace (bool): set to False to mark whitespace characters + as unprintable + """ + printable = string.ascii_letters + string.digits + string.punctuation + if include_whitespace: + printable += string.whitespace + + if isinstance(intext, bytes): + intext = intext.decode("ascii", errors="replace") + + outtext = [c if c in printable else '.' for c in intext] + outtext = ''.join(outtext) + + return outtext + + +def hex_plus_ascii(data, width=16, offset=0): + """ + Converts a data string into a two-column hex and string layout, + similar to tcpdump with -X + + Arguments: + data: incoming data to format + width: width of the columns + offset: offset output from the left by this value + """ + output = "" + for i in range(0, len(data), width): + s = data[i:i + width] + if isinstance(s, bytes): + outhex = ' '.join(["{:02X}".format(x) for x in s]) + else: + outhex = ' '.join(["{:02X}".format(ord(x)) for x in s]) + outstr = printable_text(s, include_whitespace=False) + outstr = "{:08X} {:49} {}\n".format(i + offset, outhex, outstr) + output += outstr + return output + +def gen_local_filename(path, origname): + """ + Generates a local filename based on the original. Automatically adds a + number to the end, if file already exists. + + Arguments: + path: output path for file + origname: original name of the file to transform + """ + + tmp = origname.replace("\\", "_") + tmp = tmp.replace("/", "_") + tmp = tmp.replace(":", "_") + localname = '' + for c in tmp: + if ord(c) > 32 and ord(c) < 127: + localname += c + else: + localname += "%%%02X" % ord(c) + localname = os.path.join(path, localname) + postfix = '' + i = 0 + while os.path.exists(localname + postfix): + i += 1 + postfix = "_{:04d}".format(i) + return localname + postfix + +def human_readable_filesize(bytecount): + """ + Converts the raw byte counts into a human-readable format + https://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size/1094933#1094933 + """ + for unit in ('B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'): + if abs(bytecount) < 1024.0: + return "{:3.2f} {}".format(bytecount, unit) + bytecount /= 1024.0 + return "{:3.2f} {}".format(bytecount, "YB") + diff --git a/install-ubuntu.py b/install-ubuntu.py deleted file mode 100755 index 75f1b4d..0000000 --- a/install-ubuntu.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -from pkgutil import iter_modules -from subprocess import call - -dependencies = { - "Crypto": "crypto", - "dpkt": "dpkt", - "IPy": "ipy", - "pcap": "pypcap" -} - -installed, missing_pkgs = [pkg[1] for pkg in iter_modules()], [] - -for module, pkg in dependencies.items(): - if module not in installed: - print("dshell requires {}".format(module)) - missing_pkgs.append("python-{}".format(pkg)) - else: - print("{} is installed".format(module)) - -if missing_pkgs: - cmd = ["sudo", "apt-get", "install"] + missing_pkgs - - print(" ".join(cmd)) - call(cmd) - -call(["make", "all"]) diff --git a/lib/dfile.py b/lib/dfile.py deleted file mode 100644 index 2fc9145..0000000 --- a/lib/dfile.py +++ /dev/null @@ -1,178 +0,0 @@ -''' -Dshell external file class/utils -for use in rippers, dumpers, etc. - -@author: amm -''' -import os -from dshell import Blob -from shutil import move -from hashlib import md5 - -''' -Mode Constants -''' -FILEONDISK = 1 # Object refers to file already written to disk -FILEINMEMORY = 2 # Object contains file contents in data member - -''' -dfile -- Dshell file class. - -Extends blob for offset based file chunk (segment) reassembly. -Removes time and directionality from segments. - -Decoders can instantiate this class and pass it to -output modules or other decoders. - -Decoders can choose to pass a file in memory or already -written to disk. - -A dfile object can have one of the following modes: - FILEONDISK - FILEINMEMORY - -''' - - -class dfile(Blob): - - def __init__(self, mode=FILEINMEMORY, name=None, data=None, **kwargs): - - # Initialize Segments - # Only really used in memory mode - self.segments = {} - self.startoffset = 0 - self.endoffset = 0 - - # Initialize consistent info members - self.mode = mode - self.name = name - self.diskpath = None - self.info_keys = [ - 'mode', 'name', 'diskpath', 'startoffset', 'endoffset'] - - # update with additional info - self.info(**kwargs) - # update data - if data != None: - self.update(data) - - def __iter__(self): - ''' - Undefined - ''' - pass - - def __str__(self): - ''' - Returns filename (string) - ''' - return self.name - - def __repr__(self): - ''' - Returns filename (string) - ''' - return self.name - - def md5(self): - ''' - Returns md5 of file - Calculate based on reassembly from FILEINMEMORY - or loads from FILEONDISK - ''' - if self.mode == FILEINMEMORY: - return md5(self.data()).hexdigest() - elif self.mode == FILEONDISK: - m = md5() - fh = open(self.diskpath, 'r') - m.update(fh.read()) - fh.close() - return m.hexdigest() - else: - return None - - def load(self): - ''' - Load file from disk. Converts object to mode FILEINMEMORY - ''' - if not self.mode == FILEONDISK: - return False - try: - fh = open(self.diskpath, 'r') - self.update(fh.read()) - fh.close() - self.mode = FILEINMEMORY - except: - return False - - def write(self, path='.', name=None, clobber=False, errorHandler=None, padding=None, overlap=True): - ''' - Write file contents at location relative to path. - Name on disk will be based on internal name unless one is provided. - - For mode FILEINMEMORY, file will data() will be called for reconstruction. - After writing to disk, mode will be changed to FILEONDISK. - If mode is already FILEONDISK, file will be moved to new location. - - ''' - olddiskpath = self.diskpath - if name == None: - name = self.name - self.diskpath = self.__localfilename(name, path, clobber) - if self.mode == FILEINMEMORY: - fh = open(self.diskpath, 'w') - fh.write(self.data()) - fh.close() - self.segments = {} - self.startoffset = 0 - self.endoffset = 0 - return self.diskpath - elif self.mode == FILEONDISK: - move(olddiskpath, self.diskpath) - return self.diskpath - - def update(self, data, offset=None): - if self.mode != FILEINMEMORY: - return - # if offsets are not being provided, just keep packets in wire order - if offset == None: - offset = self.endoffset - # don't buffer duplicate packets - if offset not in self.segments: - self.segments[offset] = data - # update the end offset if this packet goes at the end - if offset >= self.endoffset: - self.endoffset = offset + len(data) - - # - # Generate a local (extracted) filename based on the original - # - def __localfilename(self, origname, path='.', clobber=False): - tmp = origname.replace("\\", "_") - tmp = tmp.replace("/", "_") - tmp = tmp.replace(":", "_") - tmp = tmp.replace("?", "_") - tmp = tmp.lstrip('_') - localname = '' - for c in tmp: - if ord(c) > 32 and ord(c) < 127: - localname += c - else: - localname += "%%%02X" % ord(c) - # Truncate (from left) to max filename length on filesystem (-3 in case - # we need to add a suffix) - localname = localname[os.statvfs(path).f_namemax * -1:] - # Empty filename not allowed - if localname == '': - localname = 'blank' - localname = os.path.realpath(os.path.join(path, localname)) - if clobber: - return localname - # No Clobber mode, check to see if file exists - suffix = '' - i = 0 - while os.path.exists(localname + suffix): - i += 1 - suffix = "_%02d" % i - return localname + suffix diff --git a/lib/dnsdecoder.py b/lib/dnsdecoder.py deleted file mode 100644 index 64a4c7c..0000000 --- a/lib/dnsdecoder.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python - -import dshell -import util -import dpkt - - -class DNSDecoder(dshell.TCPDecoder): - - '''extend DNSDecoder to handle DNS request/responses - pairs request and response(s) by connection and query ID - to allow for detection of DNS spoofing, etc.. (multiple responses to request with same ID) - will call DNSHandler( - conn=Connection(), - request=dpkt.dns.DNS, - response=dpkt.dns.DNS, - requesttime=timestamp, responsetime=timestamp, - responsecount=responsecount - ) - after each response. - - config: noanswer: if True and discarding w/o response, will call with response,responsetime=None,None (True) - - ''' - - def __init__(self, **kwargs): - self.noanswer = True - dshell.TCPDecoder.__init__(self, **kwargs) # DNS is over UDP and TCP! - self.requests = {} - self.maxblobs = None - - def packetHandler(self,udp,data): - '''for each UDP packet , examine each segment (UDP packet) seperately as each will be a DNS Q/A - pair Q/A by ID and return as pairs''' - addr=udp.addr - if addr[0][1] < addr[1][1]: addr=addr[1],addr[0] #swap ports if source port is lower, to keep tuple (client,server) - connrqs = self.requests.setdefault(addr, {}) - try: - dns = dpkt.dns.DNS(data) - except Exception, e: - self._exc(e) - if dns.qr == dpkt.dns.DNS_Q: - connrqs[dns.id] = [udp.ts, dns, 0] - elif dns.qr == dpkt.dns.DNS_A: - rq = connrqs.get(dns.id, [None, None, 0]) - rq[2] += 1 - if "DNSHandler" in dir(self): - self.DNSHandler(conn=udp, request=rq[1], response=dns, requesttime=rq[0], - responsetime=udp.ts, responsecount=rq[2]) - - def blobHandler(self, conn, blob): - '''for each blob, examine each segment (UDP packet) seperately as each will be a DNS Q/A - pair Q/A by ID and return as pairs''' - connrqs = self.requests.setdefault(conn, {}) - # iterate blob as each packet will be a seperate request (catches spoofing) - for data in blob: - try: - dns = dpkt.dns.DNS(data) - except Exception, e: - self._exc(e) - continue - if dns.qr == dpkt.dns.DNS_Q: - connrqs[dns.id] = [blob.starttime, dns, 0] - elif dns.qr == dpkt.dns.DNS_A: - rq = connrqs.get(dns.id, [None, None, 0]) - rq[2] += 1 - if "DNSHandler" in dir(self): - self.DNSHandler(conn=conn, request=rq[1], response=dns, requesttime=rq[0], - responsetime=blob.starttime, responsecount=rq[2]) - - def connectionHandler(self,conn): - '''clean up unanswered requests when we discard the connection''' - if self.noanswer and "DNSHandler" in dir(self) and self.requests.get(conn): - for requesttime, request, responsecount in self.requests[conn].values(): - if not responsecount: - if type(conn) is tuple: conn=dshell.Packet(self,conn) #wrap UDP addresses - self.DNSHandler(conn=conn, request=request, response=None, - requesttime=requesttime, responsetime=None, responsecount=responsecount) - if conn in self.requests: - del self.requests[conn] - - def postModule(self): - '''flush out all remaining request state when module exits''' - for conn in self.requests.keys(): - self.connectionHandler(conn) - - -class displaystub(dshell.Decoder): - - def __init__(self): - dshell.Decoder.__init__(self, - name='dnsdecoder', - description='Intermediate class to support DNS based decoders.', - longdescription="See source code or pydoc for details on use." - ) - -if __name__ == '__main__': - dObj = displaystub() - print dObj -else: # do we always want to print something here? Maybe only in debug mode?: - dObj = displaystub() diff --git a/lib/dshell.py b/lib/dshell.py deleted file mode 100755 index a2e461e..0000000 --- a/lib/dshell.py +++ /dev/null @@ -1,1113 +0,0 @@ -""" -Dshell base classes -""" - -__version__ = "3.0" - -import dpkt -import struct -import socket -import traceback -import util -import os -import logging - -# For IP lookups -try: - import geoip2.database -except: - pass - - -class Decoder(object): - - """ - Base class that all decoders will inherit - - The Dshell class initializes the decoder to work in the framework - and provides common functions such as CC/ASN lookup - - Configuration attributes, settable by Dshell.__init__(attr=value,...) or in subclass __init__: - name: name of this decoder. - description: single-line description of this decoder - longdescription: multi-line description of this decoder - author: who to blame for this decoder - - filter: default BPF filter for capture. - - format: output format string for this decoder, overrides default for - Please read how text, DB, etc.. Output() classes parse a format string. - - optionsdict: optionParser compatible config, specific to decoder - dict of { 'optname':{'default':..., 'help':..., etc... - }, - 'optname':... } - 'optname' is set by --deodername_optname=... on command line - and under [decodername] section in config file - - - cleanupinterval - seconds with no activity before state is discarded (default 60) - - chainable - set True to indicate this decoder can be chained (can pass output to another decoder) - subDecoder - decoder to pass output to, if not None. - (create new Data objects and call subDecoder.XHandler from XHandler, etc..) - - - """ - - def __super__(self): - '''convenience function to get bound instance of superclass''' - return super(self.__class__, self) - - def __init__(self, **kwargs): - self.name = 'unnamed' - self.description = '' - self.longdescription = '' - self.filter = '' - self.author = 'xx' - self.decodedbytes = 0 - self.count = 0 - '''dict of options specific to this decoder in format - 'optname':{configdict} translates to --decodername_optname''' - self.optiondict = {} - - # out holds the output plugin. If None, will inherit the global output - self.out = None - # format is the format string for this plugin, if None, uses global - self.format = None - - # capture options - self.l2decoder = dpkt.ethernet.Ethernet # decoder to use if raw mode - # strip extra layers before IP/IPv6? (such as PPPoE, IP-over-IP, etc..) - self.striplayers = 0 - - self._DEBUG = False - - # can we chain a decoder off the output of this one? - self.chainable = False - self.subDecoder = None # decoder to pass output to for chaining - - # set flags to indicate if handlers are present - if 'packetHandler' in dir(self): - self.isPacketHandlerPresent = True - else: - self.isPacketHandlerPresent = False - if 'connectionHandler' in dir(self): - self.isConnectionHandlerPresent = True - else: - self.isConnectionHandlerPresent = False - if 'blobHandler' in dir(self): - self.isBlobHandlerPresent = True - else: - self.isBlobHandlerPresent = False - - # for connection tracking, if applicable - self.connectionsDict = {} - self.cleanupts = 0 - - # instantiate and save references to lookup function - geoip_dir = os.path.join(os.environ['DATAPATH'], "GeoIP") - try: - self.geoccdb = geoip2.database.Reader( - os.path.join(geoip_dir, "GeoLite2-Country.mmdb") - ).country - except: - self.geoccdb = None - - try: - self.geoasndb = geoip2.database.Reader( - os.path.join(geoip_dir, "GeoLite2-ASN.mmdb") - ).asn - except: - self.geoasndb = None - - # import kw args into class members - if kwargs: - self.__dict__.update(kwargs) - - ### convenience functions for alert output and logging ### - - def alert(self, *args, **kw): - '''sends alert to output handler - typically self.alert will be called with the decoded data and the packet/connection info dict last, as follows: - - self.alert(alert_arg,alert_arg2...,alert_data=value,alert_data2=value2....,**conn/pkt.info()) - - example: self.alert(decoded_data,conn.info(),blob.info()) [blob info overrides conn info] - - this will pass all information about the decoder, the connection, and the specific event up to the output module - - if a positional arg is a dict, it updates the kwargs - if an arg is a list, it extends the arg list - else it is appended to the arg list - - all arguments are optional, at the very least you want to pass the **pkt/conn.info() so all traffic info is available. - - output modules handle this data as follows: - - the name of the alerting decoder is available in the "decoder" field - - all non-keyword arguments will be concatenated into the "data" field - - keyword arguments, including all provided by .info() will be used to populate matching fields - - remaining keyword arguments that do not match fields will be represented by "key=value" strings - concatenated together into the "extra" field - ''' - oargs = [] - for a in args: - # merge dict args, overriding kws - if type(a) == dict: - kw.update(a) - elif type(a) == list: - oargs.extend(a) - else: - oargs.append(a) - if 'decoder' not in kw: - kw['decoder'] = self.name - self.out.alert(*oargs, **kw) # add decoder name - - def write(self, obj, **kw): - '''write session data''' - self.out.write(obj, **kw) - - def dump(self, *args, **kw): - '''write packet data (probably to the PCAP writer if present)''' - if len(args) == 3: - kw['len'], kw['pkt'], kw['ts'] = args - elif len(args) == 2: - kw['pkt'], kw['ts'] = args - elif len(args) == 1: - kw['pkt'] = args[0] - self.out.dump(**kw) - - def log(self, msg, level=logging.INFO): - '''logs msg at specified level (default of INFO is for -v/--verbose output)''' - self.out.log( - msg, level=level) # default level is INFO (verbose) can be overridden - - def debug(self, msg): - '''logs msg at debug level''' - self.log(msg, level=logging.DEBUG) - - def warn(self, msg): - '''logs msg at warning level''' - self.log(msg, level=logging.WARN) - pass - - def error(self, msg): - '''logs msg at error level''' - self.log(msg, level=logging.ERROR) - - def __repr__(self): - return '%s %s %s' % (self.name, self.filter, - ' '.join([('%s=%s' % (x, str(self.__dict__.get(x)))) for x in self.optiondict.keys()])) - - def preModule(self): - '''preModule is called before capture starts - default preModule, dumps object data to debug''' - if self.subDecoder: - self.subDecoder.preModule() - self.debug(self.name + ' ' + str(self.__dict__)) - - def postModule(self): - '''postModule is called after capture stops - default postModule, prints basic decoding stats''' - self.cleanConnectionStore() - self.log("%s: %d packets (%d bytes) decoded" % - (self.name, self.count, self.decodedbytes)) - if self.subDecoder: - self.subDecoder.postModule() - - def preFile(self): - if self.subDecoder: - self.subDecoder.preFile() - - def postFile(self): - if self.subDecoder: - self.subDecoder.postFile() - - def parseOptions(self, options={}): - '''option keys:values will set class members (self.key=value) - if key is in optiondict''' - for optname in self.optiondict.iterkeys(): - if optname in options: - self.__dict__[optname] = options[optname] - - def parseArgs(self, args, options={}): - '''called to parse command-line arguments and cli/config file options - if options dict contains 'all' or the decoder name as a key - class members will be updated from value''' - # get positional args after the -- - self.args = args - # update from all decoders section of config file - if 'all' in options: - self.parseOptions(options['all']) - # update from named section of config file - if self.name in options: - self.parseOptions(options[self.name]) - - def getGeoIP(self, ip, db=None, notfound='--'): - """ - Get country code associated with an IP. - Requires GeoIP library (geoip2) and data files. - """ - if not db: - db = self.geoccdb - try: - # Get country code based on order of importance - # 1st: Country that owns an IP address registered in another - # location (e.g. military bases in foreign countries) - # 2nd: Country in which the IP address is registered - # 3rd: Physical country where IP address is located - # https://dev.maxmind.com/geoip/geoip2/whats-new-in-geoip2/#Country_Registered_Country_and_Represented_Country - location = db(ip) - country = ( - location.represented_country.iso_code or - location.registered_country.iso_code or - location.country.iso_code or - notfound - ) - return country - except Exception: - # Many expected exceptions can occur here. Ignore them all and - # return default value. - return notfound - - def getASN(self, ip, db=None, notfound='--'): - """ - Get ASN associated with an IP. - Requires GeoIP library (geoip2) and data files. - """ - if not db: - db = self.geoasndb - try: - template = "AS{0.autonomous_system_number} {0.autonomous_system_organization}" - asn = template.format( db(ip) ) - return asn - except Exception: - # Many expected exceptions can occur here. Ignore them all and - # return default value. - return notfound - - def close(self, conn, ts=None): - '''for connection based decoders - close and discard the connection object''' - # just return if we have already been called on this connection - # prevents infinite loop of a handler calling close() when we call it - if conn.state == 'closed': - return - - # set state to closed - conn.state = 'closed' - if ts: - conn.endtime = ts - # we have already stopped this so don't call the handlers if we have - # already stopped - if not conn.stop: - # flush out the last blob to the blob handler - if self.isBlobHandlerPresent and conn.blobs: - self.blobHandler(conn, conn.blobs[-1]) - # process connection handler - if self.isConnectionHandlerPresent: - self.connectionHandler(conn) - # connection close handler - # will be called regardless of conn.stop right before conn object is - # destroyed - if 'connectionCloseHandler' in dir(self): - self.connectionCloseHandler(conn) - # discard but check first in case a handler deleted it - if conn.addr in self.connectionsDict: - del self.connectionsDict[conn.addr] - - def stop(self, conn): - '''stop following connection - handlers will not be called, except for connectionCloseHandler''' - conn.stop = True - - def cleanup(self, ts): - '''if cleanup interval expired, close connections not updated in last interval''' - ts = util.mktime(ts) - # if cleanup interval has passed - if self.cleanupts < (ts - self.cleanupinterval): - for conn in self.connectionsDict.values(): - if util.mktime(conn.endtime) <= self.cleanupts: - self.close(conn) - self.cleanupts = ts - - def cleanConnectionStore(self): - '''cleans connection store of all information, flushing out data''' - for conn in self.connectionsDict.values(): - self.close(conn) - - def _exc(self, e): - '''exception handler''' - self.warn(str(e)) - if self._DEBUG: - traceback.print_exc() - - def find(self, addr, state=None): - if addr in self.connectionsDict: - conn = self.connectionsDict[addr] - elif (addr[1], addr[0]) in self.connectionsDict: - conn = self.connectionsDict[(addr[1], addr[0])] - else: - return None - if not state or conn.state == state: - return conn - else: - return None - - def track(self, addr, data=None, ts=None, offset=None, **kwargs): - '''connection tracking for TCP and UDP - finds or creates connection based on addr - updates connection with data if provided (using offset to reorder) - tracks timestamps if ts provided - extra args get passed to new connection objects - ''' - conn = self.find(addr) - # look for forward and reverse address tuples - if not conn: # create new connection - # if swapping and source has low port, swap source/dest so dest has - # low port - if self.swaplowport and addr[0][1] < addr[1][1]: - addr = (addr[1], addr[0]) - # create connection and call init handler - conn = Connection(self, addr=addr, ts=ts, **kwargs) - if 'connectionInitHandler' in dir(self): - self.connectionInitHandler(conn) - # save in state dict - self.connectionsDict[addr] = conn - - # has tracking been stopped? - if conn.stop: - return False - - if data: - # forward or reverse direction? - if addr == conn.addr: - direction = 'cs' - else: - direction = 'sc' - - original_direction = conn.direction - - # update the connection to update current blob or start a new one - # and return the last one - # we will get a blob back if there is data to flush - blob = conn.update(ts, direction, data, offset=offset) - if blob and self.isBlobHandlerPresent: - self.blobHandler(conn, blob) - - # check direction and blob count. - # If we have switched direction but already have max blobs - # close connection and replace it with a new one - if self.maxblobs and (direction != original_direction) and (len(conn.blobs) >= self.maxblobs): - self.close(conn) # close and call handlers - # recurse to create a new connection for the next - # request/response - return self.track(addr, ts=ts, **kwargs) - - # we can discard all but the last blob - if not self.isConnectionHandlerPresent: - while len(conn.blobs) > 1: - conn.blobs.pop(0) - - self.cleanup(ts) # do stale state cleanup - return conn # return a reference to the connection - - '''directly extend Decoder and set raw=True to capture raw PCAP data''' - - # we get the raw output from pcapy as header, data - def decode(self, *args, **kw): - if len(args) is 3: - pktlen, pktdata, ts = args # orig_len,packet,ts format (pylibpcap) - else: # ts,pktdata (pypcap) - ts, pktdata = args - pktlen = len(pktdata) - try: - if pktlen != len(pktdata): - raise Exception('packet truncated', pktlen, pktdata) - # decode with the L2 decoder (probably Ether) - pkt = self.l2decoder(pktdata) - # attempt to collect MAC addresses - if type(pkt) == dpkt.ethernet.Ethernet: - try: - smac = "%02x:%02x:%02x:%02x:%02x:%02x" % (struct.unpack("BBBBBB", pkt.src)) - dmac = "%02x:%02x:%02x:%02x:%02x:%02x" % (struct.unpack("BBBBBB", pkt.dst)) - except struct.error: # couldn't get MAC address - smac, dmac = None, None - kw.update(smac=smac, dmac=dmac) - elif type(pkt) == dpkt.sll.SLL: - try: - # Sometimes MAC address will show up as 00:00:00:00:00:00 - # TODO decide if it should be set to None or kept as-is - smac = "%02x:%02x:%02x:%02x:%02x:%02x" % (struct.unpack("BBBBBB", pkt.hdr[:pkt.hlen])) - dmac = None - except struct.error: - smac, dmac = None, None - kw.update(smac=smac, dmac=dmac) - # strip any intermediate layers (PPPoE, etc) - for _ in xrange(int(self.striplayers)): - pkt = pkt.data - # will call self.rawHandler(len,pkt,ts) - # (hdr,data) is the PCAP header and raw packet data - if 'rawHandler' in dir(self): - self.rawHandler(pktlen, pkt, ts, **kw) - else: - pass - except Exception, e: - self._exc(e) - -# IP handler - - -class IPDecoder(Decoder): - - '''extend IP6Decoder to capture IPv4 and IPv6 data - (but does basic IPv4 defragmentation) - config: - - l2decoder: dpkt class for layer-2 decoding (Ethernet) - striplayers: strip n layers above layer-2, removes PPP/PPPoE encap, IP-over-IP, etc.. (0) - defrag: defragment IPv4 (True) - v6only: if True, will ignore IPv4 data. (False) - decode6to4: if True, will decode IPv6-over-IP, if False will treat as IP (True) - - filterfn: lambda function that accepts addr 2x2tuples and returns if packet should pass (addr:True) - - filterfn is required for IPv6 as port-based BPF filters don't work, - so keep your BPF to 'tcp' or 'udp' and set something like - self.filterfn = lambda ((sip,sp),(dip,dp)): (sp==53 or dp==53) ''' - - IP_PROTO_MAP = { - dpkt.ip.IP_PROTO_ICMP: 'ICMP', - dpkt.ip.IP_PROTO_ICMP6: 'ICMP6', - dpkt.ip.IP_PROTO_TCP: 'TCP', - dpkt.ip.IP_PROTO_UDP: 'UDP', - dpkt.ip.IP_PROTO_IP6: 'IP6', - dpkt.ip.IP_PROTO_IP: 'IP'} - - def __init__(self, **kwargs): - self.v6only = False - self.decode6to4 = True - self.defrag = True - self.striplayers = 0 - self.l2decoder = dpkt.ethernet.Ethernet - self.filterfn = lambda addr: True - Decoder.__init__(self, **kwargs) - self.frags = {} - - def ipdefrag(self, pkt): - '''ip fragment reassembly''' - # if pkt.off&dpkt.ip.IP_DF or pkt.off==0: return pkt #DF or !MF and - # offset 0 - # if we need to create a store for this IP addr/id - f = self.frags.setdefault((pkt.src, pkt.dst, pkt.id), {}) - f[pkt.off & dpkt.ip.IP_OFFMASK] = pkt - offset = 0 - data = '' - while True: - if offset not in f: - return None # we don't have this offset, can't reassemble yet - data += str(pkt.data) # add this to the data - if not pkt.off & dpkt.ip.IP_MF: - break # this is the next packet in order and no more fragments - offset = len(data) / 8 # calculate the next fragment's offset - # we hit no MF and last offset, so return a defragged packet - del self.frags[(pkt.src, pkt.dst, pkt.id)] # discard store - pkt.data = data # replace payload with defragged data - pkt.off = 0 # no frags, offset 0 - pkt.sum = 0 # recompute checksum - # dump and redecode packet to get checksum right - return dpkt.ip.IP(str(pkt)) - - def rawHandler(self, pktlen, pkt, ts, **kwargs): - '''takes ethernet data and determines if it contains IP or IP6. - defragments IPv4 - if 6to4, unencaps the IPv6 - If IP/IP6, hands off to IPDecoder via IPHandler()''' - try: - # if this is an IPv4 packet, defragment, decode and hand it off - if type(pkt.data) == dpkt.ip.IP: - if self.defrag: - # return packet if whole, None if more frags needed - pkt = self.ipdefrag(pkt.data) - else: - pkt = pkt.data # get the layer 3 packet - if pkt: # do we have a whole IP packet? - if self.decode6to4 and pkt.p == dpkt.ip.IP_PROTO_IP6: - pass # fall thru to ip6 decode - elif not self.v6only: # if we are decoding ip4 - sip, dip = socket.inet_ntoa( - pkt.src), socket.inet_ntoa(pkt.dst) - # try to decode ports - try: - sport, dport = pkt.data.sport, pkt.data.dport - except: # no ports in this layer-4 protocol - sport, dport = None, None - # generate int forms of src/dest ips - sipint, dipint = struct.unpack( - '!L', pkt.src)[0], struct.unpack('!L', pkt.dst)[0] - # call IPHandler with extra data - self.IPHandler(((sip, sport), (dip, dport)), pkt, ts, - pkttype=dpkt.ethernet.ETH_TYPE_IP, - proto=self.IP_PROTO_MAP.get( - pkt.p, pkt.p), - sipint=sipint, dipint=dipint, - **kwargs) - if pkt and type(pkt.data) == dpkt.ip6.IP6: - pkt = pkt.data # no defrag of ipv6 - # decode ipv6 addresses - sip, dip = socket.inet_ntop(socket.AF_INET6, pkt.src), socket.inet_ntop( - socket.AF_INET6, pkt.dst) - # try to get layer-4 ports - try: - sport, dport = pkt.data.sport, pkt.data.dport - except: - sport, dport = None, None - # generate int forms of src/dest ips - h, l = struct.unpack("!QQ", pkt.src) - sipint = ( (h << 64) | l ) - h, l = struct.unpack("!QQ", pkt.dst) - dipint = ( (h << 64) | l ) - # call ipv6 handler - self.IPHandler(((sip, sport), (dip, dport)), pkt, ts, - pkttype=dpkt.ethernet.ETH_TYPE_IP6, - proto=self.IP_PROTO_MAP.get(pkt.nxt, pkt.nxt), - sipint=sipint, dipint=dipint, - **kwargs) - except Exception, e: - self._exc(e) - - def IPHandler(self, addr, pkt, ts, **kwargs): - '''called if packet is IPv4/IPv6 - check packets using filterfn here''' - self.decodedbytes += len(str(pkt)) - self.count += 1 - if self.isPacketHandlerPresent and self.filterfn(addr): - return self.packetHandler(ip=Packet(self, addr, pkt=str(pkt), ts=ts, **kwargs)) - - -class IP6Decoder(IPDecoder): - pass - - -class UDPDecoder(IPDecoder): - - '''extend UDPDecoder to decode UDP optionally track state - config if tracking state with connectionHandler or blobHandler - maxblobs - if tracking state, max blobs to track before flushing - swaplowport - when establishing state, swap source/dest so dest has low port - cleanupinterval - seconds with no activity before state is discarded (default 60) ''' - - def __init__(self, **kwargs): - # by default limit UDP 'connections' to a single request and response - self.maxblobs = 2 - # can we swap source/dest so dest always has low port? - self.swaplowport = True - self.cleanupinterval = 60 - IPDecoder.__init__(self, **kwargs) - - def UDP(self, addr, data, pkt, ts=None, **kwargs): - ''' will call self.packetHandler(udp=Packet(),data=data) - (see Packet() for Packet object common attributes) - udp.pkt will contain the raw IP data - data will contain the decoded UDP payload - - State tracking: - only if connectionHandler or blobHandler is present - and packetHandler is not present - - UDPDecoder will call: - self.connectionInitHandler(conn=Connection()) - when UDP state is established - (see Connection() for Connection object attributes) - - self.blobHandler(conn=Connection(),blob=Blob()) - when stream direction switches (if following stream) - blob=(see Blob() objects) - - self.connectionHandler(conn=Connection()) - when UDP state is flushed (if following stream) - state is flushed when stale or when maxblobs is exceeded - if maxblobs exceeded, current data will go into new connection - - self.connectionCloseHandler(conn=Connection()) - when state is discarded (always) - ''' - self.decodedbytes += len(data) - self.count += 1 - try: - if self.isPacketHandlerPresent: - # create a Packet object and populate it - return self.packetHandler(udp=Packet(self, addr, pkt=pkt, ts=ts, **kwargs), data=data) - - # if no PacketHandler, we need to track state - conn = self.find(addr) - if not conn: - conn = self.track(addr, ts=ts, state='init', **kwargs) - if conn.nextoffset['cs'] is None: - conn.nextoffset['cs'] = 0 - if conn.nextoffset['sc'] is None: - conn.nextoffset['sc'] = 0 - self.track(addr, data, ts, **kwargs) - - except Exception, e: - self._exc(e) - - def IPHandler(self, addr, pkt, ts, **kwargs): - '''IPv4 dispatch, hands address, UDP payload and packet up to UDP callback''' - if self.filterfn(addr): - if type(pkt.data) == dpkt.udp.UDP: - return self.UDP(addr, str(pkt.data.data), str(pkt), ts, **kwargs) - - -class UDP6Decoder(UDPDecoder): - pass - - -class TCPDecoder(UDPDecoder): - - '''IPv6 TCP/UDP decoder - reassembles TCP and UDP streams - For TCP and UDP (if no packetHandler) - self.connectionInitHandler(conn=Connection()) - when TCP connection is established - (see Connection() for Connection object attributes) - - self.blobHandler(conn=Connection(),blob=Blob()) - when stream direction switches (if following stream) - blob=(see Blob() objects) - - self.connectionHandler(conn=Connection()) - when connection closes (if following stream) - - self.connectionCloseHandler(conn=Connection()) - when connection closes (always) - - For UDP only: - self.packetHandler(udp=Packet(),data=data) - with every packet - data=decoded UDP data - - if packetHandler is present, it will be called only for UDP (and UDP will not be tracked)''' - - def __init__(self, **kwargs): - self.maxblobs = None # no limit on connections - # can we swap source/dest so dest always has low port? - self.swaplowport = False - # if set true, will requre TCP handshake to track connection - self.ignore_handshake = False - self.cleanupinterval = 300 - # up two levels to IPDecoder - IPDecoder.__init__(self, **kwargs) - self.optiondict['ignore_handshake'] = { - 'action': 'store_true', 'help': 'ignore TCP handshake'} - - def IPHandler(self, addr, pkt, ts, **kwargs): - '''IPv4 dispatch''' - if self.filterfn(addr): - if type(pkt.data) == dpkt.udp.UDP: - return self.UDP(addr, str(pkt.data.data), str(pkt), ts, **kwargs) - elif type(pkt.data) == dpkt.tcp.TCP: - return self.TCP(addr, pkt.data, ts, **kwargs) - - def TCP(self, addr, tcp, ts, **kwargs): - '''TCP dispatch''' - self.decodedbytes += len(str(tcp)) - self.count += 1 - - try: - # attempt to find an existing connection for this address - conn = self.find(addr) - - if self.ignore_handshake: - # if we are ignoring handshakes, we will track all connections, - # even if we did not see the initialization handshake. - if not conn: - conn = self.track(addr, ts=ts, state='init', **kwargs) - # align the sequence numbers when we first see a connection - if conn.nextoffset['cs'] is None and addr == conn.addr: - conn.nextoffset['cs'] = tcp.seq + 1 - elif conn.nextoffset['sc'] is None and addr != conn.addr: - conn.nextoffset['sc'] = tcp.seq + 1 - self.track(addr, str(tcp.data), ts, - state='established', offset=tcp.seq, **kwargs) - - else: - # otherwise, only track connections if we see a TCP handshake - if (tcp.flags == dpkt.tcp.TH_SYN - or tcp.flags == dpkt.tcp.TH_SYN | dpkt.tcp.TH_CWR | dpkt.tcp.TH_ECE): - # SYN - if conn: - # if a connection already exists for the addr, - # close the old one to start fresh - self.close(conn, ts) - conn = self.track(addr, ts=ts, state='init', **kwargs) - if conn: - conn.nextoffset['cs'] = tcp.seq + 1 - elif (tcp.flags == dpkt.tcp.TH_SYN | dpkt.tcp.TH_ACK - or tcp.flags == dpkt.tcp.TH_SYN | dpkt.tcp.TH_ACK | dpkt.tcp.TH_ECE): - # SYN ACK - if conn and tcp.ack == conn.nextoffset['cs']: - conn.nextoffset['sc'] = tcp.seq + 1 - conn.state = 'established' - if conn and conn.state == 'established': - self.track(addr, str(tcp.data), ts, - state='established', offset=tcp.seq, **kwargs) - - # close connection - if conn and tcp.flags & (dpkt.tcp.TH_FIN | dpkt.tcp.TH_RST): - # flag that an IP is closing a connection with FIN or RST - conn.closeIP(addr[0]) - if conn and conn.connectionClosed(): - self.close(conn, ts) - - except Exception, e: - self._exc(e) - - -class TCP6Decoder(TCPDecoder): - pass - - -class Data(object): - - '''base class for data objects (packets,connections, etc..) - these objects hold data (appendable array, typically of strings) - and info members (updateable/accessible as members or as dict via info()) - typically one will extend the Data class and replace the data member - and associated functions (update,iter,str,repr) with a data() function - and functions to manipulate the data''' - - def __init__(self, *args, **kwargs): - self.info_keys = [] - # update with list data - self.data = list(args) - # update with keyword data - self.info(**kwargs) - - def info(self, *args, **kwargs): - '''update/return info stored in this object - data can be passwd as dict(s) or keyword args''' - args = list(args) + [kwargs] - for a in args: - for k, v in a.iteritems(): - if k not in self.info_keys: - self.info_keys.append(k) - self.__dict__[k] = v - return dict((k, self.__dict__[k]) for k in self.info_keys) - - def unpack(self, fmt, data, *args): - '''unpacks data using fmt to keys listed in args''' - self.info(dict(zip(args, struct.unpack(fmt, data)))) - - def pack(self, fmt, *args): - '''packs info keys in args using fmt''' - return struct.pack(fmt, *[self.__dict__[k] for k in args]) - - def update(self, *args, **kwargs): - '''updates data (and optionally keyword args)''' - self.data.extend(args) - self.info(kwargs) - - def __iter__(self): - '''returns each data element in order added''' - for data in self.data: - yield data - - def __str__(self): - '''return string built from data''' - return ''.join(self.data) - - def __repr__(self): - return ' '.join(['%s=%s' % (k, v) for k, v in self.info().iteritems()]) - - def __getitem__(self, k): return self.__dict__[k] - - def __setitem__(self, k, v): self.__dict__[k] = v - - -class Packet(Data): - - '''metadata class for connectionless data - Members: - sip, sport, dip, dport : source ip and port, dest ip and port - addr : ((sip,sport),(dip,dport)) tuple. sport/dport will be None if N/A - sipcc, dipcc, sipasn, dipasn : country codes and ASNs for source and dest IPs - ts : datetime.datetime() UTC timestamp of packet. use util.mktime(ts) to get POSIX timestamp - pkt : raw packet data - any additional args will be added to info dict - ''' - - def __init__(self, decoder, addr, ts=None, pkt=None, **kwargs): - self.info_keys = ['addr', 'sip', 'dip', 'sport', 'dport', 'ts'] - self.addr = addr - # do not define pkt unless passed in - self.ts = ts - ((self.sip, self.sport), (self.dip, self.dport)) = self.addr - if pkt: - self.pkt = pkt - self.info(bytes=len(self.pkt)) - - # pass instantiating decoder's cc/asn lookup objects to keep global - # cache - try: - self.info(sipcc=decoder.getGeoIP(self.sip, db=decoder.geoccdb), - sipasn=decoder.getASN(self.sip, db=decoder.geoasndb), - dipcc=decoder.getGeoIP(self.dip, db=decoder.geoccdb), - dipasn=decoder.getASN(self.dip, db=decoder.geoasndb)) - except: - self.sipcc, self.sipasn, self.dipcc, self.dipasn = None, None, None, None - - # update with additional info - self.info(**kwargs) - - def __iter__(self): - for p in self.pkt: - yield ord(p) - - def __str__(self): - return self.pkt - - def __repr__(self): - return "%(ts)s %(sip)16s :%(sport)-5s -> %(dip)5s :%(dport)-5s (%(sipcc)s -> %(dipcc)s)\n" % self.info() - - -class Connection(Packet): - - """ - Connection class is used for tracking all information - contained within an established TCP connection / UDP pseudoconnection - - Extends Packet() - - Additional members: - {client|server}ip, {client|server}port: aliases of sip,sport,dip,dport - {client|server}countrycode, {client|server}asn: aliases of sip/dip country codes and ASNs - clientpackets, serverpackets: counts of packets from client and server - clientbytes, serverbytes: total bytes from client and server - clientclosed, serverclosed: flag indicating if a direction has closed the connection - starttime,endtime: timestamps of start and end (or last packet) time of connection. - direction: indicates direction of last traffic: - 'init' : established, no traffic - 'cs': client to server - 'sc': server to client - state: TCP state of this connection - blobs: array of reassembled half stream blobs - a new blob is started when the direction changes - stop: if True, stopped following stream - - """ - - MAX_OFFSET = 0xffffffff # max offset before wrap, default is MAXINT32 for TCP sequence numbers - - def __init__(self, decoder, addr, ts=None, **kwargs): - self.state = None - # the offset we expect for the next blob in this direction - self.nextoffset = {'cs': None, 'sc': None} - # init IP-level data - Packet.__init__(self, decoder, addr, ts=ts, **kwargs) - self.clientip, self.clientport, self.serverip, self.serverport = ( - self.sip, self.sport, self.dip, self.dport) - self.info_keys.extend( - ['clientip', 'serverip', 'clientport', 'serverport']) - self.clientcountrycode, self.clientasn, self.servercountrycode, self.serverasn = ( - self.sipcc, self.sipasn, self.dipcc, self.dipasn) - self.info_keys.extend( - ['clientcountrycode', 'servercountrycode', 'clientasn', 'serverasn']) - self.clientpackets = 0 # we have the first packet for each connection - self.serverpackets = 0 - self.clientbytes = 0 - self.serverbytes = 0 - self.clientclosed = False - self.serverclosed = False - self.starttime = self.ts # datetime Obj containing start time - self.endtime = self.ts - # first update will change this, creating first blob - self.direction = 'init' - self.info_keys.extend(['clientpackets', 'clientbytes', 'serverpackets', - 'serverbytes', 'starttime', 'endtime', 'state', 'direction']) - - # list of tuples of (direction,halfstream,startoffset,endoffset) - # indicating where each side talks - self.blobs = [] - self.stop = False - - def __repr__(self): - # starttime cip sip - return '%s %16s -> %16s (%s -> %s) %6s %6s %5d %5d %7d %7d %6ds %s' % ( - self.starttime, - self.clientip, - self.serverip, - self.clientcountrycode, - self.servercountrycode, - self.clientport, - self.serverport, - self.clientpackets, - self.serverpackets, - self.clientbytes, - self.serverbytes, - (util.mktime(self.endtime) - util.mktime(self.starttime)), - self.state) - - def connectionClosed(self): - return self.serverclosed and self.clientclosed - - def closeIP(self, tuple): - ''' - Track if we have seen a FIN packet from given tuple - tuple should be of form (ip, port) - ''' - if tuple == (self.clientip, self.clientport): - self.clientclosed = True - if tuple == (self.serverip, self.serverport): - self.serverclosed = True - - def update(self, ts, direction, data, offset=None): - # if we have no blobs or direction changes, start a new blob - lastblob = None - if len(self.blobs) > 1 and self.blobs[-2].startoffset <= offset < self.blobs[-2].endoffset: - self.blobs[-2].update(ts,data,offset=offset) - else: - if direction != self.direction: - self.direction = direction - # if we have a finished blob, return it - if self.blobs: - lastblob = self.blobs[-1] - # for tracking offsets across blobs (TCP) set the startoffset of this blob to what we know it should be - # this may not necessarily be the offset of THIS data if packets - # are out of order - self.blobs.append( - Blob(ts, direction, startoffset=self.nextoffset[direction])) - self.blobs[-1].update(ts, data, offset=offset) # update latest blob - if direction == 'cs': - self.clientpackets += 1 - self.clientbytes += len(data) - elif direction == 'sc': - self.serverpackets += 1 - self.serverbytes += len(data) - self.endtime = ts - # if we are tracking offsets, expect the next blob to be where this one - # ends so far - if offset != None and ((offset + len(data)) & self.MAX_OFFSET) >= self.nextoffset[direction]: - self.nextoffset[direction] = (offset + len(data)) & self.MAX_OFFSET - return lastblob - - # return one or both sides of the stream - def data(self, direction=None, errorHandler=None, padding=None, overlap=True, caller=None): - '''returns reassembled half-stream selected by direction 'sc' or 'cs' - if no direction, return all stream data interleaved - see Blob.data() for errorHandler docs''' - return ''.join([b.data(errorHandler=errorHandler, padding=padding, overlap=overlap, caller=caller) for b in self.blobs if (not direction or b.direction == direction)]) - - def __str__(self): - '''return all data interleaved''' - return self.data(padding='') - - def __iter__(self): - '''return each blob in capture order''' - for blob in self.blobs: - yield blob - - -class Blob(Data): - - '''a blob containins a contiguous part of the half-stream - Members: - starttime,endtime : start and end timestamps of this blob - direction : direction of this blob's data 'sc' or 'cs' - data(): this blob's data - startoffset,endoffset: offset of this blob start/end in bytes from start of stream - ''' - - # max offset before wrap, default is MAXINT32 for TCP sequence numbers - MAX_OFFSET = 0xffffffff - - def __init__(self, ts, direction, startoffset): - self.starttime = ts - self.endtime = ts - self.direction = direction - self.segments = {} # offset:[segments with offset] - self.startoffset = startoffset - self.endoffset = startoffset - self.info_keys = [ - 'starttime', 'endtime', 'direction', 'startoffset', 'endoffset'] - - def update(self, ts, data, offset=None): - # if offsets are not being provided, just keep packets in wire order - if offset == None: - offset = self.endoffset - # buffer each segment in a list, keyed by offset (captures retrans, - # etc) - self.segments.setdefault(offset, []).append(data) - if ts > self.endtime: - self.endtime = ts - # update the end offset if this packet goes at the end - if (offset + len(data)) & self.MAX_OFFSET >= self.endoffset: - self.endoffset = (offset + len(data)) & self.MAX_OFFSET - - def __repr__(self): - return '%s %s (%s) +%s %d' % (self.starttime, self.endtime, self.direction, self.startoffset, len(self.segments)) - - def __str__(self): - '''returns segments of blob as string''' - return self.data(padding='') - - def data(self, errorHandler=None, padding=None, overlap=True, caller=None): - '''returns segments of blob reassembled into a string - if next segment offset is not the expected offset - errorHandler(blob,expected,offset) will be called - blob is a reference to the blob - if expectedoffset, data is overlapping - else a KeyError will be raised. - if the exception is passed and data is missing - if padding != None it will be used to fill the gap - if segment overlaps existing data - new data is kept if overlap=True - existing data is kept if overlap=False - caller: a ref to the calling object, passed to errorhandler - dup: how to handle duplicate segments: - 0: use first segment seen - -1 (default): use last segment seen - changing duplicate segment handling to always take largest segment - ''' - d = '' - nextoffset = self.startoffset - for segoffset in sorted(self.segments.iterkeys()): - if segoffset != nextoffset: - if errorHandler: # errorhandler can mangle blob data - if not errorHandler(blob=self, expected=nextoffset, offset=segoffset, caller=caller): - continue # errorhandler determines pass or fail here - elif segoffset > nextoffset: - # data missing and padding specified - if padding is not None: - if len(padding): - # add padding to data - d += str(padding) * (segoffset - nextoffset) - else: - # data missing, and no padding - raise KeyError(nextoffset) - elif segoffset < nextoffset and not overlap: - continue # skip if not allowing overlap - #find most data in segments - seg = '' - for x in self.segments[segoffset]: - if len(x) > len(seg): - seg = x - # advance next expected offset - nextoffset = ( - segoffset + len(seg)) & self.MAX_OFFSET - # append or splice data - d = d[:segoffset - self.startoffset] + \ - seg + \ - d[nextoffset - self.startoffset:] - return d - - def __iter__(self): - '''return each segment data in offset order - for TCP this will return segments ordered but not reassembled - (gaps and overlaps may exist) - for UDP this will return datagrams payloads in capture order, - (very useful for RTP or other streaming protocol.) - ''' - for segoffset in sorted(self.segments.iterkeys()): - yield self.segments[segoffset][-1] diff --git a/lib/httpdecoder.py b/lib/httpdecoder.py deleted file mode 100644 index a661a65..0000000 --- a/lib/httpdecoder.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python -import dshell -import util -import dpkt - -# for HTTPDecoder gzip decompression -import gzip -import cStringIO - - -class HTTPDecoder(dshell.TCPDecoder): - - '''extend HTTPDecoder to handle HTTP request/responses - will call HTTPHandler( - conn=Connection(), - request=dpkt.http.Request, - response=dpkt.http.Response, - requesttime=timestamp, responsetime=timestamp - ) - after each response. - - config: noresponse: if True and connection closes w/o response, will call with response,responsetime=None,None (True) - gunzip: if True will decompress gzip encoded response bodies (default True) - - ''' - - def __init__(self, **kwargs): - self.noresponse = True - self.gunzip = True - dshell.TCPDecoder.__init__(self, **kwargs) - self.requests = {} - - # Custom error handler for data reassembly --- ignores errors, keep data - def errorH(self, **x): - return True - - def blobHandler(self, conn, blob): - '''buffer the request blob and call the handler once we have the response blob''' - if conn not in self.requests: - try: - self.requests[conn] = ( - blob.starttime, dpkt.http.Request(blob.data(self.errorH))) - except Exception, e: - self.UnpackError(e) - else: - try: - if 'HTTPHandler' in dir(self): - response = dpkt.http.Response(blob.data(self.errorH)) - if self.gunzip and 'gzip' in util.getHeader(response, 'content-encoding'): - bodyUnzip = self.decompressGzipContent(response.body) - if bodyUnzip != None: - response.body = bodyUnzip - self.HTTPHandler(conn=conn, - request=self.requests[conn][1], - response=response, - requesttime=self.requests[conn][0], - responsetime=blob.starttime) - del self.requests[conn] - except Exception, e: - self.UnpackError(e) - self.HTTPHandler(conn=conn, request=self.requests[conn][ - 1], response=None, requesttime=self.requests[conn][0], responsetime=blob.starttime) - del self.requests[conn] - - def connectionHandler(self, conn): - '''when the connection closes, flush out any request blobs that did not have a response''' - if conn in self.requests: - if self.noresponse and 'HTTPHandler' in dir(self): - self.HTTPHandler(conn=conn, - request=self.requests[conn][1], - response=None, - requesttime=self.requests[conn][0], - responsetime=self.requests[conn][0]) - del self.requests[conn] - - def decompressGzipContent(self, httpcontent): - '''utility function to decompress gzip compressed content''' - cstr = cStringIO.StringIO(httpcontent) - try: - return gzip.GzipFile(fileobj=cstr).read() - except: - return None - - def UnpackError(self, error): - self._exc(error) - - -class displaystub(dshell.Decoder): - - def __init__(self): - dshell.Decoder.__init__(self, - name='httpdecoder', - description='Intermediate class to support HTTP based decoders.', - longdescription="See source code or pydoc for details on use." - ) - -if __name__ == '__main__': - dObj = displaystub() - print dObj -else: - dObj = displaystub() diff --git a/lib/output/colorout.py b/lib/output/colorout.py deleted file mode 100644 index 34f7716..0000000 --- a/lib/output/colorout.py +++ /dev/null @@ -1,377 +0,0 @@ -''' -@author: amm -''' - -import output -import util -import dshell -import sys -import cgi -import string -import datetime - - -class ColorOutput(output.TextOutput): - - ''' - Color-coded Output module - use with --output=colorout - Output to STDOUT will use XTERM color tags, if possible. - Output to FILE will use HTML - - Decoders should call self.out.write() with string data and the following kwargs: - formatTag: H1 and H2 are currently implemented - direction: cs / sc - timestamp: specify unix timestamp for current object being written - time: (bool) to display timestamp - - Note Regarding Timestamps: - ------------------------- - Decoders should *always* specify timestamp information if available in their - calls to write. (If passing a full blob or connection, colorout will extract - this information from those objects.) In HTML output mode, the timestamps will - always be embedded in the HTML with a javascript option to show/hide them. - Initial display is govered by the boolean kwarg 'time' specified in calls - to write(). (Defaults to hidden unless a single 'true' value is passed.) - - Instantiation options - --------------------- - keyword title: specify HTML title - keyword force: specify force=true color output (e.g for piping to less -R) - keyword html: specify html=true for HTML output, even when writing to STDOUT - - HTML Generator Mode: - This mode makes the HTML generation/formatting available - as a utility to other code: - 1) Instantiate with keyword htmlgenerator=True: - colorout.ColorOutput(htmlgenerator=True, title="test") - 2) After one or more calls to write(), call close() - 3) Dump HTML with htmldump() - - ''' - # COLORMODEs: - # TEXT - Plain text. No color coding. (Default) - # TTY - TTY style color encoding. - # HTML - HTML/CSS output - _COLORMODE = 'TEXT' - - # Offsets for HEX Mode - _CS_offset = 0 - _SC_offset = 0 - _NODIR_offset = 0 - - # Custom error handler for data reassembly --- ignores all errors - def errorH(self, **x): - return True - - def __init__(self, *args, **kwargs): - - if 'format' in kwargs: - fmtstr = kwargs['format'] - del kwargs['format'] # don't let base class process this - else: - fmtstr = '' - - # Title - if 'title' not in dir(self): - if 'title' in kwargs: - self.title = kwargs['title'] - del kwargs['title'] # don't let base class process this - else: - self.title = 'Dshell' - - # Check for html generator mode - if 'htmlgenerator' not in dir(self): - if 'htmlgenerator' in kwargs: - self.htmlgenerator = kwargs['htmlgenerator'] - # don't let base class process this - del kwargs['htmlgenerator'] - else: - self.htmlgenerator = False - if self.htmlgenerator: - self.htmlbuffer = '' - - # Check for force color mode - if 'force' not in dir(self): - if 'force' in kwargs: - self.force = True - del kwargs['force'] # don't let base class process this - else: - self.force = False - - # Override HTML for stdout - if 'html' not in dir(self): - if 'html' in kwargs: - self.html = True - del kwargs['html'] - else: - self.html = False - - # Call parent init - output.TextOutput.__init__(self, format=fmtstr, **kwargs) - - - # In HTML mode, if we get any single call - # to write() with the time option set, we will set - # this master boolean value to True and display all timestamps - # on initial page load. Otherwise, timestamps will be hidden - # until toggled by the user - self._htmldisplaytimes = False - - def setup(self): - # Write Header - self.setColorMode() - - if self._COLORMODE == 'HTML': - self._htmlwrite(self._HTMLHeader(self.title)) - - - def setColorMode(self): - # Determine output mode - if self.fh == sys.stdout and not self.htmlgenerator and not self.html: - # STDOUT. Use Text Based Mode. - if sys.stdout.isatty() or self.force: - self._COLORMODE = 'TTY' - else: - self._COLORMODE = 'TEXT' - else: - # File. Use HTML. - self._COLORMODE = 'HTML' - - # Internal function to write HTML - # or buffer it in factory mode - def _htmlwrite(self, text): - if self.htmlgenerator: - self.htmlbuffer += text - else: - self.fh.write(text) - if self.nobuffer: - self.fh.flush() - - def htmldump(self): - ''' - For use in HTML Generator Mode: - In this mode, HTML generated by calls to write() is buffered. This - function returns the contents of and clears the buffer. - ''' - tmp = self.htmlbuffer - self.htmlbuffer = '' - return tmp - - def close(self): - # Footer - if self._COLORMODE == 'HTML': - self._htmlwrite(self._HTMLFooter()) - output.TextOutput.close(self) - - def _reset_offsets(self, value=0): - self._CS_offset = value - self._SC_offset = value - self._NODIR_offset = value - - def write(self, *args, **kw): - - # KW Options - if 'hex' in kw and kw['hex']: - self._hexmode = True - else: - self._hexmode = False - if 'time' in kw and kw['time']: - self._timemode = True - self._htmldisplaytimes = True - else: - self._timemode = False - - # KW formatTag - if 'formatTag' in kw: - formatTag = kw['formatTag'] - del kw['formatTag'] - else: - formatTag = '' - - # KW Direction - if 'direction' in kw: - kwdirection = kw['direction'] - else: - kwdirection = '' - - # KW Offset - if 'offset' in kw: - self._reset_offsets(kw['offset']) - - # KW Timestamp - if 'timestamp' in kw and kw['timestamp'] != None: - kwtimestamp = kw['timestamp'] - else: - kwtimestamp = None - - # KW Encoding (to specify character encoding) - if 'encoding' in kw and kw['encoding'] != None: - kwencoding = kw['encoding'] - else: - kwencoding = None - - # FormatTag (HTML) - if len(formatTag) and self._COLORMODE == 'HTML': - self._htmlwrite('<%s>' % formatTag) - - # Iterate *args - for a in args: - if type(a) == dshell.Blob: - self._write_blob(a, kwencoding) - elif type(a) == dshell.Connection: - self._reset_offsets() - for blob in a: - self._write_blob(blob, kwencoding) - else: - self._write_string(a, kwdirection, kwtimestamp, kwencoding) - - # Format Post Tag - if len(formatTag) and self._COLORMODE == 'HTML': - self._htmlwrite('' % formatTag) - - def _write_blob(self, blob, encoding=None): - self._write_string( - blob.data(errorHandler=self.errorH), blob.direction, blob.starttime, encoding) - - def _write_string(self, text, direction, timestamp, encoding=None): - - colorTag = '' - - # Print TimestampcolorTag - if self._COLORMODE == 'HTML' and timestamp != None: - self._htmlwrite('
\n%s UTC:
' % - datetime.datetime.utcfromtimestamp(timestamp)) - #if self._hexmode: self._htmlwrite("
") - elif self._COLORMODE == 'TTY' and self._timemode and timestamp != None: - self.fh.write('\x1b[36m%s UTC:\x1b[0m\n' % - datetime.datetime.utcfromtimestamp(timestamp)) - if self.nobuffer: - self.fh.flush() - - # Set Direction - if direction.lower() == 'cs': - if self._COLORMODE == 'HTML': - self._htmlwrite('') - elif self._COLORMODE == 'TTY': - colorTag = '\x1b[0;31m' - elif direction.lower() == 'sc': - if self._COLORMODE == 'HTML': - self._htmlwrite('') - elif self._COLORMODE == 'TTY': - colorTag = '\x1b[0;32m' - - # Hex Mode Data - if self._hexmode: - # Hex Output - dlen = len(text) - if direction.lower() == 'cs': - msgOffset = self._CS_offset - self._CS_offset += dlen - elif direction.lower() == 'sc': - msgOffset = self._SC_offset - self._SC_offset += dlen - else: - msgOffset = self._NODIR_offset - self._NODIR_offset += dlen - text = util.hexPlusAscii(str(text), 16, msgOffset) - if self._COLORMODE == 'HTML': - text = cgi.escape(text) - self._htmlwrite(text) - elif self._COLORMODE == 'TTY': - self._write_tty(text, colorTag) - else: - self.fh.write(text) - if self.nobuffer: - self.fh.flush() - - # Plain Text - else: - if type(text) == unicode: - text = util.printableUnicode(text).encode('utf-8') - elif encoding: - try: - utext = unicode(text, encoding) - text = util.printableUnicode(utext).encode('utf-8') - except: - text = util.printableText(str(text)) - else: - text = util.printableText(str(text)) - if self._COLORMODE == 'HTML': - text = cgi.escape(text) - self._htmlwrite(text) - elif self._COLORMODE == 'TTY': - self._write_tty(text, colorTag) - else: - self.fh.write(text) - - # Close direction - if self._COLORMODE == 'HTML' and direction != '': - self._htmlwrite("") - - def _write_tty(self, text, colorTag): - - # Write color escape sequences on every line (less -R requires this) - for line in text.splitlines(True): - _line = line.split('\n') - self.fh.write(colorTag + _line[0] + '\x1b[0m') - if len(_line) > 1: - self.fh.write('\n') - if self.nobuffer: - self.fh.flush() - - def _HTMLHeader(self, title="Dshell"): - - return """ - - - -""" + title + """ - - - -
-""" - - def _HTMLFooter(self): - if self._htmldisplaytimes: - display_timestamps = "\n" - else: - display_timestamps = '' - return display_timestamps + """ - -""" - -obj = ColorOutput diff --git a/lib/output/csvout.py b/lib/output/csvout.py deleted file mode 100644 index ebbdbc2..0000000 --- a/lib/output/csvout.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -@author: tparker -''' - -import output -import util - - -class CSVOutput(output.TextOutput): - - ''' - CSV Output module - use with --output=csvout,[,data,customfield[:type],...] (a list of field:types to append to end of default format) - add [,file=...[,mode=...]] to write to outfile (or use -w arg on cmdline) - add format=... to replace the default fields or use a format string - add delim= to change delimeter from comma - ''' - - _NULL = '' - - _DEFAULT_DELIM = ',' - - _DEFAULT_FIELDS = [('decoder', 's'), ('datetime', 's'), - ('sip', 's'), ('sport', 's'), ('dip', 's'), ('dport', 's')] - - def __init__(self, *args, **kwargs): - ''' - sets up an output module, be sure to call Output.__init__ first or last - args will have the name of the module as args[0], anything else after - ''' - # start with a set of default fields - self.fields = self._DEFAULT_FIELDS - - if 'format' in kwargs: - self.fields = [] - fmtstr = kwargs['format'] - del kwargs['format'] # don't let base class process this - else: - fmtstr = '' - - # set delimiter - if 'delim' in kwargs: - self.delim = kwargs['delim'] - if self.delim.lower() == 'tab': - self.delim = "\t" - else: - self.delim = self._DEFAULT_DELIM - - self.noheader = 'noheader' in kwargs - - # parse args as fields - if len(args): - for a in args: - try: - f, t = a.split(':') # split on field:type - except: - f, t = a, 's' # default to string type - self.fields.append((f, t)) - - # build format string to pass to textoutput - if fmtstr: - fmtstr += self.delim - fmtstr += self.delim.join(['%%(%s)%s' % (f, t) for f, t in self.fields]) - - # everything else is exactly like the text output module - output.TextOutput.__init__(self, format=fmtstr, **kwargs) - - - def setup(self): - # print header if not suppressed - if self.fh and not self.noheader: - self.fh.write('#' + self.delim.join([f[0] for f in self.fields]) + "\n") - -'''NOTE: output modules return obj=reference to the CLASS - instead of a dObj=instance so we can init with args''' -obj = CSVOutput diff --git a/lib/output/elasticout.py b/lib/output/elasticout.py deleted file mode 100644 index 1e36c0e..0000000 --- a/lib/output/elasticout.py +++ /dev/null @@ -1,219 +0,0 @@ -''' -Created on May 6, 2015 - -@author: amm -''' - -import sys -import output -import dshell -import dfile -import logging -import datetime -import elasticsearch - - -class elasticout(output.TextOutput): - ''' - ElasticSearch Output module - use with --output=elasticout - - e.g. decode -d web *pcap --output elasticout,host=192.168.10.10,index=dshell - - options - ------- - host: : of an Elasticsearch search node (REQUIRED) - index: Elasticsearch index (REQUIRED) - doc_type: Elasticsearch document type for indexed documents - geoip: If set to Y, output module won't discard geoip tags - notrim: If set to Y, do not trim any fields from the output - message: If set to Y, add the decoder output message (args[0]) - as a "message" field in elasticsearch - ''' - - ELASTIC_HOST_LIST = [] - ELASTIC_INDEX = None - _DOC_TYPE = None - - # Fields to format as timestamp string - _TIMESTAMP_FIELDS = ( - 'ts', 'starttime', 'endtime', 'request_time', 'response_time') - # Fields to delete (redundant or unnecessary) - _DELETE_FIELDS = ('addr', 'direction', 'clientport', 'serverport', - 'clientip', 'serverip', 'sipint', 'dipint', 'pkttype') - # Dshell geolocation fields - _GEO_FIELDS = ('servercountrycode', 'clientcountrycode', - 'sipcc', 'dipcc', 'clientasn', 'serverasn', 'dipasn', 'sipasn') - - def __init__(self, *args, **kwargs): - - # - # Specified host - prepend to any list hard coded into the module - # - if 'host' in kwargs: - self.ELASTIC_HOST_LIST.insert(0, kwargs['host']) - - # - # Instantiate Elasticsearch client - # - if len(self.ELASTIC_HOST_LIST): - self.es = elasticsearch.Elasticsearch(self.ELASTIC_HOST_LIST) - else: - self.es = elasticsearch.Elasticsearch() - - # - # Index - # - if 'index' in kwargs: - self.ELASTIC_INDEX = kwargs['index'] - - # - # Document Type - # - if 'doc_type' in kwargs: - self._DOC_TYPE = kwargs['doc_type'] - - # - # Handle boolean options - # - self.options = {} - for o in ('geoip', 'notrim', 'message'): - self.options[o] = False - if o in kwargs: - if kwargs[o].upper() in ('Y', 'T', '1', 'YES', 'ON', 'TRUE'): - self.options[o] = True - del kwargs[o] - - # - # Check for existence of preInsert function - # this function allows child classes to have one last access - # to the data -- as will be inserted to Elasticsearch -- before - # the actual insert - # - # Function should return boolean value - # True to proceed with insert - # False to skip - # - # - if 'preInsert' in dir(self): - self.hasPreInsert = True - else: - self.hasPreInsert = False - - # Call parent init - output.TextOutput.__init__(self, **kwargs) - - def alert(self, *args, **kw): - - # - # DocType - # - if self._DOC_TYPE: - doc_type = self._DOC_TYPE - elif 'decoder' in kw: - doc_type = kw['decoder'] - del kw['decoder'] - else: - doc_type = 'dshell' - - # - # Remove Common Redundant Fields - # - if not self.options['notrim']: - for name in self._DELETE_FIELDS: - if name in kw: - del kw[name] - - # - # Time Fields - # - # Rename 'ts' to 'starttime' if 'starttime' not present - if 'ts' in kw: - if 'starttime' not in kw: - kw['starttime'] = kw['ts'] - del kw['ts'] - - # - # Remove GEOIP Fields - # - if not self.options['geoip']: - for name in self._GEO_FIELDS: - if name in kw: - del kw[name] - - # - # Perform multiple tasks, iterating across the kw dict - # - for k in kw.keys(): - # - # Convert known timestamp fields to datetime format - # Remove empty fields - # - if k.lower() in self._TIMESTAMP_FIELDS: - if type(kw[k]) == datetime: - continue - elif type(kw[k]) == str: - if len(kw[k]) == 0: - del kw[k] - continue - else: - # dshell has a different default date/time string format than elastic, - # so let's try to parse that into a datetime object - try: - kw[k] = datetime.datetime.strptime( - kw[k], '%Y-%m-%d %H:%M:%S') - except: - pass # if fail, pass it through and let elastic try to parse it - else: - # if not a string, try to - try: - kw[k] = datetime.datetime.fromtimestamp(float(kw[k])) - except: - pass - # - # Get Rid of Dfiles. Must be handled elsewhere. - # - if isinstance(kw[k], dfile.dfile): - del kw[k] - - # - # Message - # - if self.options['message']: - if 'message' not in kw: - kw['message'] = args[0].rstrip() - - # - # Allow child classes to access the data one last time before the insert - # - if self.hasPreInsert: - if not self.preInsert(kw): - return (False, None) - - # - # Insert into elastic - # - if '_id' in kw: - docid = kw['_id'] - del kw['_id'] - es_response = self.es.index( - index=self.ELASTIC_INDEX, id=docid, doc_type=doc_type, body=kw) - else: - es_response = self.es.index( - index=self.ELASTIC_INDEX, doc_type=doc_type, body=kw) - if es_response['created']: - return (True, es_response) - else: - if es_response['_version'] > 1: - self.log("Possible key collision: %s" % - (str(es_response)), logging.WARN) - # sys.stderr.write(repr(kw)) - else: - self.log("Elasticsearch error: %s" % - (str(es_response)), logging.WARN) - return (False, es_response) - - def write(self, *args, **kwargs): - print "WRITE CALLED (Not implemented in output decoder)" - -obj = elasticout diff --git a/lib/output/jsonout.py b/lib/output/jsonout.py deleted file mode 100644 index 3af2078..0000000 --- a/lib/output/jsonout.py +++ /dev/null @@ -1,135 +0,0 @@ -''' -@author: amm -''' - -import dshell -import dfile -import output -import datetime -import json -import base64 - - -class JSONOutput(output.TextOutput): - - ''' - JSON Output module - use with --output=jsonout - - usage: as with csvout, you can pass a list of field names that will be included in the JSON output - - options - ------- - geoip: If set to Y, output module won't discard geoip tags - notrim: If set to Y, do not trim any fields from the output - ensure_ascii: Enable this option in json library - - ''' - - _TIMESTAMP_FIELDS = ( - 'ts', 'starttime', 'endtime', 'request_time', 'response_time') - - def __init__(self, *args, **kwargs): - - # Options - self.options = {} - for o in ('geoip', 'notrim', 'ensure_ascii'): - self.options[o] = False - if o in kwargs: - if kwargs[o] == True or kwargs[o].upper() in ('Y', 'T', '1', 'YES', 'ON', 'TRUE'): - self.options[o] = True - del kwargs[o] - - # Args as fields - self.jsonfields = None - if len(args): - self.jsonfields = [] - for a in args: - self.jsonfields.append(a) - - # Call parent init - output.TextOutput.__init__(self, **kwargs) - - def alert(self, *args, **kw): - self.fh.write( - json.dumps(self._filter_data(kw), ensure_ascii=self.options['ensure_ascii']) + "\n") - if self.nobuffer: - self.fh.flush() - - - # Reusable function to filter data in alerts and writes - def _filter_data(self, kw): - - # User specified field list?? - if self.jsonfields != None: - for f in kw.keys(): - if f not in self.jsonfields: - del kw[f] - elif not self.options['notrim']: - # Remove Common Redundant Fields - for name in ('addr', 'direction', 'clientport', 'serverport', 'clientip', 'serverip', 'sipint', 'dipint'): - if name in kw: - del kw[name] - # Time Fields - # Rename 'ts' to 'starttime' if 'starttime' not present - if 'ts' in kw: - if 'starttime' not in kw: - kw['starttime'] = kw['ts'] - del kw['ts'] - # Convert known timestamp fields to string format - for name in self._TIMESTAMP_FIELDS: - try: - kw[name] = datetime.datetime.fromtimestamp( - float(kw[name])).strftime(self.timeformat) - except: - pass - # Remove GEOIP Fields - if not self.options['geoip']: - for name in ('servercountrycode', 'clientcountrycode', 'sipcc', 'dipcc', 'clientasn', 'serverasn', 'dipasn', 'sipasn'): - if name in kw: - del kw[name] - - outdata = {} - for n,v in kw.iteritems(): - if not isinstance(v, dfile.dfile): - outdata[n] = v - - return outdata - - - def write(self,*args,**kw): - - # Iterate *args - for a in args: - if type(a) == dshell.Blob: - self.fh.write(json.dumps(self._blob_to_dict(blob), ensure_ascii=self.options['ensure_ascii']) + "\n") - elif type(a) == dshell.Connection: - outdata = self._filter_data(a.info()) - outdata['type'] = 'conn' - outdata['data'] = [] - for blob in a: - #self._write_blob(blob, kw) - outdata['data'].append(self._blob_to_dict(blob)) - self.fh.write(json.dumps(outdata, ensure_ascii=self.options['ensure_ascii']) + "\n") - else: - d = self._filter_data(kw) - d['type'] = 'raw' - if type(a) == unicode: - d['data'] = base64.b64encode(a.encode('utf-8')) - else: - d['data'] = base64.b64encode(a) - self.fh.write(json.dumps(d, ensure_ascii=self.options['ensure_ascii']) + "\n") - - # Custom error handler for data reassembly --- ignores all errors - def errorH(self, **x): - return True - - def _blob_to_dict(self, blob): - d = self._filter_data(blob.info()) - d['type'] = 'blob' - d['data'] = base64.b64encode(blob.data(errorHandler=self.errorH)) - return d - - - -obj = JSONOutput diff --git a/lib/output/netflowout.py b/lib/output/netflowout.py deleted file mode 100644 index 0c943c4..0000000 --- a/lib/output/netflowout.py +++ /dev/null @@ -1,86 +0,0 @@ -''' -@author: amm -''' - -import output -import util -import sys -import datetime - - -class NetflowOutput(output.TextOutput): - - ''' - Netflow Output module - use with --output=netflowoutput -` use group=clientip,serverip for grouping by clientip,serverip - ''' - #_DEFAULT_FIELDS=[('decoder','s'),('datetime','s'),('sip','s'),('sport','s'),('dip','s'),('dport','s')] - #_DEFAULT_FORMAT="%(starttime)s %(sip)16s:%(sport)-5s -> %(dip)16s:%(dport)-5s" - - def __init__(self, *args, **kwargs): - self.group = kwargs.get('group') - self.groups = {} - if self.group: - self.group = self.group.split('/') - # Call parent init - output.TextOutput.__init__(self, **kwargs) - - def alert(self, *args, **kw): - if self.group: - k = tuple(kw[g] for g in self.group) # group by selected fields - if k not in self.groups: - r = k[::-1] - if r in self.groups: - k = r # is other dir in groups - else: - self.groups[k] = [] - self.groups[k].append(kw) - else: - self.__alert(**kw) # not grouping, just print it - - def close(self): - # dump groups if we are closing output - if self.group: - for k in sorted(self.groups.iterkeys()): - # write header - self.fh.write(' '.join( - '%s=%s' % (self.group[i], k[i]) for i in xrange(len(self.group))) + '\n') - for kw in self.groups[k]: - self.fh.write('\t') - self.__alert(self, **kw) - self.fh.write('\n') - output.TextOutput.close(self) - - def __alert(self, *args, **kw): - self.fh.write('%s %16s -> %16s (%s -> %s) %4s %6s %6s %5d %5d %7d %7d %-.4fs\n' % (datetime.datetime.utcfromtimestamp(kw['starttime']), - kw[ - 'clientip'], - kw[ - 'serverip'], - kw[ - 'clientcountrycode'], - kw[ - 'servercountrycode'], - kw[ - 'proto'], - kw[ - 'clientport'], - kw[ - 'serverport'], - kw[ - 'clientpackets'], - kw[ - 'serverpackets'], - kw[ - 'clientbytes'], - kw[ - 'serverbytes'], - ( - kw['endtime'] - kw['starttime']) - ) - ) - if self.nobuffer: - self.fh.flush() - -obj = NetflowOutput diff --git a/lib/output/output.py b/lib/output/output.py deleted file mode 100644 index 41a2122..0000000 --- a/lib/output/output.py +++ /dev/null @@ -1,562 +0,0 @@ -''' -dShell output classes - -@author: tparker -''' -import os -import sys -import logging -import struct -import datetime -import dshell -import util - - -class Output(object): - - ''' - dShell output base class, extended by output types - ''' - - _DEFAULT_FORMAT = '' - _DEFAULT_TIMEFORMAT = '%Y-%m-%d %H:%M:%S' - _DEFAULT_DELIM = ' ' - _NULL = None - - # true if you want to remove extra fields from the parsed record - _FILTER_EXTRA = False - - def __init__(self, *a, **kw): - ''' - base output class constructor - configuration kwords: - logger= to pass in a logger - format='format string' to override default formatstring for output class - pcap = filename to write pcap - ''' - # setup the logger - self.logger = kw.get('logger', logging) - - # parse the format string - self.setformat(kw.get('format', self._DEFAULT_FORMAT)) - self.timeformat = (kw.get('timeformat', self._DEFAULT_TIMEFORMAT)) - self.delim = (kw.get('delim', self._DEFAULT_DELIM)) - - # Run flush() after every relevant write() if this is true - self.nobuffer = (kw.get('nobuffer', False)) - - if 'pcap' in kw: - self.pcapwriter = PCAPWriter(kw['pcap']) - else: - self.pcapwriter = None - - # this is up to the output plugin to process - # by default stuffs extra fields and data into 'extra' field - # if _FILTER_EXTRA is true - self.extra = kw.get('extra', False) - - # create the default session writer - if 'session' in kw: - self.sessionwriter = SessionWriter(**kw) - else: - self.sessionwriter = None - - # write a message to the log - def log(self, msg, level=logging.INFO, *args, **kw): - '''write a message to the log - passes all args and kwargs thru to logging - except for level= is used to set logging level''' - self.logger.log(level, msg, *args, **kw) - - def setformat(self, formatstr=None, typemap=None): - '''parse a format string and extract the field info - if no string given, reverts to default for class - will set self.fields to be a list of (name,type,spec) tuples - self.fieldnames to a list of fieldnames - and self.fieldmap to a list of key=in value=out mappings - format string can also map in field to out field with %(in:out)spectype - or specify an explicit out type with %(in:out)specintype:outtype - (note this breaks compatibility with text formatting, - but useful for db or other output modules) - a typemap of [intype]=outtype (or [in]=(newintype,outtype) - can be used to map and replace types - ''' - if formatstr: - self.format = formatstr + "\n" - else: - self.format = self._DEFAULT_FORMAT + "\n" - self.fields = [] # will be a (name,type,length) tuple - self.fieldnames = [] - self.fieldmap = {} - # get all the field names - e = 0 - while True: - # find the next format spec of %(...) - s = self.format.find('%', e) + 1 - if s < 1 or self.format[s] != '(': - break # not %(... - e = self.format.find(')', s) - if e < 0: - break # didn't find a closing paren - # get text between parens as field name - fname = self.format[s + 1:e] - # len/precision specs will be 0-9 between ) and type char - fspec = '' - for i in xrange(e + 1, len(self.format)): - if self.format[i] in '1234567890.+-# lLh': - fspec += self.format[i] - else: - break # this char is not a spec char, it is the type char - ftype = self.format[i] - i += 1 - # is the field type a intype:outtype def? - if i < len(self.format) and self.format[i] == ':': - e = self.format.find(' ', i) # find the end whitespace - # split on: to get input:output mapping - ftype, outtype = self.format[i - 1:e].split(':') - else: - outtype = None # output will be same as input type - e = i # start at next char on loop - try: # field name to column mapping - fname, fmap = fname.split(':') - except: - fmap = fname # no mapping - if typemap and ftype in typemap and not outtype: - try: - (ftype, outtype) = typemap[ftype] - except: - outtype = typemap[ftype] - # append the field name,type,spec,mapping - self.fields.append((fname, ftype, fspec)) - self.fieldnames.append(fname) - if outtype: - self.fieldmap[fname] = (fmap, outtype) # map of in to out,type - - def parse(self, *args, **kw): - '''parse the input args/kwargs into a record dict according to format string - - timestamps are formatted to date/time strings - - fields not in the input will be defined but blank - - extra fields in the record will be formatted into a - "name=value name2=value2..." string and put in 'extra' - - args will go into 'data' - - format keyword can contain a new format string to use (this also sets format for future output) - ''' - # convert timestamps to proper format - for ts in [k for k in kw if k == 'ts' or k.endswith('time')]: - dt = ts[:-4] + 'datetime' # ts->datetime , Xtime -> Xdatetime - kw[dt] = datetime.datetime.fromtimestamp( - float(kw[ts])).strftime(self.timeformat) # format properly - if kw.get('direction') is 'cs': - kw['dir_arrow'] = '->' - elif kw.get('direction') is 'sc': - kw['dir_arrow'] = '<-' - else: - kw['dir_arrow'] = '--' - if 'format' in kw: - self.setformat(kw['format']) # change the format string? - del kw['format'] - # create the record initialized to the _NULL value - rec = dict((f, self._NULL) for f in self.fieldnames) - # populate record from datadict if datadict key is a field - if self._FILTER_EXTRA: - rec.update( - dict((f, kw[f]) for f in self.fieldnames if (f in kw and kw[f] != None))) - # place extra datadict keys into the extra field (and exclude the - # addr tuple) - if self.extra: - rec['extra'] = self.delim.join(['%s=%s' % (f, kw[f]) for f in sorted( - kw.keys()) if f not in self.fieldnames and f != 'addr']) - else: # not filtering extra, just lump them in as fields - rec.update(kw) - # populate the data field - if args: - rec['data'] = self.delim.join(map(str, args)) - return rec - - def dump(self, pkt=None, **kw): # pass packets to pcap - '''dump raw packet data to an output - override this if you want a format other than pcap''' - pktdata = str(pkt) # might be string, might be a dpkt object - pktlen = kw.get('len', len(pktdata)) - if self.pcapwriter: - self.pcapwriter.write(pktlen, pktdata, kw['ts']) - else: - self.log(util.hexPlusAscii(str(pkt)), level=logging.DEBUG) - - # close the PCAP output - def close(self): - if self.pcapwriter: - self.pcapwriter.close() - - def dispatch(self, m, *args, **kwargs): - '''dispatch from Q pop''' - if m == 'write': - self.write(*args, **kwargs) - if m == 'alert': - self.alert(*args, **kwargs) - if m == 'dump': - self.dump(*args, **kwargs) - - def setup(self): - """ - Perform any additional setup outside of the standard __init__. - Runs after command-line arguments are parsed, but before decoders are run. - For example, printing header data to the outfile. - """ - pass - - -class FileOutput(Output): - - def __init__(self, *args, **kw): - '''configuration for fileoutput: - fh= - file=filename to write to - mode=mode to open file as, default 'w' - ''' - # do base init first - Output.__init__(self, *args, **kw) - # get the output filehandle or file - f = None - if 'fh' in kw: - self.fh = kw['fh'] - return - elif 'file' in kw: - f = kw['file'] - elif args: - f = args[0] - if f: - if 'mode' in kw: - mode = kw['mode'] - else: - mode = 'w' - if mode == 'noclobber': - mode = 'w' - try: - while os.stat(f): - p = f.split('-') - try: - p, n = p[:-1], int(p[-1]) - except ValueError: - n = 0 - f = '-'.join(p + ['%04d' % (int(n) + 1)]) - except OSError: - pass # file not found - self.fh = open(f, mode) - else: - self.fh = sys.stdout - - def write(self, obj, **kw): - '''write session data to the session output or stdout''' - if self.sessionwriter: - self.sessionwriter.write(obj, **kw) - elif self.fh: - self.fh.write(str(obj)) - if self.nobuffer: - self.fh.flush() - - def close(self): - '''close output if not stdout''' - if self.fh != sys.stdout: - self.fh.close() - Output.close(self) - - -class TextOutput(FileOutput): - - '''formatted text output to file or stdout''' - - _DEFAULT_FORMAT = "%(decoder)s %(datetime)s %(sip)16s:%(sport)-5s %(dir_arrow)s %(dip)16s:%(dport)-5s ** %(data)s **" - _NULL = '' - - _FILTER_EXTRA = True - - def __init__(self, *args, **kw): - if 'extra' in kw: - self._DEFAULT_FORMAT += " [ %(extra)s ]" - FileOutput.__init__(self, *args, **kw) - - def alert(self, *args, **kw): - '''write an alert record - we pass in the decoder object and args/dict''' - rec = self.parse(*args, **kw) - if rec: - self.fh.write(self.format % rec) - if self.nobuffer: - self.fh.flush() - - -class DBOutput(Output): - - '''format strings as used by the DBOutput module to create tables and map fields - these follow the usual %(name)type and in most cases a custom format string will work - defualt type maps are: - s,r = VARCHAR (if field len given) /TEXT (if no len) - c = CHAR(1) - x,X,o = VARCHAR - d,i,u = INTEGER - e,E,f,F,g,G = DECIMAL - with the following extra: (using these breaks text format string compatibility) - b = boolean - t = timestamp - D = datetime - T = this field selects table - (following are postgres-only) - A = inet - H = host - N = cidr - M = macaddr - format string can also map field to column with %(field:column)type - or specify an explicit column type with %(field:column)pytype:DBTYPE - (note this also breaks compatibility with text format strings) - ''' - - _DEFAULT_FORMAT = "%(decoder)T %(ts:timestamp)t %(sip)s %(sport)s %(dip)s %(dport)s %(data:alert)s" - _NULL = None - # format type to (type,coltype) map - _TYPEMAP = {'s': 'VARCHAR', 'r': 'VARCHAR', 'c': 'CHAR(1)', - 'x': 'VARCHAR', 'X': 'VARCHAR', 'o': 'VARCHAR', - 'd': 'INTEGER', 'i': 'INTEGER', 'u': 'INTEGER', - 'e': 'DECIMAL', 'E': 'DECIMAL', - 'f': 'DECIMAL', 'F': 'DECIMAL', - 'g': 'DECIMAL', 'G': 'DECIMAL', - # 'b' isn't a python type, so (ftype,DBTYPE) tuple for value formats input as ftype - 'b': ('d', 'BOOLEAN'), - # not standard across database types! - 't': ('f', 'TIMESTAMP'), 'D': ('s', 'DATETIME'), - 'A': ('s', 'INET'), 'H': ('s', 'HOST'), 'N': ('s', 'CIDR'), 'M': ('s', 'MACADDR')} # these are postgres specific - - # acceptable params to pass to db module connect method - _DBCONNPARAMS = ['host', 'user', 'passwd', - 'password', 'db', 'database', 'port', 'charset'] - - # map of db type to insert placeholder. '%s' is the default, but sqlite3 doesn't like it - # you can override this with the 'placeholder' config keyword - _DBTYPE_PLACEHOLDER_MAP = {'sqlite3': '?'} - - def __init__(self, *args, **kw): - '''configuration: - config=db config .ini file name to parse - - config keywords: - - dbtype=database type, selects DB API module to load - in conf file use [dbtype] section name instead - - host,user,passwd,password,db,database,port will be passed to db module if present - - table=db table to use if not specified by a field - - insert_param=character to use as parameter placeholder for INSERT - (sqlite3=?, default=%%s) - - format_types=types to format before insert (default=x) - ('s' to pad strings, 'x' to convert to hex, 'f' to format floats, 'fx' for hex and floats...) - ''' - self.dbconfig = kw.copy() - # if we were passed a config.ini file, parse it and add the k/v pairs - # to the config - if 'config' in self.dbconfig: - import ConfigParser - config = ConfigParser.ConfigParser() - config.read(self.dbconfig['config']) - sections = config.sections() - if len(sections) > 0: - self.dbconfig['dbtype'] = sections[0] - for k, v in config.items(sections[0], raw=True): - self.dbconfig[k] = v - # import the db module - self.db = __import__(self.dbconfig['dbtype']) - # create a connection, using a dict filtered to db conn params - self.dbconn = self.db.connect( - *args, **dict((k, self.dbconfig[k]) for k in self._DBCONNPARAMS if k in self.dbconfig)) - # do the base init last to catch the format string, etc.. (as it may - # have come from the config file) - Output.__init__(self, *args, **self.dbconfig) - - def createtable(self, table=None): - '''creates a table based on the format string''' - if not table and 'table' in self.dbconfig: - table = self.dbconfig['table'] - try: - cursor = self.dbconn.cursor() - sqlfields = [] - for fname, ftype, fspec in [f for f in self.fields if f[1] != 'T']: - ctype = self.fieldmap[fname][1] - # if no width spec, use TEXT instead of VARCHAR and hope the db - # likes it - if ctype == 'VARCHAR' and not fspec: - ctype = 'TEXT' - fdef = self.fieldmap[fname][0] + ' ' + ctype - if fspec: - # try to conver python format spec to something SQL will - # take - fdef += '(' + \ - fspec.strip('+-# lLh').replace('.', ',') + ')' - sqlfields.append(fdef) - sql = 'CREATE TABLE "' + table + '" (' + ','.join(sqlfields) + ')' - self.log(sql, logging.DEBUG) - return cursor.execute(sql) - except: - raise - - def close(self): - '''closes database connection''' - self.dbconn.close() - Output.close(self) - - def alert(self, *args, **kw): - '''write an output record - we pass in the decoder object and args/dict''' - rec = self.parse(self, *args, **kw) - if rec: - self.insert(rec) - - def setformat(self, formatstr=None): - '''calls main setformat and then builds the insert SQL''' - # what is the insert param?? some databases use %s, some use ? - # try to map it or take the placeholder keyword from config - ph = self.dbconfig.get('insert_param', - self._DBTYPE_PLACEHOLDER_MAP.get( - self.dbconfig['dbtype'], '%%s') - ) - # these are the types we need to format before passing to the db - self.format_types = self.dbconfig.get('format_types', 'x') - Output.setformat(self, formatstr, typemap=self._TYPEMAP) - # build all fields we map (except for [T]able select) - self.tablefield = 'decoder' # default to decodername - for fname, ftype, fspec in self.fields: - if ftype == 'T': - self.tablefield = fname - sqlfields = [self.fieldmap[fname][0] - for (fname, ftype, fspec) in self.fields if fname in self.fieldmap] - self.insertsql = 'INSERT INTO "%%s" (%s) VALUES (%s)' % ( - ','.join(sqlfields), ','.join([ph] * len(sqlfields))) - - def insert(self, rec, table=None): - ''' inserts rec dict using self.format into table (if given, else default or specified by field) - if insert fails, tries to create table and insert again before raising exception ''' - if not table: - if 'table' in self.dbconfig: - table = self.dbconfig['table'] - elif rec[self.tablefield]: - table = rec[self.tablefield] - try: - sqlvalues = [] - cursor = self.dbconn.cursor() - for fname, ftype, fspec in self.fields: - if fname in self.fieldmap: - # do we preformat this data? - if ftype in self.format_types: - sqlvalues.append(('%' + fspec + ftype) % rec[fname]) - else: - sqlvalues.append(rec[fname]) - # create a INSERT INTO table (fields) VALUES (?,?,?) for execute - sql = self.insertsql % table - self.log(sql + ' %s' % sqlvalues, logging.DEBUG) - except: - raise - # try once, if it fails, try to create table and retry - # throws on second failure or create table failure - fail = False - while True: - try: - cursor.execute(sql, sqlvalues) - self.dbconn.commit() - break # success - except Exception, e: - self.log(e, level=logging.WARNING) - if fail: - raise - else: - fail = True - try: - self.createtable(table) - except: - raise - - -class PCAPWriter(FileOutput): - - '''writes a pcap file''' - - def __init__(self, *args, **kw): - FileOutput.__init__(self, *args, **kw) - if self.fh: - self.fh.write( - struct.pack('IHHIIII', 0xa1b2c3d4, 2, 4, 0, 0, 65535, 1)) - - # overrides Output.write to write session as PCAP - # data flow is Output.dump->pcapwriter.write - def write(self, pktlen, pktdata, ts): - if self.fh: - self.fh.write( - struct.pack('II', int(ts), int((ts - int(ts)) * 1000000))) - # captured length, original length - self.fh.write(struct.pack('II', len(pktdata), pktlen)) - self.fh.write(pktdata) - - -class SessionWriter(Output): - - '''writes the session to one or more files''' - - def __init__(self, session=None, **kw): - self.file = kw.get('session', session) - self.dir = kw.get('direction', 'both') - self.mode = kw.get('mode', 'a') - self.timeformat = (kw.get('timeformat', self._DEFAULT_TIMEFORMAT)) - self.fieldnames = [] - - def write(self, obj, **kwargs): - out = None - kw = dict(**kwargs) - # if a session object with info() and data() methods (conn or blob, but - # not packet) - try: - kw.update(**obj.info()) # get object info - kw = self.parse(**kw) - if self.dir == 'both': - ds = [None] - elif self.dir == 'split': - ds = ['cs', 'sc'] - else: - ds = [self.dir] - for d in ds: - kw.update(direction=d if d else 'both') # set direction - # format filename and open - out = FileOutput(self.file % kw, mode=self.mode) - # write obj data for direction - out.fh.write(obj.data(direction=d)) - out.close() - except: # if not a session object - # build filename from kw - out = FileOutput(self.file % kw, mode=self.mode) - out.fh.write(str(obj)) - out.close() - - -class QueueOutput(Output): - - '''pipes pickled packets to parent process''' - - def __init__(self, q, **kwargs): - self.queue = q - Output.__init__(self, **kwargs) - - def write(self, *args, **kw): self.dispatch('write', *args, **kw) - - def alert(self, *args, **kw): self.dispatch('alert', *args, **kw) - - def dump(self, *args, **kw): self.dispatch('dump', *args, **kw) - - def dispatch(self, m, *args, **kw): # takes (method,...) to Q - self.queue.put((m, args, kw)) - - def close(self): - self.queue.close() - Output.close(self) - - -# default output module -obj = TextOutput diff --git a/lib/output/xmlout.py b/lib/output/xmlout.py deleted file mode 100644 index 37a5e7a..0000000 --- a/lib/output/xmlout.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -@author: tparker -''' - -import output -import util -import dshell -from xml.etree import ElementTree as ET - - -class XMLOutput(output.FileOutput): - - '''XMLOutput Module''' - - def __init__(self, *args, **kwargs): - '''init the underlying file output to get the file handle''' - output.FileOutput.__init__( - self, *args, **kwargs) # pass all to fileoutput - self.root = ET.Element('dshell') - self.element = self.root - - def alert(self, *args, **kwargs): - '''we will assume we get alerts before we get the matching session data''' - self.element = ET.SubElement( - self.root, 'alert', self._filter_attr(kwargs)) - self.element.text = self._filter_text(' '.join(args)) - - def write(self, obj, parent=None, **kwargs): - '''write the object data under the last alert element (or the root if no alert) - if a conn object recurse in by iterating - else write the string output of the object''' - if not parent: - parent = self.element - kw = dict(**kwargs) - # turns "" into "yyyy" - tag = str(type(obj)).split("'", 2)[1] - if tag.startswith('dshell.'): # is a dshell object - kw.update(**obj.info()) # get attribs - # turns "dshell.Connection" into "Connection" - tag = tag.split('dshell.')[1] - e = ET.SubElement(parent, tag, self._filter_attr(kw)) - if tag == 'Connection': # recurse on blobs in conn - for blob in obj: - self.write(blob, parent=e) - return # subobjects will have the data - # leave this up to the object to handle - e.text = self._filter_text(str(obj)) - - def _filter_attr(self, d): return dict((k, str(v)) - for (k, v) in d.iteritems()) - - def _filter_text(self, t): return ''.join(c for c in t if ord(c) < 128) - - def close(self): - '''write the ElementTree to the file''' - ET.ElementTree(self.root).write(self.fh) - -'''NOTE: output modules return obj=reference to the CLASS - instead of a dObj=instance so we can init with args''' -obj = XMLOutput diff --git a/lib/smbdecoder.py b/lib/smbdecoder.py deleted file mode 100644 index 3266e99..0000000 --- a/lib/smbdecoder.py +++ /dev/null @@ -1,429 +0,0 @@ -''' -2015 Feb 13 - -Extend dshell.TCPDecoder to handle SMB Message Requests/Responses - -Will call SMBHandler( - conn = Connection(), - request=dshell.smb.smbdecoder.SMB(), - response=dshell.smb.smbdecoder.SMB(), - requesttime=timestamp, - responsetime=timestamp, - cmd= [3] - status= [2] A 32-bit field used to communicate error - messages from the server to the client - ) - -Requests are tracked by MID - -It will be up to the decoder to handle each SMB Command. - -Several functions create throw-away variables when unpacking data. Because of -this, pylint checks were run with "-d unused-variables" - -References: -[1] http://anonsvn.wireshark.org/viewvc/trunk/epan/dissectors/packet-smb.c?revision=32650&view=co&pathrev=32650 -[2] http://msdn.microsoft.com/en-us/library/ee441774%28v=prot.13%29.aspx SMB Header Protocol Definition -[3] http://msdn.microsoft.com/en-us/library/ee441616(v=prot.13).aspx -''' - - -import dshell -import struct -#import binascii - -SMB_PROTOCOL = '\xffSMB' -SMB_STATUS_SUCCESS = 0x0 -NTLMSSP_IDENT = 'NTLMSSP\x00' -NTLMSSP_AUTH = 0x00000003 -NTLMSSP_CHALLENGE = 0x00000002 - - -class SMBDecoder(dshell.TCPDecoder): - - def __init__(self, **kwargs): - self.requests = {} # requests are stored by MID - dshell.TCPDecoder.__init__(self, **kwargs) - - def connectionInitHandler(self, conn): - self.requests[conn.addr] = {} - - def blobHandler(self, conn, blob): - data = blob.data() - offset = 0 - datalen = len(data) - while offset < datalen: - try: - offset += self.smbFactory(conn, blob, data[offset:]) - except InsufficientData: - return - - # Returns number of bytes used by NetBIOS+SMB - # e.g. mlength+4 - def smbFactory(self, conn, blob, data): - - try: - msgtype, mlength, smbdata = self.parseNetBIOSSessionService(data) - except InsufficientData: - raise - - try: - # create SMB Message (abstract data model: SMB header + extra) - smb = SMB(smbdata) - except InsufficientData: - raise - - if smb.proto != SMB_PROTOCOL: - return mlength + 4 - - if blob.direction == 'cs': - self.requests[conn.addr][smb.mid] = [blob.starttime, smb] - elif blob.direction == 'sc': - if smb.mid in self.requests[conn.addr].keys(): - requesttime, request = self.requests[conn.addr][smb.mid] - responsetime, response = blob.starttime, smb - - if 'SMBHandler' in dir(self): - self.SMBHandler(conn=conn, request=request, response=response, - requesttime=requesttime, responsetime=responsetime, cmd=smb.cmd, status=smb.status) - - del self.requests[conn.addr][smb.mid] - - return mlength + 4 - - def connectionHandler(self, conn): - """ clean up all requests associated with this connection """ - if conn.addr in self.requests: - if len(self.requests[conn.addr]) > 0: - for mid in self.requests[conn.addr].keys(): - requesttime, request = self.requests[conn.addr][mid] - self.SMBHandler(conn=conn, request=request, response=None, - requesttime=requesttime, responsetime=None, cmd=request.cmd, status=-1) - del self.requests[conn.addr][mid] - del self.requests[conn.addr] - - def postModule(self): - """ clean up self.requests to process all SMB messages that only have a single request and no response """ - for k in self.requests.keys(): - for mid in self.requests[k].keys(): - requesttime, request = self.requests[k][mid] - self.SMBHandler(conn=None, request=request, response=None, - requesttime=requesttime, responsetime=None, cmd=request.cmd, status=-1) - del self.requests[k][mid] - del self.requests[k] - - def parseNetBIOSSessionService(self, data): - """ parse the NetBIOS Session Service header [2]""" - if len(data) < 4: - raise InsufficientData - msgtype = struct.unpack('B', data[0])[0] - arg1, arg2, arg3 = struct.unpack('3B', data[1:4]) - mlength = (arg1 * 512) + (arg2 * 256) + arg3 - smbdata = data[4:] - return msgtype, mlength, smbdata - - def SMBHandler(self, conn, request=None, response=None, requesttime=None, responsetime=None, cmd=None, status=None): - "Placeholder. Overwrite in separate decoders." - pass - - -class InsufficientData(Exception): - pass - - -class SMB(): - - def __init__(self, pktdata): - """ - Generic SMB class. Handles parsing of SMB Header Messages and some specific SMB Command Objects - - Reference: - [1] http://msdn.microsoft.com/en-us/library/ee441774%28v=prot.13%29.aspx SMB Header Protocol Definition - [2] http://msdn.microsoft.com/en-us/library/ee441616(v=prot.13).aspx - - - proto = 4 bytes 4s 4-byte literal string '\xFF', 'S', 'M', 'B' - cmd = 1 byte B one-byte command code, commands listed at [2] - status = 4 bytes I A 32-bit field used to communicate error messages from the server to the client [SUCCESS = 0x0000 - flags1 = 1 byte B - flags2 = 2 bytes H - pidhigh = 2 bytes H - security = 8 bytes 8s - reserved = 2 bytes H - tid = 2 bytes H - pidlow = 2 bytes H - uid = 2 bytes H Associate a session with a specific user - mid = 2 bytes H Multiplexer identifier - """ - self.filename = None - if len(pktdata) < 32: - raise InsufficientData - self.proto, self.cmd, self.status, self.flags1, self.flags2, self.pidhigh, self.security, self.reserved, self.tid, self.pidlow, self.uid, self.mid = struct.unpack( - '<4sBIBHH8sHHHHH', pktdata[:32]) - self.smbdata = pktdata[32:] - - def PARSE_NT_CREATE_ANDX_REQUEST(self, data): - """ return the filename associated with the request (return None if err)""" - try: - wct, andxcmd, rsrv1, andxoffset, rsrv2, filenamelen, cflags, rootfid, mask, size, attrib, share = struct.unpack( - ' len(secblob): - return None - msgtype = struct.unpack( - '