#!/usr/bin/perl # WifiGeiger is a war-walking tool that helps you hone in on the exact location # of an access point based on auditory feedback, much like a Geiger counter. It # allows you to covertly explore a neighborhood wearing only earbuds and a # backpack (containing a laptop). use strict; use warnings; use Data::Dumper; # process command-line args if (@ARGV<3) { (my $zero = $0) =~ s#^.*/##; die "$zero [device] [BSSID] [channel]\n"; } my ($device, $bssid, $channel) = @ARGV; # kick everything off my $synth = new WifiGeiger::Synthesizer(); my $netmon = new WifiGeiger::NetworkMonitor($device, $bssid, $channel, $synth); #while (1) { $netmon->check_if_any_new_input() } $synth->main_loop($netmon); ############################################################################### #### NetworkMonitor ########################################################### ############################################################################### package WifiGeiger::NetworkMonitor; use strict; use warnings; use Symbol qw[ gensym ]; use Data::Dumper; use Time::HiRes qw[time]; use List::Util; sub new { my $class = shift; my ($device, $bssid, $channel, $synthesizer) = @_; # set the channel# system "iwconfig '$device' channel $channel"; my $tshark = Symbol::gensym(); open $tshark, "tshark -i '$device' -l -R wlan_mgt.fixed.beacon -T fields -e frame.time_relative -e prism.signal.data -e wlan.bssid |" or die "Unable to run 'tshark': $!\n"; my $self = { device => $device, bssid => $bssid, synthesizer => $synthesizer, BufferedSelect => new IO::BufferedSelect($tshark), #weighted_avg => 0, recent_packets => [], min_power => 999999, max_power => 0, min_numpackets => 99999, max_numpackets => 0, }; return bless $self, $class; } # This must return relatively quickly, within a few hundred milliseconds, # otherwise it will hold up the main loop. # # If there's any packet data available, then process it, otherwise return. sub check_if_any_new_input { my $self = shift; my $soundcard_time = shift; # in seconds (floating-point) foreach my $line (map {$_->[1]} $self->{BufferedSelect}->read_line(0)) { $line =~ s/[\n\r]+$//s; # super-chomp my @fields = split /\t/, $line; my $wifi_arrival_time = $fields[0]; # in seconds (floating point) my $power = hex($fields[1]); my $bssid = $fields[2]; if (lc($bssid) eq lc($self->{bssid})) { $self->positive_hit($soundcard_time, $wifi_arrival_time, $power); } } } # We got a packet that we were looking for. sub positive_hit { my $self = shift; my $soundcard_time = shift; # in seconds (floating-point) my $wifi_arrival_time = shift; # in seconds (floating-point) my $power = shift; ## At this point, $wifi_arrival_time may be up to 1.0 second ## old (possibly a little older, but I haven't seen that on my ## machine) due to the sound-card driver's bursty blocking. ## ## So what we'll do is average all packets received over the ## last 1.5 seconds, and output our audio based on that. ## ## Fortunately, the packet's timestamp is the same as the system ## time, so we know exactly how old each packet is. At least ## we're able to rely on THAT. ## The time that we average over isn't too important. It could ## be the same as the soundcard-lag, it could even be less. ## What's really important though, is that we can't just play a ## single tone for each incoming packet, otherwise they would ## all pile up on top of each other right after the soundcard ## lag-spike. my $system_time = Time::HiRes::time(); my $packet_how_old = $system_time - $wifi_arrival_time; push(@{$self->{recent_packets}}, [$wifi_arrival_time, $power]); # remove any packets from the list that are too old $self->{recent_packets} = [grep {($system_time - $_->[0]) <= 1.5} @{$self->{recent_packets}}]; my $num_packets = scalar(@{$self->{recent_packets}}); my $sum_power = List::Util::sum( map {$_->[1]} @{$self->{recent_packets}} ); my $mean_power = $sum_power / $num_packets; #$self->{min_power} = $mean_power if ($mean_power < $self->{min_power}); #$self->{max_power} = $mean_power if ($mean_power > $self->{max_power}); $self->{min_power} = $sum_power if ($sum_power < $self->{min_power}); $self->{max_power} = $sum_power if ($sum_power > $self->{max_power}); $self->{min_numpackets} = $num_packets if ($num_packets < $self->{min_numpackets}); $self->{max_numpackets} = $num_packets if ($num_packets > $self->{max_numpackets}); # if ($self->{weighted_avg} == 0) { # $self->{weighted_avg} = $power; # } else { # $self->{weighted_avg} = 0.8 * $self->{weighted_avg} + 0.2 * $power; # } #my $rate = map_range($mean_power, my $rate = map_range($sum_power, $self->{min_power}, $self->{max_power}, 1, 0.06); my $volume = map_range($num_packets, $self->{min_numpackets}, $self->{max_numpackets}, 0.1, 0.95); $self->{synthesizer}->repeat_note(0, $volume, $rate); printf "%7.3f %5d %7.3f -- %4.2f %4.2f\n", $packet_how_old, $num_packets, $mean_power, $rate, $volume; #$self->{synthesizer}->play_new_note(0, ($power / $self->{weighted_avg}) * .19); } # given one min-max range, and a second min-max range, and a value that's # part of the first range, find the analogous value in the second range sub map_range { my $range1_value = shift; my $range1_min = shift; my $range1_max = shift; my $range2_min = shift; my $range2_max = shift; my $range1_span = $range1_max - $range1_min; my $range2_span = $range2_max - $range2_min; if ($range1_span == 0) { # only option in this case is to pick an arbitrary number return ($range2_min + $range2_max) / 2; } my $range12_percentage = ($range1_value - $range1_min) / $range1_span; my $range2_value = $range12_percentage * $range2_span + $range2_min; return $range2_value; } ############################################################################### #### Synthesizer ############################################################## ############################################################################### # this script contains its own minimal synthesizer so that it can # operate in environments where one isn't available (e.g. BackTrack 3) # # Yup, this eats a fair bit of CPU, but it gets the job done. package WifiGeiger::Synthesizer; use Symbol qw[ gensym ]; use Time::HiRes qw[time]; sub pi {3.1415926} sub new { my $class = shift; my $self = { }; # /dev/dsp is the audio device in Linux. It generally is associated # with oss or open sound system drivers. There is also alsa which can # provide /dev/dsp. $self->{dev_dsp} = gensym(); open $self->{dev_dsp}, ">/dev/dsp" or die "Unable to write to /dev/dsp: $!\n"; # default /dev/dsp settings are unsigned 8-bit, 8kHz $self->{samplerate} = 8000; $self = bless($self, $class); # synthesize the notes, and cache them in memory $self->{note_waveforms} = [ $self->synthesize_single_note(440, 0.001), #synthesize_single_note(880, 0.001), ]; #while (1) { samples_out($note_waveforms[0]); } return $self; } # tell the synthesizer to repeat a note at the specified interval sub repeat_note { my $self = shift; my $note_id = shift; my $amplitude = shift; my $repeat_every = shift; # in floating-point seconds $self->{repeat_note} = $note_id; $self->{repeat_amplitude} = $amplitude; $self->{repeat_every} = $repeat_every * $self->{samplerate}; # in number of samples if (!defined($self->{last_repeated})) { $self->{last_repeated} = 0; } } sub stop_repeating_note { my $self = shift; delete $self->{last_repeated}; } # add a new note to the queue sub play_new_note { my $self = shift; my $note_id = shift; # an index into the 'note_waveforms' array -- if you need to play a new note, have another created for you up there my $amplitude = shift; push(@{$self->{active_notes}}, {time_started => $self->{current_time}, note=>$note_id, amplitude=>$amplitude}); } sub main_loop { my $self = shift; my $networkmonitor = shift; # main loop: # - multiple notes are added together as needed # - Blocking here is actually useful. It lets the soundcard driver TELL US what time it is. We don't have to worry about synchronization. # http://manuals.opensound.com/developer/audio_timing.html # - We check to see if there's any new input to process, after outputing every sample. $self->{active_notes} = []; $self->{current_time} = 0; # here, "time" is counted in number of samples that we've output so far #my $last_played = 0; my $time_offset_in_seconds = 0; # this accumulates every time we reset the per-sample clock back to zero for (;;) { #my $now = time(); #if ($now - $last_played > 0.25) { # start a new note every 0.25 seconds # if ($self->{current_time} - $last_played > 0.25 * $self->{samplerate}) { # start a new note every 0.25 seconds # $self->play_new_note( # 0, # note_id # 0.8); # amplitude # $last_played = $self->{current_time}; # # #print Dumper {current_time => $current_time, active_notes => \@active_notes}; # } ####### VVVVVVV new-note generation VVVVVVVVV ######## my $seconds_elapsed = $time_offset_in_seconds + $self->{current_time} / $self->{samplerate}; $networkmonitor->check_if_any_new_input($seconds_elapsed); if (defined($self->{last_repeated})) { if ($self->{current_time} - $self->{last_repeated} > $self->{repeat_every}) { $self->play_new_note( $self->{repeat_note}, $self->{repeat_amplitude} ); $self->{last_repeated} = $self->{current_time}; } } ####### ^^^^^^^ new-note generation ^^^^^^^^^ ######## # add together the waveforms for all the notes we're currently playing my $sample_to_output = 0; for (my $ctr=0; $ctr{active_notes}}); $ctr++) { my $note = $self->{active_notes}[$ctr]; my $sample_num = $self->{current_time} - $note->{time_started}; # have we finished playing this specific note? if ($sample_num >= scalar(@{ $self->{note_waveforms}[ $note->{note} ] })) { splice(@{$self->{active_notes}}, $ctr, 1); # remove this one from the list of active notes $ctr--; next; } $sample_to_output += $note->{amplitude} * $self->{note_waveforms}[ $note->{note} ][$sample_num]; } # this blocks until the opensound.com driver says to compute the next sample my $time_before = time(); $self->sample_out($sample_to_output); my $time_difference = time() - $time_before; if ($time_difference > 0.01) { printf "--- sound driver blocked for: %5.4f\n", $time_difference; $|++; } # increment time $self->{current_time}++; if ($self->{current_time} >65535) { # re-set the clock to zero every once in a while, and change all the note's times too $self->{current_time} -= 65535; foreach my $note (@{$self->{active_notes}}) { $note->{time_started} -= 65535; } #$last_played -= 65535; $time_offset_in_seconds += 65535 / $self->{samplerate}; if (defined($self->{last_repeated})) { $self->{last_repeated} -= 65535; } } } } # Synthesize a single note. This gets cached to memory, not directly played, so we return an array. sub synthesize_single_note { my $self = shift; my $freq = shift; # Hz my $falloff = shift; # defines the envelope. This is the exponent (values of 0.01 to 0.001 are good) my $period = $self->{samplerate} / $freq; my @samples; for (my $ctr=0; ; $ctr++) { my $amp = 127 * $falloff ** ($ctr / $self->{samplerate}); # hard-code for a maximum amplitude of 127 push(@samples, sin(2*&pi * $ctr/$period) * $amp + sin(2*&pi * $ctr/(2*$period)) * $amp*0.20 + sin(2*&pi * $ctr/(4*$period)) * $amp*0.10 + sin(2*&pi * $ctr/(8*$period)) * $amp*0.05 ); # stop when the exponential fall-off has resulted in the envelope going to [effectively] zero last if ($ctr > 5 && abs($samples[-1]) <= 1 && abs($samples[-2]) <= 1 && abs($samples[-3]) <= 1 && abs($samples[-4]) <= 1 && abs($samples[-5]) <= 1); } return \@samples; } # Outputs a single data-point to the sound card # takes in a -128 to 127 value (int8) sub sample_out { my $self = shift; my $sample = shift; # in case output is larger than possible $sample = $sample < -128 ? -128 : $sample > 127 ? 127 : $sample; my $fh = $self->{dev_dsp}; print $fh chr($sample+128); # change range from int8 (-128 to 127) to uint8 (0 to 255) } # output an array of samples sub samples_out { if (scalar(@_)==1 && ref($_[0])) { foreach (@{$_[0]}) { sample_out($_); } } else { foreach (@_) { sample_out($_); } } } ############################################################################### #### IO::BufferedSelect ####################################################### ############################################################################### # copy-n-pasted from http://search.cpan.org/dist/IO-BufferedSelect/lib/IO/BufferedSelect.pm package IO::BufferedSelect; use strict; use warnings; use IO::Select; sub new($@) { my $class = shift; my @handles = @_; my $self = { handles => \@handles, buffers => [ map { '' } @handles ], eof => [ map { 0 } @handles ], selector => new IO::Select( @handles ) }; return bless $self; } sub read_line($;$@) { my $self = shift; my ($timeout, @handles) = @_; # Convert @handles to a "set" of indices my %use_idx = (); if(@handles) { foreach my $idx( 0..$#{$self->{handles}} ) { $use_idx{$idx} = 1 if grep { $_ == $self->{handles}->[$idx] } @handles; } } else { $use_idx{$_} = 1 foreach( 0..$#{$self->{handles}} ); } for( my $is_first = 1 ; 1 ; $is_first = 0 ) { # If we have any lines in buffers, return those first my @result = (); foreach my $idx( 0..$#{$self->{handles}} ) { next unless $use_idx{$idx}; if($self->{buffers}->[$idx] =~ s/(.*\n)//) { push @result, [ $self->{handles}->[$idx], $1 ]; } elsif($self->{eof}->[$idx]) { # NOTE: we discard any unterminated data at EOF push @result, [ $self->{handles}->[$idx], undef ]; } } # Only give it one shot if $timeout is defined return @result if ( @result or (defined($timeout) and !$is_first) ); # Do a select(), optionally with a timeout my @ready = $self->{selector}->can_read( $timeout ); # Read into $self->{buffers} foreach my $fh( @ready ) { foreach my $idx( 0..$#{$self->{handles}} ) { next unless $fh == $self->{handles}->[$idx]; next unless $use_idx{$idx}; my $bytes = sysread $fh, $self->{buffers}->[$idx], 1024, length $self->{buffers}->[$idx]; $self->{eof}->[$idx] = 1 if($bytes == 0); } } } }