Perl Winny Scanner

検出ツールの開発者が語る,「Winnyを検出する方法」Winny 検出方法が詳しく書いてあったので、PerlWinny Scanner を実装してみました。

port scan するところは fork + socket で書いていて、あまりいけてないのでもっといい方法を知っている人は教えてください。このスクリプトを実行すると /var/tmp/test_socket というディレクトリが出来ます。中身は空になるはずですけど。

Winny の port に繋いで得られた文字列を

#   my(undef, $passphrase, $encrypted) = unpack "A2A4A5", $msg;
    my( undef, $passphrase, $encrypted ) = unpack "a2a4a5", $msg;
    my $snipped_ph = ( split /\x00/, $passphrase )[0];
    my $decrypted = RC4( $snipped_ph, $encrypted );

とするだけで Winny かどうかは判別できる。というのがすごいな。

2006-04-19 追記 unpack の引数間違ってたので perlpacktut 見て出直します。NULL 文字も知らなかったので追加してみた。でもまだ検知失敗するときがある。全然すごくないですね...。

2006-04-20 追記 最新版を winny_scanner.pl に置くことにして、このエントリに書いてあるスクリプトは Obsolete にします(スクリプトに手を入れるたびに、このエントリを更新するのが面倒)。一応個人テストレベルで新しいのは誤検知しないことを確認してますけど、まだなにかあるかも。

2006-06-21 追記 何故かいまだにアクセスがあるのでスクリプトCPAN にアップロードしておきました。winny_scanner source はほとんど同じわけですが、上のリンクは削除しておきます。

winny_scanner.pl

#!/usr/local/bin/perl

use strict;
use warnings;
use IO::Socket::INET;
use FileHandle;
use File::Path;
use Getopt::Long;
use Term::ANSIColor qw(:constants);
$Term::ANSIColor::AUTORESET = 1;
use Crypt::RC4;

my $WINNY_STRING = "\x01\x00\x00\x00\x61";
my $PATH         = '/var/tmp/test_socket/';
my $INTERVAL     = 50;

######################################################################

my $host;
my $port;
my $debug;
my $result = GetOptions(
    "host=s" => \$host,
    "port=s" => \$port,
    "debug"  => \$debug,
);

unless ( defined $host and defined $port ) {
    printf STDERR "Usage: winny_scanner.pl ";
    printf STDERR "--host='192.168.1.1,192.168.1.2' ";
    die "--port='10000..10500'\n";
}

# trivial parse
no strict;
my @host_list = split /\s?,\s?/, $host;
my @port_list = eval "$port";
die "illegal port : $@\n", if $@;
use strict;

######################################################################

my %SCAN;
local $SIG{CHLD} = "IGNORE";

if( !-d $PATH ) {
    eval { mkpath($PATH) };
    die "cannot mkpath $@\n" if $@;
    if($debug) {
        print YELLOW "create $PATH\n";
        print RESET;
    }
}


my $process_count = 0;
for my $host (@host_list) {
    for my $port (@port_list) {
        my $pid = fork();
        if ( !defined $pid ) {
            die "cannot fork : $!\n";
        }
        # parent
        elsif ( $pid > 0 ) {
            $process_count++;
            if( 0 == $process_count % $INTERVAL ) {
                if($debug) {
                    print YELLOW "parent sleep\n";
                    print RESET;
                }
                sleep(2);
            }
        }
        # child
        elsif ( $pid == 0 ) {
            child_business( $host, $port );
        }
    }
}

sleep(2);

opendir(DIR, $PATH);
while ( my $file = readdir(DIR) ) {
    next if $file =~ /\./;
    next unless $file =~ /^\d+$/;

    file_to_hash($PATH . $file);
    unlink($PATH . $file) or die "cannot unlink $file : $!\n";
}
closedir(DIR);

die "no Winny found\n" unless %SCAN;

# output
for my $host ( sort keys %SCAN ) {
    for my $port ( sort keys %{ $SCAN{$host} } ) {
        if ( $SCAN{$host}->{$port} ) {
            printf "%s:%s is winny\n", $host, $port;
        }
    }
}

######################################################################

sub child_business {
    my $host = shift;
    my $port = shift;

    if($debug) {
        print YELLOW "open socket $host : $port\n";
        print RESET;
    }

    my $sock = IO::Socket::INET->new(
        Proto    => 'tcp',
        PeerAddr => $host,
        PeerPort => $port,
        Timeout  => 1,
    );
    exit unless defined $sock;
    my $msg = $sock->getline;
    $sock->close;

    my $winny = is_winny($msg);

    my $file = $PATH . $$;
    my $fh = new FileHandle;
    $fh->open(">$file") or die "cannot open write file $file : $!\n";
    $fh->print("$host:$port:$winny\n");
    $fh->close;

    exit;
}

sub is_winny {
    my $msg = shift;

    return 0 unless $msg;

#   my(undef, $passphrase, $encrypted) = unpack "A2A4A5", $msg;
    my( undef, $passphrase, $encrypted ) = unpack "a2a4a5", $msg;
 
    my $snipped_ph = ( split /\x00/, $passphrase )[0];
    my $decrypted = RC4( $snipped_ph, $encrypted );
#   my $decrypted = RC4($passphrase, $encrypted);

    if($debug) {
        my $hex_dec = unpack "H*", $decrypted;
        print GREEN "dec = $hex_dec\n";
        print RESET;
    }

    if ( $decrypted eq $WINNY_STRING ) {
        return 1;
    }
    return 0;
}

sub file_to_hash {
    my $file = shift;
    my $fh   = new FileHandle;
    $fh->open($file) or die "cannot open read file $file : $!\n";

    my $line = $fh->getline;
    my($host, $port, $winny) = split /:/, $line;
    $SCAN{$host}->{$port} = $winny;

    $fh->close;
}

=head1 NAME

winny_scanner.pl - Perl Winny Scanner

=head1 SYNOPSIS

% ./winny_scanner.pl --host='192.168.1.1,192.168.1.2' --port='10000..10500'

=head1 ABSTRACT

Perl Winny Scanner inspired by
   L<http://itpro.nikkeibp.co.jp/article/Watcher/20060411/235051/>

Do not scan the network if you are not the administrator of it.

=head1 COPYRIGHT

Copyright 2006, <massa.hara at gmail.com>

This script is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut