本日の成果

プライベートな時間でやりたいことがいろいろあるのですが、本業に休日が浸食される今日この頃です……。

ただ、せっかくの休日なので、今日の成果物は世に公開しておくことにします。お題は「TCP 通信っぽい応答を Raw IP を使ってやってみよう」。

  1. 適当な TCP 通信を ethereal などでキャプチャしておく。
    • その際には、サーバ側の返答を1パケットごとに分けてファイルに保存する。
  2. 適当な手段で、自分がサーバ代わりに返信する状況を整える。
    • ネットワークの設定を書き換えて自分をルータだと思わせるのが手っ取り早いでしょう。
    • たとえば、ettercap というツールにはまさにそういう目的で偽 DHCP サーバを簡単に立てる機能があるようですが、ご利用は自己責任で。
      • 起動しっぱなしだと ettercap が親切にもパケットを正しい先に転送してしまいますので、DHCP サーバの役目さえ終われば、ettercap は終了させてしまうのが吉です。
  3. ターゲットクライアントでもう一度通信を試み、タイムアウトしないうちに、SYN パケットの AN と送信元ポート番号を ethereal などで目視して控えておく。
    • SYN セグメントの送信タイムアウト (R2) は RFC に従っていれば最低 180 秒ですが、3回送ったら諦めてしまう Windows のような子もいますので、要注意です。
  4. すかさず、控えたパラメータで引数に設定しつつ、下に貼り付けた perl スクリプトを実行する。
    • 下のコマンド使用例は2つの libpcap 形式のキャプチャファイルの中身を
    • その回のセッションに合わせて適当に AN と送信先ポートを書き換えつつ
    • 書き換えたデータに合わせて IP Checksum と TCP Checksum を書き換えつつ
    • 書き換えたパケットの内容を bittwist を使って2番目のネットワークインターフェイスに送信します。
    • 引数指定により、送信パケットの AN を、引数で指定した値から各パケットの送信前に 1, 436 と増加させます。
    • また、2回目のパケットを送信する前に2000msのウェイトを入れます。
  5. すると、TCP 通信をきちんと行っているかのようにクライアントを錯覚させることができる。
> perl send_packets.pl --command="bittwist -i 2" --an=0x9af57c69 --dstport=51119
   http_reply_1.cap,0,1 http_reply_2.cap,2000,436

bittwist を使っているのは、WinXP SP2 環境下で、Raw なパケットデータのファイル内容を送信できるツールを他に見つけることが出来なかったためです。WinPcap の開発キットを使って自作すればいいんですが、再生産することもないですので日和りました。ただ、bittwist の素性もよく存じておりませんので、ご利用は自己責任で。

bittwist では、libpcap 形式のファイルを入力に受け取りますので、このツールでは先頭に 0x28 バイトのヘッダがあると仮定してパースしています。そうではなくて生データを直接指定するようなパケット送信コマンドを使う場合は、ヘッダのない生送信パケットバイナリファイルを渡しつつ、オプションで --offset=0 と指定してください。

楽したい症候群

Perl 用の Winpcap バインディングは ActivePerl 用ばかりで cygwin とは基本的に相性が悪いと認識していますが、もしも Perl 用にいいバインディングがあれば教えてくださいませ……。
Net::Pcap と Win32::NetPacket は少なくともすんなりインストールはできませんでした。しかも、Net::Pcap には生パケット送信機能が無いような気がします。

そして、これを書いた本当の動機は、こんなものを頑張って作らなくても、このクールなツールを使えばパケットの返答は自由自在だぜぃ、という情報が欲しいからだったりします。Python でも Ruby でもかまいませんので、Windows 上の、できれば cygwin 環境でお気楽に組めるパケット応答スクリプティング環境を探しております。求む、情報。

ソースは以下。

#!/usr/bin/env perl

use strict;
use Getopt::Long;
use Fcntl;
use File::Temp qw/tempfile/;

use vars qw($packet %opts);
my $command = "bittwist";

GetOptions(\%opts,
#           "srceth:s",
#           "dsteth:s",
#           "srcip:s",
#           "dstip:s",
           "srcport:s",
           "dstport:s",
#           "sn:s",
           "an:s",
           "offset:s",
           "command:s",
           "use-cygwin-path"
           );

$command = $opts{command} if ( exists $opts{command} );
my $an_offset = 0;

foreach my $arg (@ARGV)
{
    my ($cap_file, $interval, $an_inc) = split ',', $arg;
    my %args;

    # sleep
    if ( $interval )
    {
        select(undef, undef, undef, $interval / 1000.0 );
    }

    # read cap file
    sysopen IFH, $cap_file, O_RDONLY;
    binmode IFH;
    $packet = join '', <IFH>;
    close IFH;

    # update cap
    if ( defined $an_inc )
    {
        $an_offset += $an_inc;
        $args{an_offset} = $an_offset;
    }
    update_packet(%args);

    # write cap file
    my ($tmp_fh, $tmp_filename) = tempfile( SUFFIX => '.cap' );
    binmode $tmp_fh;
    syswrite $tmp_fh, $packet;
    close $tmp_fh;

    # send packet
    if ( !$opts{"use-cygwin-path"} )
    {
        $tmp_filename = `cygpath -w $tmp_filename`;
        chomp $tmp_filename;
    }
    my $command_line = "$command '$tmp_filename'";
    print $command_line . "\n";
    system($command_line);
}

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

