This post is also available in: English (英語)
概要
Unit 42は、5月初旬に「プリンス オブ ペルシャ」というタイトルのブログを公開しました。
そこで、以前は未公開のマルウェア ファミリ、Infy を使用して、世界中の政府機関や注目を集めている業界を標的とした10年間に及ぶ活動を発見したことを説明しました。
この記事の公開の後、C2ドメインの責任を担うパーティとの協力を通じて、Unit 42のリサーチャーは、複数のC2ドメインの制御権を得ることに成功しました。これにより、この活動での攻撃者による被害者へのアクセスは無効化され、この活動で現在被害を受けている標的への洞察を深め、影響を受けるパーティに通知することができます。
公開後
元のブログの公開後1週間は、C2インフラストラクチャに異常な変化は見られませんでした。以前に定期的に見られたとおり、既存のドメインは新しいIPアドレスに移行されました。いくつかの新たなインストール ドメインが、現在のドメインの命名規則(新しいIOCの付録を参照のこと)に従って追加されました。
攻撃者が新しいバージョン(31)を開発し、これが単一のカナダの標的に対して仕掛けられたことがわかりました。
ファイルの説明は、基本的には同じままです(“CLMediaLibrary Dynamic Link Library V3”)。もっとも重要な点は、10年間の活動全体を通じて使用されていたことを発見し、以前のブログで解説したエンコード キーに何の変更もなかったことです(現在はオフセット20と、URLエンコードで2番目に渡す場合はオフセット11を使用)。このことから、攻撃者は私たちの最初のレポートに気付いていないと判断しています。
シンクホール
C2ドメインの責任を担うパーティとの協力を通じて、私たちはそれらの1つではなくすべての制御権を得て、制御下のサーバにAレコードを転送しました。これによって、攻撃者がその後、さらにドメイン構成を変更したり、被害者へコマンドを発行したり、または大半の被害者からさらにデータを取得したりできないように、防御しました。転送後の接続の分析では、攻撃者がサードパーティ サービスを使用して、彼らが突然ほぼすべてのトラフィックを失った理由を突き止めようとしたことが示されました。図1は、被害を受けたC2トラフィックの地理的な場所を示しています。当社のシンクホール サーバと現在通信している時点でのすべてを表しています。
![図1 被害を受けたC2トラフィックのグラフィカル表示](https://www.paloaltonetworks.jp/content/dam/paloaltonetworks-com/ja_JP/Images/blog/PoP-Game-Over-1.png)
私たちは、その後、シンクホールの制御をShadowserverに移管しました。引き続き被害者への通知と修復を担当していただいていることに感謝します(https://www.shadowserver.org/wiki/pmwiki.php/Involve/GetReportsOnYourNetwork)。
被害者
被害を受けたC2トラフィックを分析し、Infy活動の被害者が誰であったかがわかりました。35カ国の326の被害者システムにインストールされた456のマルウェア エージェントを特定しました。図2は、被害者の場所の地理的な詳細を示しています。元のブログで、この活動では大勢のイラン市民が標的とされ、すべての犠牲者のほぼ1/3がイラン人だったと判明したことを述べました。また、たとえば、クライムウェア攻撃に比べると、被害者の総数はそれほど多くないことも述べました。
![図2 被害者の地理的な場所。被害者が発見されなかったため、この地図ではニュージーランドが省略されています。](https://www.paloaltonetworks.jp/content/dam/paloaltonetworks-com/ja_JP/Images/blog/PoP-Game-Over-2.png)
バージョン
元のブログでは、Infyマルウェアには、はっきり異なる2つの基本的な亜種があると述べました。本来の“Infy”亜種に加え、より新しい、精巧でインタラクティブなフル機能装備の“Infy M”亜種が、より高価値と思われる標的に対して導入されました。総計すると、すべての被害者の93%がInfyに感染し、60%がInfy “M”に感染しました(図3)。犠牲者の総数が少ないことと合わせると、これは、活動の個々の標的にきめ細かく注意を払ったことを示唆しています。犠牲者の大半は、無料の機能セットに関連している可能性があります。また、本来の亜種に感染した被害者のシステムには、標的が攻撃者にとってより価値が高いと思える場合に、後から“M”亜種を追加するための“アップグレード”パスが提供された可能性もあります。
![図3 InfyおよびInfy “M”感染の内訳](https://www.paloaltonetworks.jp/content/dam/paloaltonetworks-com/ja_JP/Images/blog/PoP-Game-Over-3.png)
Infy “M”の場合は、大半の標的で最新バージョン(7.8)が使用され、古い6.xバージョンはまったく使用されていません(図4)。これは、これらのより価値の高い標的には、最新バージョンを使って最新の状態を保持するために、かなりの注意が払われたことを示しています。
これに対し、より基本的な元のInfy亜種に関して、多種多様なバージョンがインストールされていることが分かります(図5)。これによると旧バージョンによる被害者が多くおり、旧バージョンの中には、10年経った最初のバージョンもあります。これはこうした個々の標的に対する関心がはるかに低いことを示しています(なお、私たちは少数の古い6.xバージョンを確認することは確認しましたが、これらは接続時にバージョンを通知してきません)。
![図4 Infy「M」被害に関するバージョン](https://www.paloaltonetworks.jp/content/dam/paloaltonetworks-com/ja_JP/Images/blog/PoP-Game-Over-4.png)
![図5 「元の」Infy被害に関するバージョン](https://www.paloaltonetworks.jp/content/dam/paloaltonetworks-com/ja_JP/Images/blog/PoP-Game-Over-5.png)
ゲーム オーバー
捕捉の直後、Infyの新バージョン(31)においても、過去に確認済みのパターンの利用により、複数のドメインが既知の攻撃活動用IPアドレスに対して登録されていることを私たちは確認しました。パターン範囲box4035[.]net – box4090[.]net (138.201.0.134)におけるほとんどどのドメインが該当します。しかし、これらはどのサンプルC2リストの中にも確認されませんでした。Bestwebstat[.]comは別の運用者によるシンクホールでした。
バージョン15-24のInfyに感染した一部の被害者は、まだ攻撃者の管理下にあるC2サーバus1s2[.]strangled[.]netを引き続き利用していました。6月上旬、攻撃者はこのC2を利用して新型のInfy「M」バージョン8.0をus1s2[.]strangled[.]net/bdc.tmpからダウンロードするよう指示しました。私たちはこのようにInfy亜種がInfy「M」に直接更新されているところを初めて目撃しました。その際、偽装名称「Macromedia v4」が使われましたが、これはInfy v31で見られた「v3」を変更したものです。また、攻撃者はこのバージョンで音声録音機能を削除しました。
uvps1[.]cotbm[.]comがデータを秘密裏に盗み出すのに用いられましたが、以前はアドレスが138.201.47.150であったのが私たちの最初のブログの公開後には144.76.250.205へと変わりました。また、/themes/u.phpにおいてマルウェアの更新が提供されていました。
不思議なC2エントリ「hxxp://box」も追加されていました(注: 記事公開にあたり無害化した表記にしてあります)。これがどのように機能するのかは不明です。ことによると、セキュリティ侵害を受けた被害者のイントラネットのデバイス、または攻撃者によって、被害者のコンピューター上にあるHOSTSファイルが変更された結果かもしれません。
私たちによる捕捉の後、攻撃者はサーバのドメイン名だけでなくIPアドレスを自分たちのマルウェアC2リストに追加し始めました。さらに、ZIPパスワードをわずかに「Z8(2000_2001ul」から「Z8(2000_2001uIEr3」へと変更しました。マルウェアの新バージョンにはKaspersky Labs、AvastおよびTrend Microがないか調べるアンチウイルス検査機能が追加されました。マルウェアのデータ キャプチャは以下のファイル拡張子を検索します。
.doc, .docx, .xls, .xlsx, .xlr, .pps, .ppt, .pptx, .mdb, .accdb, .db, .dbf, .sql, .jpg, .jpeg, .psd, .tif, .mp4, .3gp, .txt, .rtf, .odt, .htm, .html, .pdf, .wps, .contact, .csv, .nbu, .vcf, .pst, .zip, .rar, .7z, .zipx, .pgp, .tc, .vhd, .p12, .crt.pem,.key.pfx, .asc, .cer, .p7b, .sst, .doc, .docx, .xls, .xlsx, .xlr, .pps, .ppt, .pptx.
また、以下のフォルダー位置を検索します。
:\$recycle.bin, :\documents and settings, :\msocache, :\program files, :\program files (x86), :\programdata, :\recovery, :\system volume information:\users, :\windows, :\boot, :\inetpub, :\i386.
マルウェアは、この攻撃活動で当初から現在に至るまで確認されている同一の復号化キーを使い続けていました。
6月中旬、C2ドメインの管理責任者と法執行当局の協力により、私たちは残りのC2ドメインをnullルート化して、IPアドレスで直接指定されていたサーバを無効化することに成功しました。これにより10年にわたる攻撃活動は終焉を迎えましたが、当然、やがてこの攻撃者が何か別のものを装って戻ってくるものと思われます。
捕捉作業をお手伝いしていただいたマルウェア リサーチ チーム(Yaron Samuel、Artiom Radune、Mashav Sapir、Netanel Rimer)に感謝いたします。
付録1 – 秘密裏に持ち出すためのアルゴリズム
このマルウェアは、マルウェア文字列を暗号化するのに使われるものとは別のアルゴリズムを使って、秘密裏に持ち出すデータを暗号化します。そのアルゴリズムには次のものがあります。
- キーロガー データ+言語
- マルウェア ログ―インストール時刻、DLLのパスと名称、ログのパス、ダウンロード回数、成功/失敗した接続の数
- 被害者のコンピューターに関する情報: タイム ゾーン、ドライブとその種類の一覧、実行中のプロセス、ディスク情報
まず、このマルウェアはすべてのバイトに1を加え、次いで被害者のコンピューター名に基づいて暗号化キーを初期化します(キーにおけるオフセットは「コンピューター名の各文字コードの合計」%「キーの長さ」で計算されます)。その後、キーがデータの暗号化に使われます(復号化関数を参照のこと)。暗号化されたデータは引き続きbase64でコード化されます。
秘密裏に持ち出すデータを暗号化するpythonコード:
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 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
import os,sys import string import base64 import fileinput FIRST_PHASE = "OQTJEqtsK0AUB9YXMwr8idozF7VWRPpnhNCHI6Dlkaubyxf5423jvcZ1LSGmge" SECOND_PHASE = "PqOwI1eUrYtT2yR3p4E5o6WiQu7ASlDkFj8GhHaJ9sKdLfMgNzBx0ZcXvCmVnb" global FULL_KEY FULL_KEY= "" def sub_1_for_hex(str_input): str_output = "" for letter in str_input: try: str_output += chr(ord(letter)-1) except: print "sub_1_for_hex func problem" continue return str_output def sum_comp_name(comp_name): sum = 0 for letter in comp_name: sum+= ord(letter) return sum def init_key(comp): comp_name_sum = sum_comp_name(comp) carry = divmod(comp_name_sum, 62) index = carry[1] -1 end_key = FIRST_PHASE[:index] key = FIRST_PHASE[index:] key = key + end_key key = key + key return key def decrypt(num_list,offset): global FULL_KEY input = "" for num_str in num_list: try: input += num_str.decode('hex') except: input += ')' result = "" for i, c in enumerate(input): i = i % 62 +1 try: index = FULL_KEY.index(c)-1 except ValueError: result += c continue translated = SECOND_PHASE[(index - i +offset) % len(SECOND_PHASE)] result += translated return result def found_infy_enc_data(line): found_infy_str = "show=\"---------- Administration Reporting Service " found_infy_index = line.find(found_infy_str) if not found_infy_index==-1: return True,found_infy_index else: return False,found_infy_index def extract_comp_name(line): comp = r"\xd\xa-----" comp_index = line.find(comp) comp_name = line[comp_index+len(comp):] comp_name = comp_name[:comp_name.find("-----")] print "(((=)))" + comp_name return comp_name def extract_enc_data(line): header = r"\xd\xa_____" start_index = line.find(header)+len(header) line = line[start_index:] endindex = line.index("_____\" value=") line = line[:endindex] return line def write_enc_infy_data_to_file(dec_line,comp_name,filename): file1 = open(filename + "\\" + comp_name + ".txt",'ab') file1.writelines(dec_line) file1.close() def enc_wrapper(enc,comp_name): global FULL_KEY print FULL_KEY FULL_KEY = init_key(comp_name) enc_final = "" for letter in enc: if len(hex(ord(letter))[2:])==1: enc_final += "0" + hex(ord(letter))[2:] elif len(hex(ord(letter))[2:])==2: enc_final += hex(ord(letter))[2:] else: print "not good hex length" exit() enc = enc_final.upper() enc = enc.replace("2E","21") enc = enc.replace("C5DC5A","") enc = enc.replace("D03D00","") enc = enc.replace("0B0E","2121") enc = enc.replace("01","21") enc_len = len(enc) enc_rev = "" num_list = [] enc_print ="" for i in range(0,enc_len/2): enc_rev = enc[-2:] if not enc_rev=="0B" and not enc_rev=="0E" and not enc_rev=="00" and not enc_rev=="D0": enc_print +=enc_rev num_list.append(enc_rev) enc= enc[:-2] #the first part is always ok dec_str = decrypt(num_list,0) final = sub_1_for_hex(dec_str) index = final.find("OK: Sent") if index==-1: print comp_name + " - did not found OK: Sent !!!!\n\n\n\n" #exit() decrypt_data = comp_name + " ++==++ " + str(i) + ": " + final + "\n" final_start = final[0:500] if final_start in UNIQUE_DATA: print comp_name + " already have this data" return UNIQUE_DATA.append(final_start) index = final.find("Installed Date:") if index==-1: for i in range(1,61): dec_str = decrypt3(num_list,i) final = sub_1_for_hex(dec_str) ##print all 62 options index2 = final.find("PROGRAM START:") index3 = final.find("Installed Date:") if not index2 ==-1 or not index3 ==-1: decrypt_data += str(i) + ": " + final + "\n" write_enc_infy_data_to_file(decrypt_data,comp_name,FILE_OUTPUT_NAME) def read_enc_data_files(): for root,dir,files in os.walk(PDML_PATH): for file in files: filename = root+ "\\" + file if os.path.isfile(filename): print filename for line in fileinput.input([filename]): line = line.strip() is_found,found_infy_index= found_infy_enc_data(line) if not is_found: continue line = line[found_infy_index:] #get computer name (for use in init_key() later) comp_name = extract_comp_name(line) UNIQUE_COMP.append(comp_name) #get the infy encrypted data line = extract_enc_data(line) #base64 decode enc_data dec_line = line.decode('base64') #append enc_data to file write_enc_infy_data_to_file(dec_line,comp_name,FILE_ENC_OUTPUT_NAME) enc_wrapper(dec_line,comp_name) try: read_enc_data_files() except: print "exception!!!!" |
付録2 – IoC
Infyバージョン31: f07e85143e057ee565c25db2a9f36491102d4e526ffb02c83e580712ec00eb27
Infy「M」バージョン8.0: 583349B7A2385A1E8DE682A43351798CA113CBBB80686193ECF9A61E6942786A
5.9.94.34
138.201.0.134
138.201.47.150
144.76.250.205
138.201.47.158
138.201.47.153
us1s2[.]strangled[.]net
uvps1[.]cotbm[.]com
gstat[.]strangled[.]net
secup[.]soon[.]it
p208[.]ige[.]es
lu[.]ige[.]es
updateserver1[.]com
updateserver3[.]com
updatebox4[.]com
bestupdateserver[.]com
bestupdateserver2[.]com
bestbox3[.]com
safehostline[.]com
youripinfo[.]com
bestupser[.]awardspace[.]info
box4035[.]net
box4036[.]net
box4037[.]net
box4038[.]net
box4039[.]net
box4040[.]net
box4041[.]net
box4042[.]net
box4043[.]net
box4044[.]net
box4045[.]net
box4046[.]net
box4047[.]net
box4048[.]net
box4049[.]net
box4050[.]net
box4051[.]net
box4052[.]net
box4053[.]net
box4054[.]net
box4055[.]net
box4056[.]net
box4057[.]net
box4058[.]net
box4059[.]net
box4060[.]net
box4061[.]net
box4062[.]net
box4063[.]net
box4064[.]net
box4065[.]net
box4066[.]net
box4067[.]net
box4068[.]net
box4069[.]net
box4070[.]net
box4071[.]net
box4072[.]net
box4075[.]net
box4078[.]net
box4079[.]net
box4080[.]net
box4081[.]net
box4082[.]net
box4083[.]net
box4084[.]net
box4085[.]net
box4086[.]net
box4087[.]net
box4088[.]net
box4089[.]net
box4090[.]net