LoginSignup
59
45

More than 5 years have passed since last update.

そろそろマルウェアMiraiのコードについて語ってみようと思う

Posted at

はじめに

2016年に登場したマルウェアMiraiは何もかもが異色の存在でした。

  • 当時最大規模のDDoS攻撃を発生させた
  • 乗っ取りのターゲットは過去あまり例のなかったIoT製品(IoTカメラやそのレコーダ、ルータなど)
  • まさかのソースコード公開

2018年現在でもMiraiと思われるポートスキャンは多々行われていますが、一方でその多くはMiraiを元にした亜種となりました。また、Miraiの開発者は2017年末に逮捕 されています。

事件から時間も立ちましたし、そろそろMiraiのコードを読んで 面白かった部分 についてまとめておこうか、というのがこの記事です。

あくまで面白かった部分なので、環境の作り方だの挙動だの具体的な攻撃方法だのについては書きません。 あくまで面白かった部分 です。

Miraiのソースコード

githubに置かれています。
https://github.com/jgamblin/Mirai-Source-Code

もともとは別のサイトにアップロードされていたものなので、「無断転載ってどうなの」とか「勝手にGPLライセンスを名乗るのっておかしくない?」とか当時騒がれました。マルウェアのライセンスをめぐる議論があった、というのも一つの面白ポイントですね。

なお、Miraiは攻撃バイナリがC言語で書かれており、管理側がGo言語で書かれています。

ポイント1: プロセス名を隠す

怪しげなプロセス名があったら落としたくなりますし、明らかにマルウェアとわかるもののログが残っていたりしたら調べたくもなるでしょう。攻撃する側としては、プロセス名という情報も隠蔽したいものです。

/mirai/bot/main.c
    // Hide process name
    name_buf_len = ((rand_next() % 6) + 3) * 4;
    rand_alphastr(name_buf, name_buf_len);
    name_buf[name_buf_len] = 0;
    prctl(PR_SET_NAME, name_buf);

prctl関数は第一引数で挙動を変えるプロセス操作関数で、 PR_SET_NAME を指定することでプロセス名を第二引数で指定したものに変更できます。
普通にプログラムを書いていて プロセス名を変えたくなることなんてそうは無い と思いますが、そんなことできるんだなぁ、という面白ポイントです。

ポイント2: 感染端末のアーキテクチャを調べる

MiraiはIoT機器をターゲットにした点が話題になりましたが、コードを書く側にとってターゲットがIoT機器だと何が変わるのでしょうか?
ポイントはアーキテクチャで、侵入できた端末がx86なのかamd64なのか、さらに機器によってはarmを採用している場合もあるでしょう。せっかく侵入できたのに、攻撃バイナリが当該環境で実行できない、というのは残念ですよね。
そこで、Miraiは侵入した端末のアーキテクチャを調べ、それに応じた攻撃バイナリのダウンロードを行います。