sub update_packet
{
    my %args = @_;

    my $offset = 0x28; # libpcap 形式のヘッダは 0x28 バイトあるようでした。
    $offset = eval($opts{offset}) if ( exists $opts{offset} );

    my $eth_offset = $offset;
    my $eth_header_length = 0x0e;

    my $ip = undef;
    my $proto = "unknown";

    my $ip_offset;
    my $ip_length;
    my $ip_header_length;
    my $tcp_offset;
    my $tcp_length;
    my $tcp_header_length;
    my $tcp_data_offset;
    my $tcp_data_length;

    $ip = ( getU16($eth_offset+12) == 0x0800 );
    if ( $ip )
    {
        $ip_offset = $eth_offset + $eth_header_length;
        $ip_header_length = (getU8($ip_offset) & 0x0f) * 4;
        $ip_length = getU16($ip_offset+2);

        $proto = "tcp" if ( getU8($ip_offset+9) == 6 );
        $proto = "udp" if ( getU8($ip_offset+9) == 17 );
        if ( $proto eq "tcp" )
        {
            $tcp_offset = $ip_offset + $ip_header_length;
            $tcp_length = $ip_length - $ip_header_length;
            $tcp_header_length = (getU8($tcp_offset+0x0c) >> 4) * 4;
            $tcp_data_offset = $tcp_offset + $tcp_header_length;
            $tcp_data_length = $tcp_length - $tcp_header_length;
        }
    }

    printf STDERR "eth: %d, ip: %d, tcp: %d, tcp-data:%dbytes\n",
    $eth_header_length, $ip_header_length, $tcp_header_length, $tcp_data_length;

    if ( $proto eq "tcp" )
    {
        my $an_offset = $args{an_offset} || 0;
        setU16($tcp_offset+0, eval($opts{srcport})) if ( exists $opts{srcport} );
        setU16($tcp_offset+2, eval($opts{dstport})) if ( exists $opts{dstport} );
        setU32($tcp_offset+4, eval($opts{sn})) if ( exists $opts{sn} );
        setU32($tcp_offset+8, eval($opts{an}) + $an_offset) if ( exists $opts{an} );
    }

    my $src_ip;
    my $dst_ip;

    if ( $ip )
    {
        $src_ip = getU32($ip_offset+12);
        $dst_ip = getU32($ip_offset+16);
    }

    if ( $ip )
    {
        # IP Checksum
        setU16($ip_offset+10, 0);
        setU16($ip_offset+10, calc_checksum(substr $packet, $ip_offset, $ip_header_length));

        if ( $proto eq "tcp" )
        {
            # TCP Checksum
            my $tcp_pseudo_header = pack("NNCCn", $src_ip, $dst_ip, 0, 6, $tcp_length);
            setU16($tcp_offset+16, 0);
            setU16($tcp_offset+16, calc_checksum($tcp_pseudo_header.substr $packet, $tcp_offset, $tcp_length));
        }
    }
}

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

sub getU32
{
    my $offset = shift;
    return unpack("N", substr $packet, $offset, 4);
}

sub getU16
{
    my $offset = shift;
    return unpack("n", substr $packet, $offset, 2);
}

sub getU8
{
    my $offset = shift;
    return unpack("C", substr $packet, $offset, 1);
}

sub setU32
{
    my ($offset, $value) = @_;
    substr $packet, $offset, 4, pack("N", $value);
}

sub setU16
{
    my ($offset, $value) = @_;
    substr $packet, $offset, 2, pack("n", $value);
}

sub setU8
{
    my ($offset, $value) = @_;
    substr $packet, $offset, 1, pack("C", $value);
}

sub calc_checksum
{
    my @data = unpack("n*", $_[0]);
    my $sum = 0;
    foreach my $h ( @data )
    {
        $sum += $h;
        $sum += $sum >> 16;
        $sum &= 0xffff;
    }
    $sum ^= 0xffff;
    $sum = 0xffff if ( $sum == 0 );
    return $sum;
}

なお、グローバル変数を使っているのが汚い、とか、calc_checksum の引数はリファレンスで渡さないとコピーが……とか、$sum の計算はエンディアンが逆のままでも大丈夫なのに……とか、$sum の溢れの計算は最後に一括してやるべきだ、といった苦情は受け付けかねます。

また、UDP 対応(UDP Checksum を書き換えるコードが入っていません)や、Ethernet Address などのその他の様々なパラメータの書き換え機能はみなさまへの宿題ということで……。