Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.log
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ I decided to build this simple python script using scapy so that I could record

```
usage: probemon.py [-h] [-i INTERFACE] [-t TIME] [-o OUTPUT] [-b MAX_BYTES]
[-c MAX_BACKUPS] [-d DELIMITER] [-f] [-s]
[-c MAX_BACKUPS] [-d DELIMITER] [-f] [-s] [-r] [-D] [-l]
[-x MQTT_BROKER] [-u MQTT_USER] [-p MQTT_PASSWORD]
[-m MQTT_TOPIC] [-P FILENAME]

a command line tool for logging 802.11 probe request frames

Expand All @@ -16,8 +18,6 @@ optional arguments:
-i INTERFACE, --interface INTERFACE
capture interface
-t TIME, --time TIME output time format (unix, iso)
-o OUTPUT, --output OUTPUT
logging output location
-b MAX_BYTES, --max-bytes MAX_BYTES
maximum log size in bytes before rotating
-c MAX_BACKUPS, --max-backups MAX_BACKUPS
Expand All @@ -26,6 +26,36 @@ optional arguments:
output field delimiter
-f, --mac-info include MAC address manufacturer
-s, --ssid include probe SSID in output
-l, --log enable live scrolling view of the logfile
-r, --rssi include rssi in output
-D, --debug enable debug output
-l, --log enable scrolling live view of the logfile
-P FILENAME, --pid FILENAME
save PID to file
-x MQTT_BROKER, --mqtt-broker MQTT_BROKER
mqtt broker server
-u MQTT_USER, --mqtt-user MQTT_USER
mqtt user
-p MQTT_PASSWORD, --mqtt-password MQTT_PASSWORD
mqtt password
-m MQTT_TOPIC, --mqtt-topic MQTT_TOPIC
mqtt topic
```

## systemd Service-File Example

```
[Unit]
Description=Probemon MQTT Service

[Service]
PIDFile=/run/probemon.pid
RemainAfterExit=no
Restart=on-failure
RestartSec=5s
ExecStart=/root/python/probemon/probemon.py -i mon0 --mac-info --ssid --rssi --mqtt-broker IP --mqtt-user USERNAME --mqtt-password PASSWORD --mqtt-topic TOPIC --pid /run/probemon.pid
StandardOutput=null

[Install]
WantedBy=multi-user.target
Alias=probemon.servic
```
257 changes: 179 additions & 78 deletions probemon.py
Original file line number Diff line number Diff line change
@@ -1,96 +1,197 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-

import time
import datetime
from logging.handlers import RotatingFileHandler
from pprint import pprint
from scapy.all import *
import argparse
import time
from datetime import datetime
import netaddr
import os
import sys
import time
import paho.mqtt.client as mqtt
import json
import struct
import logging
from scapy.all import *
from pprint import pprint
from logging.handlers import RotatingFileHandler
logging.getLogger("scapy.runtime").setLevel(logging.ERROR)


NAME = 'probemon'
DESCRIPTION = "a command line tool for logging 802.11 probe request frames"

DEBUG = False

def build_packet_callback(time_fmt, logger, delimiter, mac_info, ssid, rssi):
def packet_callback(packet):

if not packet.haslayer(Dot11):
return

# we are looking for management frames with a probe subtype
# if neither match we are done here
if packet.type != 0 or packet.subtype != 0x04:
return

# list of output fields
fields = []

# determine preferred time format
log_time = str(int(time.time()))
if time_fmt == 'iso':
log_time = datetime.datetime.now().isoformat()
client = mqtt.Client()
sensor_data = {'macaddress': "", 'time': "", 'make': "", 'ssid': "", 'rssi': 0}

fields.append(log_time)

# append the mac address itself
fields.append(packet.addr2)

# parse mac address and look up the organization from the vendor octets
if mac_info:
try:
parsed_mac = netaddr.EUI(packet.addr2)
fields.append(parsed_mac.oui.registration().org)
except netaddr.core.NotRegisteredError, e:
fields.append('UNKNOWN')
DEBUG = False

# include the SSID in the probe frame
if ssid:
fields.append(packet.info)

if rssi:
rssi_val = -(256-ord(packet.notdecoded[-4:-3]))
fields.append(str(rssi_val))

logger.info(delimiter.join(fields))
def parse_rssi(packet):
# parse dbm_antsignal from radiotap header
# borrowed from python-radiotap module
radiotap_header_fmt = '<BBHI'
radiotap_header_len = struct.calcsize(radiotap_header_fmt)
version, pad, radiotap_len, present = struct.unpack_from(
radiotap_header_fmt, packet)

start = radiotap_header_len
bits = [int(b) for b in bin(present)[2:].rjust(32, '0')]
bits.reverse()
if bits[5] == 0:
return 0

while present & (1 << 31):
present, = struct.unpack_from('<I', packet, start)
start += 4
offset = start
if bits[0] == 1:
offset = (offset + 8 - 1) & ~(8 - 1)
offset += 8
if bits[1] == 1:
offset += 1
if bits[2] == 1:
offset += 1
if bits[3] == 1:
offset = (offset + 2 - 1) & ~(2 - 1)
offset += 4
if bits[4] == 1:
offset += 2
dbm_antsignal, = struct.unpack_from('<b', packet, offset)
return dbm_antsignal


def build_packet_callback(time_fmt, logger, delimiter, mac_info, ssid, rssi, mqtt_topic):
def packet_callback(packet):

global sensor_data
global client

# we are looking for management frames with a probe subtype
# if neither match we are done here
if packet.type != 0 or packet.subtype != 0x04 or packet.type is None:
return

# list of output fields
fields = []