/loader/src/connection.c
int connection_consume_arch(struct connection *conn)
{
    if (!conn->info.has_arch)
    {
        struct elf_hdr *ehdr;
        int elf_start_pos;

        if ((elf_start_pos = util_memsearch(conn->rdbuf, conn->rdbuf_pos, "ELF", 3)) == -1)
            return 0;
        elf_start_pos -= 4; // Go back ELF
                :

                :
        /* arm mpsl spc m68k ppc x86 mips sh4 */
        if (ehdr->e_machine == EM_ARM || ehdr->e_machine == EM_AARCH64)
            strcpy(conn->info.arch, "arm");
        else if (ehdr->e_machine == EM_MIPS || ehdr->e_machine == EM_MIPS_RS3_LE)
        {
            if (ehdr->e_ident[EI_DATA] == EE_LITTLE)
                strcpy(conn->info.arch, "mpsl");
            else
                strcpy(conn->info.arch, "mips");
        }
        else if (ehdr->e_machine == EM_386 || ehdr->e_machine == EM_486 || ehdr->e_machine == EM_860 || ehdr->e_machine == EM_X86_64)
            strcpy(conn->info.arch, "x86");
        else if (ehdr->e_machine == EM_SPARC || ehdr->e_machine == EM_SPARC32PLUS || ehdr->e_machine == EM_SPARCV9)
            strcpy(conn->info.arch, "spc");
        else if (ehdr->e_machine == EM_68K || ehdr->e_machine == EM_88K)
            strcpy(conn->info.arch, "m68k");
        else if (ehdr->e_machine == EM_PPC || ehdr->e_machine == EM_PPC64)
            strcpy(conn->info.arch, "ppc");
        else if (ehdr->e_machine == EM_SH)
            strcpy(conn->info.arch, "sh4");
                :
                :

ここでちょこちょこ出てくる elf がキモで、バイナリ系の人であればご存知の ELF(エルフ)ヘッダ からアーキテクチャの情報を抜き出してくる、という動きになっています。
ELFをざっくりいうとBSDやLinuxで使われる実行バイナリのフォーマットで、ELFヘッダにはアーキテクチャに関する情報が入っています。
ELFヘッダの表示については、 readelf コマンドで実際に見てみるのも面白いです。

$ readelf -h /bin/echo
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1c50
  Start of program headers:          64 (bytes into file)
  Start of section headers:          33208 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         28
  Section header string table index: 27

なかなか凝ったことしてるなぁ……という面白ポイントです。

ポイント3: ポートスキャンしたらヤバそうなところは狙わない

Miraiはインターネット上のv4アドレスをランダムに生成して侵入を試みますが、loopbackアドレスなど侵入を試みてもしょうがないアドレスがあります。無駄ならまだマシで、ケンカを売ってはいけない相手もいるでしょう。

/mirai/bot/scanner.c
    do
    {
        tmp = rand_next();

        o1 = tmp & 0xff;
        o2 = (tmp >> 8) & 0xff;
        o3 = (tmp >> 16) & 0xff;
        o4 = (tmp >> 24) & 0xff;
    }
    while (o1 == 127 ||                             // 127.0.0.0/8      - Loopback
          (o1 == 0) ||                              // 0.0.0.0/8        - Invalid address space
          (o1 == 3) ||                              // 3.0.0.0/8        - General Electric Company
          (o1 == 15 || o1 == 16) ||                 // 15.0.0.0/7       - Hewlett-Packard Company
          (o1 == 56) ||                             // 56.0.0.0/8       - US Postal Service
          (o1 == 10) ||                             // 10.0.0.0/8       - Internal network
          (o1 == 192 && o2 == 168) ||               // 192.168.0.0/16   - Internal network
          (o1 == 172 && o2 >= 16 && o2 < 32) ||     // 172.16.0.0/14    - Internal network
          (o1 == 100 && o2 >= 64 && o2 < 127) ||    // 100.64.0.0/10    - IANA NAT reserved
          (o1 == 169 && o2 > 254) ||                // 169.254.0.0/16   - IANA NAT reserved
          (o1 == 198 && o2 >= 18 && o2 < 20) ||     // 198.18.0.0/15    - IANA Special use
          (o1 >= 224) ||                            // 224.*.*.*+       - Multicast
          (o1 == 6 || o1 == 7 || o1 == 11 || o1 == 21 || o1 == 22 || o1 == 26 || o1 == 28 || o1 == 29 || o1 == 30 || o1 == 33 || o1 == 55 || o1 == 214 || o1 == 215) // Department of Defense
    );

アドレスをランダムに生成したあと、ケンカを売ってはいけない相手だった場合はパスします。
GE社やHP社など、意外とセキュリティベンダじゃないところを恐れるもんなんだなーとは思いましたが、一番おもしろいのは最後の行です。 Department of Defense 、つまり アメリカ国防総省 です。そりゃケンカ売りたくない相手でしょうね。

ポイント4: 謎のメッセージ

今回の記事を書いた理由というか、一番おもしろいのがコレです。

mirai.gif

Miraiの管理コンソールへログイン(ロシア語)すると、何やらすごそうな文字列がダララーっと流れます。
libc.poison.so1 、なんだかヤバそうですね。実際のコードを見てみましょう。

/mirai/cnc/admin.go
    this.conn.Write([]byte("[+] DDOS | Succesfully hijacked connection\r\n"))
    time.Sleep(250 * time.Millisecond)
    this.conn.Write([]byte("[+] DDOS | Masking connection from utmp+wtmp...\r\n"))
    time.Sleep(500 * time.Millisecond)
    this.conn.Write([]byte("[+] DDOS | Hiding from netstat...\r\n"))
    time.Sleep(150 * time.Millisecond)
    this.conn.Write([]byte("[+] DDOS | Removing all traces of LD_PRELOAD...\r\n"))
    for i := 0; i < 4; i++ {
        time.Sleep(100 * time.Millisecond)
        this.conn.Write([]byte(fmt.Sprintf("[+] DDOS | Wiping env libc.poison.so.%d\r\n", i + 1)))
    }
    this.conn.Write([]byte("[+] DDOS | Setting up virtual terminal...\r\n"))
    time.Sleep(1 * time.Second)

( ゚д゚)

time.Sleep(250 * time.Millisecond)

( ゚д゚)

そう、何もしていないのです。

並列で動かして裏で何かしている、とかそういうのも無いです。本当にただのsleepなのです。
なんでこんなことを頑張っているのかというと、DDoSはすでにイタズラではなく商売になっており、管理コンソールへのログイン権をお客さんに買ってもらうことで利益をあげるものになっているのです(実際に攻撃するのは管理コンソールを動かすお客様なので、仮に逮捕されたとしても攻撃を行った罪は開発者に向かない、という話もあるとか)。
お客様へのアピールとして、顧客満足度上昇のため、なんだかスゴいことをやっていそうな画面を見せる わけです。

おわりに

実際に攻撃で使われたマルウェアのコードを読む機会というのはあまりありません。マルウェアのコードは、普段のお仕事で書くコードとはまた違う領域の知見が多く見られます。

ここで紹介していない面白ポイントもたくさんあるので、興味をもった方はぜひ自分の目で面白ポイントを探してみてください。

59
45
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
45