#!/usr/bin/perl # Grouch.pl - by Jeff Jarmoc - 09/07/2009 # # A heavily modified version of snacsnatcher.pl (http://cpansearch.perl.org/src/MATTHEWG/Net-OSCAR-1.925/snacsnatcher) # Created for the jham corp forensics puzzle contest (http://jhamcorp.com/What%20We%27re%20Up%20To....html) use strict; use warnings; use Socket; use lib "./blib/lib"; use lib "/usr/include"; use Net::OSCAR qw(:all); use Net::OSCAR::XML; use Net::OSCAR::Utility qw(hexdump); use Net::OSCAR::Constants; use Net::Pcap; use Digest::MD5; our $session = Net::OSCAR->new(); our $init_time = undef; our $outfile = undef; our $verbose = 0; # Increasing this GREATLY increases the data that's output about an OSCAR transfer. sub BEGIN { eval { require "net/bpf.ph"; }; die "Couldn't find net/bpf.ph.\nPlease create it by doing cd /usr/include ; h2ph net/bpf.h\n$@\n" if $@; } my $file = shift or die "Usage: grouch pcapfile [-v]\n"; use vars qw($packet %buffer %bufflen %snacbuff %ft_states %seqnos $datalink @blarray); $packet = ""; #0; sub ssdump_scalar($$); sub ssdump_list($$); sub ssdump_hash($$); sub docx2txt($); sub ssdump_scalar($$) { my($val, $depth) = @_; my $hex = hexdump($val); if($hex and $hex ne $val) { if ($verbose) { print join("\n", map { ("\t" x $depth) . $_ } split(/\n/, $hex ) ), "\n"; } } else { $val ||= ""; print "$val\n" if $verbose; } } sub ssdump_list($$) { my($val, $depth) = @_; print "\t" x $depth if $verbose; foreach (@$val) { print "[\n" if $verbose; if(!ref($_)) { print "\t" x ($depth+1) if $verbose; ssdump_scalar($_, $depth); } elsif(ref($_) eq "HASH") { ssdump_hash($_, $depth+1); } elsif(ref($_) eq "ARRAY") { ssdump_list($_, $depth+1); } elsif(ref($_) eq "SCALAR") { print "\t" x ($depth+1) if $verbose; ssdump_scalar($$_, $depth+1); } else { die "Unknown reftype: " . ref($_) . "\n"; } if ($verbose) { print "\t" x $depth; print "],"; } } print "\n" if $verbose; } sub ssdump_hash($$) { my($struct, $depth) = @_; if (defined $struct->{filename}) { if ($struct->{bytes_received}) { # File transfer complete, let's take a look at the file. my $magic_number = ''; print "\nReceived file $struct->{filename} - $struct->{bytes_received} Bytes\n"; close (FILE); open (FILE, $outfile); read FILE, $magic_number, 4; close (FILE); open (FILE, $outfile); print "\tMagic Number: ", hexdump($magic_number), "\n"; print "\tMD5: ", Digest::MD5->new->addfile(*FILE)->hexdigest, "\n"; close(FILE); if ($outfile =~ /.*\.docx/) { print "\tPlain-text contents of $outfile follow:\n"; print docx2txt($outfile); print "\n"; } else { # Other file interpreters could be added print "\tCannot display file $outfile\n" } $outfile=undef; return; } else { if ($struct->{flags}) { print "\nReceiving file $struct->{filename}\n"; # Open a file handle for storing our file, die if the file exists. if (!$outfile) { $outfile = $struct->{filename}; die "$outfile exists, exiting!" if (-e $outfile); open(FILE, ">$outfile"); binmode(FILE); } } } } elsif (defined $struct->{message}) { print "\t $struct->{message} \n\n"; ssdump_hash($struct->{message_body}, $depth+1); } elsif (defined $struct->{message_body} ) { print " exchange with: $struct->{screenname}\n"; my $body = $struct->{message_body}; ssdump_hash($body, $depth+1); } else { foreach my $key (sort keys %$struct) { my $val = $struct->{$key}; if ($verbose) { print "\t" x $depth; print $key, " => "; } if(!ref($val)) { if($key =~ /ip$/ and $val =~ /^\d+$/) { my($q1, $q2, $q3, $q4) = ( ($val >> 24), (($val >> 16) & 0xFF), (($val >> 8) & 0xFF), ($val & 0xFF) ); $val = "$q1.$q2.$q3.$q4"; } elsif($key eq "capability") { $val = OSCAR_CAPS_INVERSE()->{$val} if exists(OSCAR_CAPS_INVERSE()->{$val}); } ssdump_scalar($val, $depth); } elsif(ref($val) eq "HASH") { print "\n" if $verbose; ssdump_hash($val, $depth+1); } elsif(ref($val) eq "ARRAY") { print "\n" if $verbose; if($key eq "capabilities") { @$val = map { exists(OSCAR_CAPS_INVERSE()->{$_}) ? OSCAR_CAPS_INVERSE()->{$_} : $_ } @$val; } elsif($key eq "shortcaps") { @$val = map { exists(OSCAR_CAPS_SHORT_INVERSE()->{$_}) ? OSCAR_CAPS_SHORT_INVERSE()->{$_} : $_ } @$val; } ssdump_list($val, $depth); } elsif(ref($val) eq "SCALAR") { ssdump_scalar($$val, $depth); } else { die "Unknown reftype: " . ref($val) . "\n"; } } } } sub got_packet($$$) { my($user, $hdr, $pkt) = @_; my($inaddr, $outaddr); my $tlv; my $time = $hdr->{tv_sec} . "." . $hdr->{tv_usec}; $init_time ||= $time; $time -= $init_time; $time = sprintf("%0.3f", $time); $packet++; # This removes the datalink-level headers from a packet. # You may need to adjust this - this is a very Q&D hack. # Only ethernet (DLT_EN10MB) is tested. # # These are taken from tcpdump. # if($datalink == DLT_NULL or $datalink == DLT_LOOP) { substr($pkt, 0, 4) = ""; } elsif($datalink == DLT_EN10MB or $datalink == DLT_IEEE802) { substr($pkt, 0, 14) = ""; } elsif($datalink == DLT_SLIP) { substr($pkt, 0, 16) = ""; } elsif($datalink == DLT_PPP) { substr($pkt, 0, 4) = ""; } elsif($datalink == DLT_LINUX_SLL) { substr($pkt, 0, 16) = ""; } else { die "Unsupported datalink $datalink\n"; } my($iplen, $diffserv, $totlen) = unpack("CCn", $pkt); $iplen = ($iplen&0x0F) * 4; my $src = substr($pkt, 12, 4); my $dst = substr($pkt, 16, 4); substr($pkt, 0, $iplen) = ""; #Get rid of IP headers $src = inet_ntoa($src); $dst = inet_ntoa($dst); my($src_port, $dst_port, $seqno, $ack_seq, $tcplen, $flags) = unpack("nnNNCC", $pkt); $tcplen = ($tcplen>>4)*4; substr($pkt, 0, $tcplen) = ""; return if $flags & 0x2; # SYN my $conn_key = "$src:$src_port -> $dst:$dst_port"; $buffer{$conn_key} ||= ""; $bufflen{$conn_key} ||= 0; # Ignore retransmissions $seqnos{$conn_key} ||= [undef, undef, undef, undef, undef, undef, undef, undef, undef, undef]; #return if grep {defined($_) and $_ eq $seqno} @{$seqnos{$conn_key}}; # Above cause problems for packets with the same SEQ but different length, which appear in the .pcap i'm developing against. Should probably check for retransmits some other way. shift @{$seqnos{$conn_key}}; push @{$seqnos{$conn_key}}, $seqno; PACKET: while($pkt) { return if ($totlen == $iplen + $tcplen); # Ignore packet payload if TCP/IP headers say there's no payload. print " Packet Data for $packet \n" . hexdump($pkt) . "\n" if ($verbose > 2); if($buffer{$conn_key}) { $pkt = $buffer{$conn_key} . $pkt; $buffer{$conn_key} = ""; } if($bufflen{$conn_key}) { if(length($pkt) < $bufflen{$conn_key}) { $buffer{$conn_key} = $pkt; return; } else { $bufflen{$conn_key} = 0; } } else { if(length($pkt) < $tcplen) { $buffer{$conn_key} = $pkt; $bufflen{$conn_key} = $tcplen; return; } } if ($snacbuff{$conn_key}) { $pkt = $snacbuff{$conn_key} . $pkt; $snacbuff{$conn_key} = ""; } if (substr($pkt, 0, 4) eq "OFT2") { process_xfer($time, \$pkt, $conn_key); } elsif (substr($pkt, 0, 1) eq "*") { process_snac($time, \$pkt, $conn_key); #return; } else { if($ft_states{$conn_key}) { print "$time: $conn_key: " . length($pkt) . " bytes of FT data $tcplen \n" if $verbose; print hexdump($pkt), "\n" if ($verbose); if (defined $outfile) { print "Writing: to $outfile\n ", hexdump($pkt), "\n" if $verbose; print FILE ($pkt); } } $pkt = ''; } } } sub process_xfer { my($time, $pkt, $conn_key) = @_; print "$time: $conn_key\n" if $verbose; $ft_states{$conn_key} = 1; my %ft_data = protoparse($session, "file_transfer_header")->unpack($$pkt); if ($verbose) { printf "$time: $conn_key\n" ."\t[type=%04X] [encrypt=%d] [compress=%d] [files=%d/%d] [parts=%d/%d] [bytes=%d/%d]\n", delete @ft_data{qw(type encrypt compress files_left file_count parts_left part_count bytes_left byte_count)}; } print "\tHEADER IS NOT 256 BYTES!!\n" unless $ft_data{header_length} == 256; substr($$pkt, 0, $ft_data{header_length} + 4) = ""; ssdump_hash(\%ft_data, 1); print "\n" if $verbose; } sub process_snac { my($time, $pkt, $conn_key) = @_; my($chan, $seqno, $len) = unpack("xCnn", substr($$pkt, 0, 6, "")); if(length($$pkt) < $len) { $snacbuff{$conn_key} = pack("CCnn", 42, $chan, $seqno, $len); $snacbuff{$conn_key} .= $$pkt; return; } my $snac = substr($$pkt, 0, $len, ""); if ($verbose) { print "$time: $conn_key"; printf " ch=%02X", $chan; } my %snac_data = protoparse($session, "snac")->unpack($snac); if ($verbose) { printf " fl=%02X/%02X", $snac_data{flags1} || 0, $snac_data{flags2} || 0; printf " [%04X/%04X]", $snac_data{family} || 0, $snac_data{subtype} || 0; } my $protobit = snac_to_protobit(%snac_data); if(!$protobit) { if ($verbose) { print " == UNKNOWN\n"; print hexdump($snac_data{data}); print "\n"; } } else { print " == $protobit\n" if $verbose; my %data = protoparse($session, $protobit)->unpack($snac_data{data}); if($protobit =~ /^buddylist_(add|modify|delete)$/) { %data = protoparse($session, "buddylist_change")->unpack($snac_data{data}); } if($protobit =~ /^(incoming|outgoing)_IM$/) { my $channel_data; print $protobit; if($data{channel} == 1) { $channel_data = {protoparse($session, "standard_IM_footer")->unpack($data{message_body})}; } elsif($data{channel} == 2) { $channel_data = {protoparse($session, "rendezvous_IM")->unpack($data{message_body})}; my $type = OSCAR_CAPS_INVERSE()->{$channel_data->{capability}}; if($type eq "chat") { $channel_data->{svcdata} = {protoparse($session, "chat_invite_rendezvous_data")->unpack($channel_data->{svcdata})}; } elsif($type eq "filexfer") { $channel_data->{svcdata} = {protoparse($session, "file_transfer_rendezvous_data")->unpack($channel_data->{svcdata})}; } elsif($type eq "sendlist") { $channel_data->{svcdata} = {protoparse($session, "buddy_list_transfer_rendezvous_data")->unpack($channel_data->{svcdata})}; } } else { $channel_data = $data{message_body}; } $data{message_body} = $channel_data; } ssdump_hash(\%data, 1); } print "\n" if $verbose; } sub docx2txt ($) { # VERY simple docx2text conversion. # # See Sandeep Kumar's docx2text.pl for a much more robust converter # http://www.textlib.com/doc2text.html my ($infile) = @_ ; my $unzip = "/usr/bin/unzip"; my $nulldevice ="/dev/null"; my $nl = "\n"; my $content = `$unzip -p '$infile' word/document.xml 2>$nulldevice`; return "Failed to extract required information from $infile!\n" if ! $content; $content =~ s/(\r)?\n//; $content =~ s{]+?/>||}|$nl|og; $content =~ s/<.*?>//og; return "----\n",$content,"----\n"; } my $pcap = Net::Pcap::open_offline($file, \$!) or die "Couldn't open $file: $!\n"; $datalink = Net::Pcap::datalink($pcap); Net::Pcap::dispatch($pcap, 0, \&got_packet, undef); Net::Pcap::close($pcap);