diff --git a/README.md b/README.md index b82179c..d78b135 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ sudo $(which poetry) run python3 ctiger.py -h * Website: http://anoduck.github.io * Github: [@anoduck](https://github.com/anoduck) +[![built with Codeium](https://codeium.com/badges/main)](https://codeium.com) + ## Show your support Give a ⭐️ if this project helped you! diff --git a/ctiger.py b/ctiger.py index ceb76c8..0e4ef99 100644 --- a/ctiger.py +++ b/ctiger.py @@ -9,7 +9,6 @@ Created on Fri Jul 21 17:05:07 2023 import os import sys -import fcntl import argparse from scapy.sendrecv import sniff from scapy.sendrecv import AsyncSniffer @@ -24,7 +23,6 @@ from scapy.layers.dot11 import Dot11FCS from scapy.config import Conf as scapyconfig from scapy.layers.eap import EAPOL from scapy.utils import PcapWriter -from getmac import get_mac_address # Import Faker. from faker import Faker # Import the WifiESSID class from Faker Wi-Fi ESSID. @@ -33,15 +31,14 @@ from faker import Faker # from scapy_ex import Dot11Elt from art.art import tprint from dataclasses import dataclass -import struct import multiprocessing as mp import asyncio import threading -from random import choice, randint +from threading import Thread +from random import choice from configobj import ConfigObj, validate from collections import Counter import pandas as pd -import socket import signal import logging from time import sleep @@ -214,24 +211,36 @@ def PRN2(pkt): # print("Sequence Control:", frame.SC) # print(feature(frame)) # print("\n") +# ------------------------------------------------------------------ +# Five choices for Duration/ID: +# 1. Calculated per packet +# 2. Or one of the following: 16383, 26370, 32767, 65535 +# +# Calculated as: +# bytes = struct.pack("H", bytes)[0] +# ------------------------------------------------------------------ def strainer(pkt) -> None: if pkt[Dot11].type == 0 and pkt[Dot11].subtype == 4: bssid = pkt[Dot11FCS].addr2 log.info('BSSID for strainer: {0}'.format(bssid)) log.debug('Local Macaddr is: {0}'.format(macaddr)) + idval = [16383, 26370, 32767, 65535] + durid = choice(idval) new_pkt = RadioTap()/Dot11(proto=0, type=1, subtype=11, addr1=bssid, addr2=macaddr, - ID=65535) + ID=durid) log.debug('Sending RTS frame to {0} with type 11'.format(bssid)) - res = srp1(new_pkt, timeout=3, verbose=0, retry=0) - if res: + res = srp1(new_pkt, timeout=3, verbose=0, retry=0, threaded=True) + if res is not None: if res[Dot11].type == 1 and res[Dot11].subtype == 12: log.debug('Recieved CTS packet.') log.info('Intercepted CTS from: {0}'.format(bssid)) dbm_signal = pkt.dBm_AntSignal channel = extract_channel(res[Dot11]) scan_df.loc[bssid] = ['N/A', dbm_signal, channel, 'N/A'] + scan_df.to_csv(valid_file, mode='a') # ------------------------------------------------------------------- @@ -312,7 +321,8 @@ class NetDev: mon_crtd = self.interface + 'mon' self.mon_crtd = mon_crtd if self.mon_type == 'create': - self.create_if() + self.create_if(interface=self.interface, + macaddr=self.macaddr, mon_crtd=mon_crtd) mon_if = self.mon_crtd return mon_if elif self.mon_type == 'switch': @@ -325,15 +335,6 @@ class NetDev: sys.exit(1) -def signal_handler(signal, frame) -> None: - print('You pressed Ctrl+C!') - log.info('Shutting down') - scan_df.to_csv('ct_purge.csv') - log.info('Saved results to: {0}'.format('ct_purge.csv')) - log.info('Going Down!!') - sys.exit(0) - - # ██████╗ ██╗ ██╗██████╗ ██████╗ ███████╗ # ██╔══██╗██║ ██║██╔══██╗██╔════╝ ██╔════╝ # ██████╔╝██║ ██║██████╔╝██║ ███╗█████╗ @@ -349,6 +350,9 @@ class Purge(object): self.valid_file = kwargs.get('valid_file') self.channels = kwargs.get('channels') + def __getitem__(self, pkt): + return pkt + def channel_runner(self, mon_if, channels) -> None: self.mon_if = mon_if self.channels = channels @@ -385,6 +389,7 @@ class Purge(object): # log.info('We will be writing captured macs to ', str(self.valid_file)) chop = asyncio.to_thread(self.channel_runner, self.mon_if, self.channels) + global chopper chopper = asyncio.create_task(chop) log.info('Channel runner started.') await asyncio.sleep(0) @@ -397,13 +402,152 @@ class Purge(object): log.info('asniffer started') forever_wait = threading.Event() forever_wait.wait() + + def send_pkt(self, bssid) -> None: + self.bssid = bssid + idval = [16383, 26370, 32767, 65535] + durid = choice(idval) + new_pkt = RadioTap()/Dot11(proto=0, type=1, subtype=11, + addr1=bssid, + addr2=macaddr, + ID=durid) + log.debug('Sending RTS frame to {0} with type 11'.format(bssid)) + sendp(new_pkt, timeout=3, verbose=0, retry=0, threaded=True) + async def get_interface(self, interface, mon_type) -> str: + self.interface = interface + self.mon_type = mon_type + ndev = NetDev(interface=self.interface, mon_type=self.mon_type) + mon_if = ndev.start_monitor(interface=self.interface, + mon_type=self.mon_type) + return mon_if + + async def random_chan(self, channels): + self.channels = channels + chlist = self.channels.split(',') + chans = [int(chan) for chan in chlist] + ichan = choice(chans) + return ichan + + async def change_chan(self, mon_if, ichan): + self.mon_if = mon_if + self.ichan = ichan + log.debug('Monitor interface: {0}'.format(self.mon_if)) + log.debug('New Channel: {0}'.format(self.ichan)) + # iw [options] dev set channel + change_it = os.system( + 'iw dev ' + self.mon_if + ' set channel ' + self.ichan + ) + if change_it: + current_channel = self.ichan + return current_channel + + def probe_proc(self, probe_pkts): + ptup_list = [] + for ppkt in probe_pkts: + dbm_signal = ppkt.dBm_AntSignal + self.dbm_signal = dbm_signal + bssid = ppkt[Dot11FCS].addr2 + self.bssid = bssid + ptuple = (bssid, dbm_signal) + ptup_list.append(ptuple) + punified = list(set(ptup_list)) + return punified + + def cts_prn(self, pkt): + dbm_signal = pkt.dBm_AntSignal + ichan = extract_channel(pkt[Dot11]) + scan_df.loc[self.bssid] = [macaddr, dbm_signal, ichan, 'N/A'] + scan_df.to_csv(valid_file, mode='a') + log.info('Results written to {0}'.format(valid_file)) + + async def chan_timer(self): + await asyncio.sleep(14.7) + log.info('Channel timer done.') + + def probe_prn(self, pkt): + bssid = pkt[Dot11FCS].addr2 + self.bssid = bssid + log.info('Sending RTS frame to {0}'.format(bssid)) + self.send_pkt(bssid) + return + + async def mac_revealer(self, interface, mon_type, valid_file, channels): + log.info('mac revealer started') + self.interface = interface + self.mon_type = mon_type + self.valid_file = valid_file + self.channels = channels + log.info('setting up class attributes') + global scan_df + scan_df = get_df() + log.info('acquired Dataframe') + mon_if = await self.get_interface(self.interface, self.mon_type) + log.debug('mon_if: {0}'.format(mon_if)) + log.debug('return type: {0}'.format(type(mon_if))) + self.mon_if = mon_if + log.info('interface {0} is up and running.'.format(self.mon_if)) + chop = asyncio.to_thread(self.channel_runner, + self.mon_if, self.channels) + global chopper + chopper = asyncio.create_task(chop) + log.info('Channel runner started.') + while True: + probe_sniff = AsyncSniffer( + iface=mon_if, prn=self.probe_prn, + filter="type mgt subtype probe-req", + monitor=True) + probe_sniff.start() + log.info('Probe sniffer started') + # presult = await self.probe_proc(probe_sniff.results) + # log.info('Processing results from probe sniffer.') + # bssid = presult[0] + # dbm_signal = presult[1] + await asyncio.sleep(0) + # await self.send_pkt(bssid) + # log.info('Sending RTS frame to {0}'.format(bssid)) + # await asyncio.sleep(0) + cts_sniff = AsyncSniffer(filter='type ctl subtype cts', + iface=mon_if, prn=self.cts_prn, + monitor=True) + cts_sniff.start() + log.info('CTS sniffer started') + await asyncio.sleep(0) + # valid_cts = await self.cts_proc(cts_sniff.results) + # log.info('Validating CTS packet.') + # if valid_cts: + # scan_df.loc[bssid] = [macaddr, dbm_signal, ichan, 'N/A'] + # scan_df.to_csv(valid_file, mode='a') + # log.info('Results written to {0}'.format(valid_file)) + # else: + # log.info('Unable to validate CTS packet.') + # await asyncio.sleep(0) + # alltasks = asyncio.all_tasks() + # current_task = asyncio.current_task() + # alltasks.remove(asyncio.current_task) + # await asyncio.wait(alltasks) + # ttimer = asyncio.create_task(self.chan_timer()) + # await ttimer + # log.info('Beginning channel hopping.') + # ichan = await self.random_chan(self.channels) + # self.ichan = ichan + # await asyncio.create_task(self.change_chan(self.mon_if, self.ichan)) + # await asyncio.sleep(0) + # continue def start_purge(self) -> None: - asyncio.run(self.mac_purge(self.interface, - self.mon_type, - self.valid_file, - self.channels)) + signal.signal(signal.SIGINT, signal_handler) + print('Enter Ctrl+C TWICE to fully stop the script.') + # asyncio.run(self.mac_purge(self.interface, + # self.mon_type, + # self.valid_file, + # self.channels)) + asyncio.run(self.mac_revealer(self.interface, + self.mon_type, + self.valid_file, + self.channels)) + forever_wait = threading.Event() + forever_wait.wait() # ----------------------------------------------------------------------------- @@ -613,11 +757,21 @@ def get_df(): return scan_df -def get_log(log_file): +def signal_handler(signal, frame) -> None: + print('You pressed Ctrl+C!') + log.info('Shutting down') + log.info('Going Down!!') + exit(0) + + +def get_log(log_file, log_level): logfile = os.path.abspath(log_file) log = logging.getLogger('scapy.runtime') if log.hasHandlers(): log.handlers.clear() + # lvl = str(f'logging.{log_level}') + # lgvl = lvl.strip() + # log.setLevel(lgvl) log.setLevel(logging.DEBUG) handler = logging.FileHandler(logfile, mode='a', encoding='utf-8') formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') @@ -648,7 +802,7 @@ def process_args(args: argparse.Namespace) -> None: None """ global log - log = get_log(args.log_file) + log = get_log(args.log_file, args.log_level) log.info('Started crouching tiger') log.info('Started logger...') match args.module: @@ -660,6 +814,8 @@ def process_args(args: argparse.Namespace) -> None: #mon_dev, mon_type, valid_file, channels #mon_dev, mon_type, valid_file, channels log.debug('args: {0}'.format(args)) + global valid_file + valid_file = args.valid_file pge = Purge(interface=args.name, mon_type=args.mon_type, valid_file=args.valid_file, diff --git a/ctiger_study.ipynb b/ctiger_study.ipynb index a229105..26d9142 100644 --- a/ctiger_study.ipynb +++ b/ctiger_study.ipynb @@ -1963,6 +1963,117 @@ "DEBUG: Invalid monitor type\n", "```" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom Scapy Sessions\n", + "\n", + "One can configure their own scapy session, " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# SPDX-License-Identifier: GPL-2.0-only\n", + "# This file is part of Scapy\n", + "# See https://scapy.net/ for more information\n", + "\n", + "\"\"\"\n", + "Sessions: decode flow of packets when sniffing\n", + "\"\"\"\n", + "\n", + "from collections import defaultdict\n", + "import socket\n", + "import struct\n", + "\n", + "from scapy.compat import orb\n", + "from scapy.config import conf\n", + "from scapy.packet import NoPayload, Packet\n", + "from scapy.pton_ntop import inet_pton\n", + "\n", + "# Typing imports\n", + "from typing import (\n", + " Any,\n", + " DefaultDict,\n", + " Dict,\n", + " Iterator,\n", + " List,\n", + " Optional,\n", + " Tuple,\n", + " cast,\n", + " TYPE_CHECKING,\n", + ")\n", + "from scapy.compat import Self\n", + "if TYPE_CHECKING:\n", + " from scapy.supersocket import SuperSocket\n", + "\n", + "\n", + "# You will only need to extend the Default Session\n", + "class DefaultSession(object):\n", + " \"\"\"Default session: no stream decoding\"\"\"\n", + "\n", + " def __init__(self, supersession: Optional[Self] = None):\n", + " if supersession and not isinstance(supersession, DefaultSession):\n", + " supersession = supersession()\n", + " self.supersession = supersession\n", + "\n", + " def process(self, pkt: Packet) -> Optional[Packet]:\n", + " \"\"\"\n", + " Called to pre-process the packet\n", + " \"\"\"\n", + " # Optionally handle supersession\n", + " if self.supersession:\n", + " return self.supersession.process(pkt)\n", + " return pkt\n", + "\n", + " def recv(self, sock: 'SuperSocket') -> Iterator[Packet]:\n", + " \"\"\"\n", + " Will be called by sniff() to ask for a packet\n", + " \"\"\"\n", + " pkt = sock.recv()\n", + " if not pkt:\n", + " return\n", + " pkt = self.process(pkt)\n", + " if pkt:\n", + " yield pkt\n", + " \n", + " \n", + "class Dot11Session(DefaultSession):\n", + " \"\"\"Decode Dot11 packets 'on-the-flow'.\n", + "\n", + " Usage:\n", + " >>> sniff(session=Dot11Session)\n", + " \"\"\"\n", + "\n", + " def __init__(self, *args, **kwargs):\n", + " # type: (*Any, **Any) -> None\n", + " DefaultSession.__init__(self, *args, **kwargs)\n", + " self.keys = {} # type: Dict[Tuple[Any, ...], List[Packet]] # noqa: E501\n", + "\n", + "\n", + "class IPSession(DefaultSession):\n", + " \"\"\"Defragment IP packets 'on-the-flow'.\n", + "\n", + " Usage:\n", + " >>> sniff(session=IPSession)\n", + " \"\"\"\n", + "\n", + " def __init__(self, *args, **kwargs):\n", + " # type: (*Any, **Any) -> None\n", + " DefaultSession.__init__(self, *args, **kwargs)\n", + " self.fragments = defaultdict(list) # type: DefaultDict[Tuple[Any, ...], List[Packet]] # noqa: E501\n", + "\n", + " def process(self, packet: Packet) -> Optional[Packet]:\n", + " from scapy.layers.inet import IP, _defrag_ip_pkt\n", + " if IP not in packet:\n", + " return packet\n", + " return _defrag_ip_pkt(packet, self.fragments)[1] # type: ignore\n" + ] } ], "metadata": {