#!/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);