# determine preferred time format
log_time = str(int(time.time()))
if time_fmt == 'iso':
log_time = datetime.now().isoformat()

fields.append(log_time)

# append the mac address itself
fields.append(packet.addr2)

# parse mac address and look up the organization from the vendor octets
if mac_info:
try:
parsed_mac = netaddr.EUI(packet.addr2)
mac_make = parsed_mac.oui.registration().org
except netaddr.core.NotRegisteredError as e:
mac_make = 'UNKNOWN'
fields.append(mac_make)

# include the SSID in the probe frame
if ssid:
fields.append(packet.info.decode(encoding='utf-8', errors='replace'))

if rssi:
if sys.version_info > (3,):
rssi_val = parse_rssi(memoryview(bytes(packet)))
else:
rssi_val = parse_rssi(buffer(str(packet)))

fields.append(str(rssi_val))
else:
rssi_val = 0

sensor_data['macaddress'] = packet.addr2
sensor_data['time'] = log_time
sensor_data['make'] = mac_make
sensor_data['ssid'] = packet.info.decode(encoding='utf-8', errors='replace')
sensor_data['uppercaseSSID'] = packet.info.decode(encoding='utf-8', errors='replace').upper()
sensor_data['rssi'] = rssi_val

logger.info(delimiter.join(fields))

client.publish(mqtt_topic, json.dumps(sensor_data), 1)
return packet_callback

return packet_callback

def main():
parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument('-i', '--interface', help="capture interface")
parser.add_argument('-t', '--time', default='iso', help="output time format (unix, iso)")
parser.add_argument('-o', '--output', default='probemon.log', help="logging output location")
parser.add_argument('-b', '--max-bytes', default=5000000, help="maximum log size in bytes before rotating")
parser.add_argument('-c', '--max-backups', default=99999, help="maximum number of log files to keep")
parser.add_argument('-d', '--delimiter', default='\t', help="output field delimiter")
parser.add_argument('-f', '--mac-info', action='store_true', help="include MAC address manufacturer")
parser.add_argument('-s', '--ssid', action='store_true', help="include probe SSID in output")
parser.add_argument('-r', '--rssi', action='store_true', help="include rssi in output")
parser.add_argument('-D', '--debug', action='store_true', help="enable debug output")
parser.add_argument('-l', '--log', action='store_true', help="enable scrolling live view of the logfile")
args = parser.parse_args()

if not args.interface:
print "error: capture interface not given, try --help"
sys.exit(-1)

DEBUG = args.debug

# setup our rotating logger
logger = logging.getLogger(NAME)
logger.setLevel(logging.INFO)
handler = RotatingFileHandler(args.output, maxBytes=args.max_bytes, backupCount=args.max_backups)
logger.addHandler(handler)
if args.log:
logger.addHandler(logging.StreamHandler(sys.stdout))
built_packet_cb = build_packet_callback(args.time, logger,
args.delimiter, args.mac_info, args.ssid, args.rssi)
sniff(iface=args.interface, prn=built_packet_cb, store=0)
global topic

parser = argparse.ArgumentParser(description=DESCRIPTION)
parser.add_argument('-i', '--interface', help="capture interface")
parser.add_argument('-t', '--time', default='iso',
help="output time format (unix, iso)")
parser.add_argument('-b', '--max-bytes', default=5000000,
help="maximum log size in bytes before rotating")
parser.add_argument('-c', '--max-backups', default=99999,
help="maximum number of log files to keep")
parser.add_argument('-d', '--delimiter', default='\t',
help="output field delimiter")
parser.add_argument('-f', '--mac-info', action='store_true',
help="include MAC address manufacturer")
parser.add_argument('-s', '--ssid', action='store_true',
help="include probe SSID in output")
parser.add_argument('-r', '--rssi', action='store_true',
help="include rssi in output")
parser.add_argument('-D', '--debug', action='store_true',
help="enable debug output")
parser.add_argument('-l', '--log', action='store_true',
help="enable scrolling live view of the logfile")
parser.add_argument('-x', '--mqtt-broker', default='',
help="mqtt broker server")
parser.add_argument('-o', '--mqtt-port', default='1883',
help="mqtt broker port")
parser.add_argument('-u', '--mqtt-user', default='', help="mqtt user")
parser.add_argument('-p', '--mqtt-password',
default='', help="mqtt password")
parser.add_argument('-m', '--mqtt-topic',
default='probemon/request', help="mqtt topic")
parser.add_argument('-P', '--pid', default='', help="PID File")
args = parser.parse_args()

if not args.interface:
print("error: capture interface not given, try --help")
sys.exit(-1)

DEBUG = args.debug

# PID speichern
if args.pid:
if os.path.isfile(args.pid):
print("%s already exists, exiting" % args.pid)
sys.exit()

pid = str(os.getpid())
open(args.pid, 'w').write(pid)

try:
if args.mqtt_user and args.mqtt_password:
client.username_pw_set(args.mqtt_user, args.mqtt_password)

if args.mqtt_broker:
client.connect(args.mqtt_broker, int(args.mqtt_port), 1)
client.loop_start()

# setup our rotating logger
logger = logging.getLogger(NAME)
logger.setLevel(logging.INFO)
if args.log:
logger.addHandler(logging.StreamHandler(sys.stdout))
built_packet_cb = build_packet_callback(args.time, logger,
args.delimiter, args.mac_info, args.ssid, args.rssi, args.mqtt_topic)
sniff(iface=args.interface, prn=built_packet_cb, store=0, monitor=True)
finally:
# Remove PID File on Exit
if args.pid:
os.unlink(args.pid)


if __name__ == '__main__':
main()
main()