-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwifimap
executable file
·298 lines (258 loc) · 11.3 KB
/
wifimap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/usr/bin/env python
"""
Sniffs on the WiFi network and generates a graph showing the communication
between the devices, including information about the announced SSIDs. Will
loop infinitely - kill the program with SIGINT in order to stop it
gracefully. You can also use SIGUSR1 to force the generation of the graph
without killing the program - useful for the periodic generation of the
visuals.
Example usage (the "timeout" kills wifimap after 5s):
timeout --foreground -s INT 5s wifimap | dot -Tsvg > out.svg
The example assumes that you saved this script as "wifimap" with rights to
being executed somewhere to your $PATH.
TODO:
* make channel hopping more intelligent, staying longer on better channels
* split wifimap into sniffing and graphing programs, maybe also ch-hopping
* verify if there's a bug related to 'Announces' being printed with wrong
source
* maybe keep stats about the connection recency and allow to only display
connections from last N seconds?
* detect whether wlan0 or wlp3s0 should be used by default
Requires "tshark" application in PATH, which is provided by Wireshark. Also,
a Python library "lxml" needs to be installed.
tshark also needs to grant sniffing privileges without running as root for
the user running it - you can add these by calling the following command:
sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip /usr/sbin/tshark
NOTE: This will only work if your Wi-Fi card supports sniffing.
Author: Jacek Wielemborek, licensed under WTFPL
"""
import subprocess
import sys
import time
import re
import threading
from StringIO import StringIO
from lxml import etree
from collections import defaultdict
import signal
def can_do_sudo():
"""Tells whether we can run sudo without being asked for password."""
sudo_true_p = subprocess.Popen("SUDO_ASKPASS=/usr/bin/false sudo -A true",
shell=True)
sudo_true_p.communicate()
return sudo_true_p.returncode == 0
class ChannelHopper(threading.Thread):
"""A channel hopper thread. Switches the Wireless channel periodically."""
def __init__(self, interval, iface):
"""
Constructs a ChannelHopper instance.
Args:
interval - the time between channel switches
iface - the interface to perform switching on
"""
threading.Thread.__init__(self)
self.running = True
self.interval = interval
self.iface = iface
def get_frequency(self):
"""Returns the current frequency of the interface."""
iwconfig_output = subprocess.check_output(["iwconfig", self.iface])
return re.findall('Frequency:([^ ]+)', iwconfig_output)[0]
def run(self):
if not can_do_sudo():
sys.stderr.write("Could not run passwordless sudo - channel hopper"
" skipped.\n")
return
else:
sys.stderr.write("Channel hopper started.\n")
start = self.get_frequency()
channel = 1
try:
while self.running:
retcode = subprocess.call(['sudo', '-A',
'iwconfig', self.iface, 'channel',
str(channel)],
stderr=subprocess.PIPE)
if retcode != 0:
channel = 0
time.sleep(self.interval)
channel += 1
finally:
subprocess.call(['sudo', '-A',
'iwconfig', self.iface, 'freq', start])
end = self.get_frequency()
if start != end:
sys.stderr.write("WTF: Could not restore the "
"frequency (%s vs %s)\n" % (start, end))
else:
sys.stderr.write("Channel hopper stopped.\n")
class Wifimap(object):
def __init__(self):
self.seen = defaultdict(lambda: defaultdict(int))
self.announces = defaultdict(list)
def parse_packet(self, packet_str):
"""
Parses a <packet></packet> XML string, returning information about the
sender, receiver and the announced networks. If sender or receiver is
not known, returns '?' in its place. If SSID is not announced, None
is returned.
"""
packet_dict = {}
ssid = None
for line in packet_str.split("\n"):
if '"wlan.ra"' in line or '"wlan.ta"' in line \
or '"wlan.sa"' in line:
field = etree.fromstring(line)
if 'ff:ff:ff:ff:ff:ff' in field.get('showname'):
continue
name = field.get('name')
packet_dict[name] = field.get('showname').split(': ')[1]
packet_dict[name] = packet_dict[name].replace(' ', '\\n')
if '"wlan_mgt.ssid"' in line:
field = etree.fromstring(line)
if ssid is not None and field.get('show') != ssid:
sys.stderr.write("WTF: SSID: %s vs %s" % (repr(ssid),
repr(field.get('show'))))
ssid = field.get('show')
if packet_dict.get('wlan.ta') != packet_dict.get('wlan.sa') \
and packet_dict.get('wlan.ta') is not None \
and packet_dict.get('wlan.sa') is not None:
sys.stderr.write("WTF: ta=%s != sa=%s\n" %
(repr(packet_dict.get('wlan.ta')),
repr(packet_dict.get('wlan.sa'))))
to_mac = packet_dict.get('wlan.ra', '?') \
if packet_dict.get('wlan.ra') != 'ffffffffffff' else '?'
from_mac = packet_dict.get('wlan.ta', '?') or \
packet_dict.get('wlan.sa', '?')
return from_mac, to_mac, ssid
def handle_packet(self, from_mac, to_mac, ssid):
"""
Handles information about noticing a given packet in order to prepare
it for reporting.
"""
self.seen[from_mac][to_mac] += 1
if ssid:
found = self.announces[from_mac]
if len(found) != 0 and ssid not in found:
sys.stderr.write('WTF: two ssids: %s, %s, %s\n' %
(from_mac, ssid, found))
if ssid not in found:
self.announces[from_mac] += [ssid]
def print_report(self, skip_broadcast=False):
"""
Prints out a DOT file based on the gathered information.
"""
print("strict digraph {")
for from_mac in self.seen:
for to_mac in self.seen[from_mac]:
if skip_broadcast and (from_mac == '?' or to_mac == '?'):
continue
from_mac_display = from_mac
if from_mac in self.announces:
from_mac_display += '\\nAnnounces: '
from_mac_display += ',\\n'.join(self.announces[from_mac])
to_mac_display = to_mac
if to_mac in self.announces:
to_mac_display += '\\nAnnounces: '
to_mac_display += ',\\n'.join(self.announces[to_mac])
print('"%s" -> "%s";' % (from_mac_display, to_mac_display))
print("}")
def read_from_file(self, infile, outfile=None):
"""
Reads the output of tshark -T pdml. If outfile is specified,
the information is also saved to the outfile.
"""
packet = StringIO()
while True:
line = infile.readline()
if line == '':
break
packet.write(line)
if outfile:
outfile.write(line)
if '</packet>' in line:
packet_str = packet.getvalue()
packet_info = self.parse_packet(packet_str)
self.handle_packet(*packet_info)
packet = StringIO()
def get_dump_wifimap(wmap, skip_broadcast):
"""
Returns a closure that is supposed to work as a signal handler. It can be
used when SIGUSR1 is received to force the generation of the report at the
given time.
Args:
wmap - a Wifimap instance that will be used for printing the report
skip_broadcast - a boolean value telling whether broadcasts should not
be reported
"""
def dump_wifimap(*args, **kwargs):
# If sys.stdout is redirected to a file (as opposed to the terminal),
# this will make wifimap rewrite it.
try:
sys.stdout.seek(0)
except IOError:
pass
wmap.print_report(skip_broadcast)
sys.stdout.flush()
return dump_wifimap
def main():
from argparse import ArgumentParser, RawTextHelpFormatter, FileType
parser = ArgumentParser(description=__doc__,
formatter_class=RawTextHelpFormatter)
parser.add_argument('--infile', help='file to read the PDML data'
' from instead of sniffing (implies'
' --no-channel-hop)', type=FileType('r'))
parser.add_argument('--outfile', help='file to save the a copy of PDML'
' data to while sniffing', type=FileType('w'))
parser.add_argument('--skip-broadcast', action='store_true', help='do not'
' draw broadcast connections - this will remove some'
' results')
parser.add_argument('--no-channel-hop', action='store_true', help='do not'
' attempt channel hopping even if possible - might'
' give more results')
parser.add_argument('--channel-hop-interval', type=float, default=5,
help='channel hopping interval in seconds'
' (default: 5.0)')
parser.add_argument('--iface', default='wlp3s0', help='name of the WLAN'
' interface to perform sniffing and hopping on'
' (default: wlp3s0)')
parser.add_argument('--justhop', action='store_true', help="don't do any"
"sniffing, just run the channel hopper")
args = parser.parse_args()
if not args.no_channel_hop:
ch_hopper = ChannelHopper(interval=args.channel_hop_interval,
iface=args.iface)
ch_hopper.start()
if args.justhop:
try:
while True:
time.sleep(1)
finally:
ch_hopper.running = False
ch_hopper.join()
tshark_p = None
if args.infile:
args.no_channel_hop = True
else:
try:
tshark_p = subprocess.Popen(["tshark", "-i", args.iface,
"-I", "-y", "IEEE802_11_RADIO",
"-T", "pdml"],
stdout=subprocess.PIPE)
except OSError:
sys.exit("ERROR: Attempt to call tshark failed. "
"Have you installed Wireshark? Is tshark in your $PATH?")
args.infile = tshark_p.stdout
wmap = Wifimap()
signal.signal(signal.SIGUSR1, get_dump_wifimap(wmap, args.skip_broadcast))
try:
wmap.read_from_file(args.infile, args.outfile)
except KeyboardInterrupt:
pass
finally:
wmap.print_report(args.skip_broadcast)
if not args.no_channel_hop:
ch_hopper.running = False
ch_hopper.join()
if __name__ == '__main__':
main()