Nozomi Networks  Labs による BlackMatter ランサムウェアのテクニカル分析とツール

Nozomi Networks Labs による BlackMatter ランサムウェアのテクニカル分析とツール

先週末、アイオワ州を拠点とするNEW Cooperative Inc.がランサムウェア集団BlackMatterの最新の被害者となった。農業協同組合として運営されている同社によると、この事件は積極的に処理されているが、この記事を書いている時点では、攻撃の全影響は明らかになっていない。

BlackMatterは、そのウェブサイトのメディアからの問い合わせのセクションで、悪意のある作戦の標的にしてはならない重要なインフラの標的を明確に列挙している。NEW協同組合のような規模の組織が重要インフラに分類される可能性は十分にある。もしそうなら、この攻撃は重大な結果をもたらす可能性がある。現代のサプライチェーンは突発的な混乱に対して脆弱であり、その影響の全容が理解されるのはかなり後になってからであることがしばしば見受けられる。

このブログでは、Nozomi Networks Labs が BlackMatter ランサムウェアの実行ファイルを分析するために行ったプロセス、マルウェアが分析の障害となる方法、およびそれらを克服するために行った方法について説明します。また、他の研究者がこのランサムウェアの他のインスタンスから重要な情報を抽出するのに役立つスクリプトをいくつか紹介します。

主な機能

このランサムウェアはChaCha20とRSAアルゴリズムのバージョンで被害者のファイルを暗号化する。RSAは、攻撃者側に保存されている秘密鍵なしでは復号化が不可能であることを保証するために使用されます。このマルウェアは、READMEファイルの形で、復号化するための手順を記したメモを残します。さらに、壁紙を変更して注意を喚起します:

READMEファイル
BlackMatter ランサムウェア

BlackMatterランサムウェアの実行ファイルによって変更された壁紙で、復号化手順が記載されたREADMEファイルに注意を喚起している(クリックで拡大)

さらに、このマルウェアは、以下のような一般的なランサムウェアの動作を実行する:

  • WMI クエリ SELECT * FROM Win32_ShadowCopy を使用して、最初に一覧を表示してシャドウ コピー(ローカル バックアップ)を削除します。
  • ごみ箱内のファイルを削除する
  • コンフィギュレーションで指定されたプロセスやサービスの終了
  • 壁紙が、復号化の指示のためにREADMEテキストファイルを指すように変更する。
  • Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7} is used for UAC (user account control) bypass
  • 暗号化されたファイルは、READMEファイル名のプレフィックスに表示される被害者IDと一致する新しい拡張子を持ち、レジストリにも保存されます。この犠牲者IDは、レジストリのMachineGuid値から取得されます。

アンチデバッグ・テクニック

このマルウェアは、どのWinAPIに依存しているかを隠すことで、解析を妨害しようとしている。これを回避するために、マルウェアは必要なインポート関数の一部をハッシュによって解決する:

ハッシュ名によるWinAPI関数の識別
ハッシュ名によるWinAPI関数の識別

さらに分析を複雑にするために、ハッシュによるWinAPIアドレスの一括解決の場合、マルウェアは発見されたアドレスを保存するユニークな方法を使用する。単にテーブルに保存するのではなく、解決されたWinAPIアドレスごとに、それをエンコードする5つの異なる方法(rol、ror、xor、xor+rol、xor+ror)のうち1つをランダムに選択し、エンコードされたアドレスを、呼び出しの直前にデコードする動的に構築されたコード・スニペットとともに保存する:

各APIアドレスを動的に復号化し、そのアドレスに制御を転送するコード・スニペットを構築する。
各APIアドレスを動的に復号化し、そのアドレスに制御を転送するコード・スニペットを構築する。

プロキシのコード・スニペットのひとつを紹介しよう:

APIを呼び出すために動的にビルドされたコード・スニペット
APIを呼び出すために動的にビルドされたコード・スニペット

マルウェアが使用するもう1つのアンチデバッグ・トリックは、これらのスニペットを格納するために割り当てるプライベート・ヒープ・ブロックの末尾に0xABABABシーケンスがあるかどうかをチェックすることです。デバッガがアタッチされている場合、このシーケンスが追加され、マルウェアはスニペットのアドレスをカスタムインポートテーブルに保存しないため、後にデバッグされたサンプルがクラッシュすることになります。

マルウェアチェック
マルウェアは、デバッガを明らかにする0xABABABシーケンスの存在をチェックする。

文字列は通常、使用される直前にその場で復号化される:

ダイナミックAPI

IDAPythonの機能を使えば、そのほとんどを自動的に見つけて解読することができる:

自動復号化

SOFTWARE\Microsoft\Cryptography
MachineGuid
__ProviderArchitecture
ROOT\CIMV2
ID
SELECT * FROM Win32_ShadowCopy
WQL
Win32_ShadowCopy.ID='%s'
Global\%.8x%.8x%.8x%.8x
Times New Roman
.bmp
Control Panel\Desktop
WallPaper
WallpaperStyle
Z:\
dllhost.exe
Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}
%s.README.txt
Control Panel\International
LocaleName
sLanguage
SOFTWARE\Microsoft\Windows NT\CurrentVersion
ProductName
%.8x%.8x%.8x%.8x%
POST
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
%s=%s
%s=%s
%.8x%.8x%.8x%.8x%
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
%u.%u
%u.%u
\\%s\
LDAP://rootDSE
defaultNamingContext
LDAP://CN=Computers,
dNSHostName
\\%s\
ExchangeInstallPath
Program Files
Mailbox
SOFTWARE\%s
hScreen

構成

サンプルの暗号化されたコンフィギュレーションは.rsrcセクションに格納され、さらに圧縮され、個々のフィールドはbase64エンコードされている。復号化されたC2コンフィギュレーションを以下に示します。このサンプルは、C2のセットによって証明されるように、プレーンHTTPとHTTPSの両方のエンドポイントとやり取りすることができます。

設定の復号化とBase64エンコードされたC2
設定の復号化とBase64エンコードされたC2

マルウェアはこれらのC2と通信する際に、ランダムなHTTPクエリ値を生成する:

C2の1つとのネットワーク通信
C2の1つとのネットワーク通信

通信の安全性を確保するために、AESアルゴリズムが使用されている。

平文での対象システムの詳細
平文での対象システムの詳細

以下は抽出されたコンフィギュレーションである:

{
  "SHA256_SAMPLE": "706F3EEC328E91FF7F66C8F0A2FB9B556325C153A329A2062DC85879C540839D",
  "RSA_KEY": "232FBA5316E1C9A3F0E603EF0ECB534A1FC1E8BA5F89DBD886D98FBF88EEDDE66CC65E00BBB827CD0262B65C505D95A008C48427A73AE6EB888EB47A8A6246B43326931A7D59DFDAD141A054B445C51FBA1E3DF3F41CBA82AF44B96F21388C00DD696F7B3B976313C662B6283C0D082B5E68F3FFD7946A72C67F8A698172BE70",
  "COMPANY_VICTIM_ID": "90A881FFA127B004CEC6802588FCE307",
  "AES_KEY": "B59C952C492BD3D1F8F5140AA2855CDE",
  "BOT_MALWARE_VERSION": "2.0",
  "ODD_CRYPT_LARGE_FILES": "false",
  "NEED_MAKE_LOGON": "true",
  "MOUNT_UNITS_AND_CRYPT": "true",
  "CRYPT_NETWORK_RESOURCES_AND_AD": "true",
  "TERMINATE_PROCESSES": "true",
  "STOP_SERVICES_AND_DELETE": "true",
  "CREATE_MUTEX": "true",
  "PREPARE_VICTIM_DATA_AND_SEND": "true",
  "PRINT_RANSOM_NOTE": "true",
  "PROCESS_TO_KILL": [
    { "": "encsvc" },
    { "": "thebat" },
    { "": "mydesktopqos" },
    { "": "xfssvccon" },
    { "": "firefox" },
    { "": "infopath" },
    { "": "winword" },
    { "": "steam" },
    { "": "synctime" },
    { "": "notepad" },
    { "": "ocomm" },
    { "": "onenote" },
    { "": "mspub" },
    { "": "thunderbird" },
    { "": "agntsvc" },
    { "": "sql" },
    { "": "excel" },
    { "": "powerpnt" },
    { "": "outlook" },
    { "": "wordpad" },
    { "": "dbeng50" },
    { "": "isqlplussvc" },
    { "": "sqbcoreservice" },
    { "": "oracle" },
    { "": "ocautoupds" },
    { "": "dbsnmp" },
    { "": "msaccess" },
    { "": "tbirdconfig" },
    { "": "ocssd" },
    { "": "mydesktopservice" },
    { "": "visio" }
  ],
  "SERVICES_TO_KILL": [
    { "": "mepocs" },
    { "": "memtas" },
    { "": "veeam" },
    { "": "svc$" },
    { "": "backup" },
    { "": "sql" },
    { "": "vss" },
    { "": "msexchange" }
  ],
  "C2_URLS": [
    { "": "https://mojobiden[.]com" },
    { "": "http://mojobiden[.]com" },
    { "": "https://nowautomation[.]com" },
    { "": "http://nowautomation[.]com" }
  ],
  "LOGON_USERS_INFORMATION": [
    { "": "" },
    { "": "" },
    { "": "" },
    { "": "" },
    { "": "" },
    { "": "" }
  ],
  "RANSOM_NOTE": [
    {
      "": "      ~+                                               \r\n               *       +\r\n         '    BLACK        |\r\n     ()   .-.,='``'=.    - o -         \r\n           '=/_       \\     |          \r\n        *   | '=._    |                \r\n             \\     `=./`,        '   \r\n          .   '=.__.=' `='      *\r\n +             Matter        +\r\n      O     *        '       .\r\n\r\n>>> Whathappens?\r\n   Your network is encrypted,and currently not operational. \r\n   Weneed only money, after payment we will give you a decryptor for the entirenetwork and you will restore all the data.\r\n\r\n>>> What datastolen?\r\n   From your network wasstolen 1000 GB of data.\r\n   If you donot contact us we will publish all your data in our blog and will send it tothe biggest mass media.\r\n   Blog postlink: http://.onion/\r\n\r\n>>>What guarantees? \r\n   We are not apolitically motivated group and we do not need anything other than your money.\r\n   If you pay, we will provide youthe programs for decryption and we will delete your data. \r\n   If we do not give you decrypters or we donot delete your data, no one will pay us in the future, this does not complywith our goals. \r\n   We always keep ourpromises.\r\n\r\n>> How to contact with us? \r\n   1. Download and install TOR Browser (https://www.torproject.org/).\r\n   2. Open http://.onion/\r\n  \r\n>> Warning! Recoveryrecommendations.  \r\n   We strongly recommend you to do not MODIFYor REPAIR your files, that will damage them."
    }
  ]
}

全体的に、被害者IDがMachineGuid値から導き出される方法、使用される暗号化技術、構成が構造化され保護される方法など、DarkSideランサムウェアファミリーと複数の類似点があります。DarkSide実行ファイルに関する詳細は、以前のブログでご覧いただけます。

BlackMatter ランサムウェア対策と侵害の兆候

Nozomi Networks 弊社のThreat Intelligence サービスをご利用のお客様は、すでにこの脅威に対してカバーされています。さらに、Nozomi Networks Labsは、この状況の進展を監視しており、お客様への補償を拡大し、主要なアップデートについてコミュニティに通知します。

重要インフラの運用を守るセキュリティ専門家にとって、ランサムウェアに対するサイバー耐性のための一般的な推奨事項は、当社の最新レポート(OT/IoT Security Report)に記載されています。

セキュリティ研究者にとって、BlackMatterがどのように分析を回避するか、そしてコードから重要な情報を抽出する方法について、このブログで提供された説明は、マルウェアが進化するにつれて役に立つはずである。

この分析からわかった妥協の指標(IOC)と、分析に使用したスクリプトは以下の通り。

IOC一覧

mojobiden.com ナウオートメーションドットコム 706f3eec328e91ff7f66c8f0a2fb9b556325c153a329a2062dc85879c540839d
// Created by Nozomi Networks Labs

import "pe"

rule blackmatter_ransomware : blackmatter ransomware {
    meta:
        date = "2021-09-20"
        name = "BlackMatter - RANSOMWARE"
        author = "Nozomi Networks Labs"
        description = "Generic detection for BlackMatter ransomware"
        actor = "BlackMatter"
        x_threat_name = "BlackMatter ransomware"
        x_mitre_technique = "T1486"
        hash1 = "706f3eec328e91ff7f66c8f0a2fb9b556325c153a329a2062dc85879c540839d"
        hash2 = "9cf9441554ac727f9d191ad9de1dc101867ffe5264699cafcf2734a4b89d5d6a"
        hash3 = "b0e929e35c47a60f65e4420389cad46190c26e8cfaabe922efd73747b682776a"
        hash4 = "2cdb5edf3039863c30818ca34d9240cb0068ad33128895500721bcdca70c78fd"
        hash5 = "f7b3da61cb6a37569270554776dbbd1406d7203718c0419c922aa393c07e9884"
        hash6 = "8f1b0affffb2f2f58b477515d1ce54f4daa40a761d828041603d5536c2d53539"
        hash7 = "e4a2260bcba8059207fdcc2d59841a8c4ddbe39b6b835feef671bceb95cd232d"
        nn_ts = "1632088800.0"
        nn_sig = "f7c69f3b527ffb3f0c2aa613e902d8d4f0e39966048bb6cfa57556115fa18ed9"
        nn_id = "92f90d15-9392-4076-96b5-1e42ac9874c5"

    condition:
        uint16(0) == 0x5a4d and
        uint32(uint32(0x3c)) == 0x00004550 and
        filesize < 100KB and
        pe.imphash() == "2e4ae81fc349a1616df79a6f5499743f"
}

IDAPythonスクリプト

以下は、マルウェアによって動的に入力されたカスタムインポートテーブルを復元するスクリプトです。このスクリプトは、カーソルが一括復号化関数の位置(このサンプルの場合、RVA 0x78EC)にあるときに押される新しいホットキーZを定義します。

# Author: Alexey Kleymenov (a member of Nozomi Networks Labs)

import os
import struct
import pefile
import ida_kernwin

PATH_TO_DLLS = 'c:\\windows\\system32\\'
HARDCODED_XOR_KEY = 0x17019FF8

def extract_api_hashes(start):
    '''
    Returns a dictionary where keys are import functions to write data and values are list of hashes.
    The first hash is the DLL name's hash, the rest are WinAPI names' hashes.
    '''
    decryptor_address = start
    print('Bulk API decryptor address: %x' % decryptor_address)

    api_hashes = {}
    for head in Heads():
        flags = GetFlags(head)
        if isCode(flags):
            prev = prev_head(head)
            prev_2 = prev_head(prev)

            if print_insn_mnem(head) == 'call' and get_operand_value(head, 0) == decryptor_address:
                print('Found the decryptor called: %x' % head)

                if print_insn_mnem(prev) == 'push' and print_insn_mnem(prev_2) == 'push':
                    func_hashes = get_operand_value(prev_2, 0)
                    import_table = get_operand_value(prev, 0)
                    api_hashes[import_table] = []

                    for i in range(0, 0xffff, 4):
                        api_hash = struct.unpack("<I", get_bytes(func_hashes + i, 4))[0]
                        if api_hash == 0xCCCCCCCC:
                            break
                        else:
                            api_hashes[import_table].append(api_hash ^ HARDCODED_XOR_KEY)
                else:
                    print('Non-standard arguments %x' % head)
    return api_hashes

def calculate_checksum(name, value):
    '''Standard ror 0x0D'''
    for symbol in name:
        value = ((value >> 0x0D) | (value << (0x20 - 0x0D))) & 0xFFFFFFFF
        value += ord(symbol) & 0xFFFFFFFF
    return value

def build_mappings(dll_filepath, dll_hashes):
    '''
    Calculates API checksums for the DLLs of interest
    '''
    dll_name = os.path.basename(dll_filepath)
    dll_checksum = calculate_checksum(dll_name.lower() + '\x00', 0)
    result = {}

    if dll_checksum in dll_hashes:
        dll = pefile.PE(dll_filepath, fast_load=True)
        dll.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_EXPORT']])

        if hasattr(dll, 'DIRECTORY_ENTRY_EXPORT'):
            dll_name = dll_name.replace('.', '_')
            result[dll_checksum] = {'dll_name': dll_name}
            export_directory = dll.DIRECTORY_ENTRY_EXPORT

            for symbol in export_directory.symbols:
                if symbol.name is not None:
                    api_name = symbol.name.decode('latin-1')
                    api_checksum = calculate_checksum(api_name + '\x00', dll_checksum)
                    result[api_checksum] = {'dll_name': dll_name, 'api_name': api_name}
    return result

def parse_dlls(path_to_dlls, dll_hashes):
    '''
    Walks all files in the given path and builds export hash mappings
    '''
    list_dlls = os.listdir(path_to_dlls)
    mappings = {}

    for dll_filename in list_dlls:
        full_path = os.path.join(path_to_dlls, dll_filename)
        mappings.update(build_mappings(full_path, dll_hashes))

    return mappings

def decrypt_all():
    '''
    Should be run with the cursor at the bulk decryption function
    '''
    start = get_screen_ea()
    api_hashes = extract_api_hashes(start)
    dll_hashes = []

    for _, hashes in api_hashes.items():
        dll_hashes.append(hashes[0])

    dll_mappings = parse_dlls(PATH_TO_DLLS, dll_hashes)

    for import_table, hashes in api_hashes.items():
        dll_hash = hashes[0]
        api_hashes = hashes[1:]

        if dll_hash in dll_mappings:
            print('Found DLL hash %x = %s' % (dll_hash, dll_mappings[dll_hash]['dll_name']))

            for i, api_hash in enumerate(api_hashes):
                if api_hash in dll_mappings:
                    addr = import_table + (i + 1) * 4
                    print('Found API hash for %x = %s (%s)' % (
                        addr,
                        dll_mappings[api_hash]['api_name'],
                        dll_mappings[api_hash]['dll_name']
                    ))
                    set_name(addr, dll_mappings[api_hash]['api_name'])
                else:
                    print('API hash %x not found' % api_hash)
        else:
            print('DLL hash %x not found' % dll_hash)

ida_kernwin.add_hotkey("z", decrypt_all)


# Additional: Search & Decrypt Encrypted Strings

# Author: Alexey Kleymenov (a member of Nozomi Networks Labs)

import struct
import ida_kernwin

HARDCODED_XOR_KEY = 0x17019FF8

def is_utf16_heur(string):
    counter = 0
    for val in string:
        if val == 0:
            counter += 1
    if counter / float(len(string)) > 0.4:
        return True
    return False

def decrypt_string(start_addr):
    addr = start_addr
    result = b""

    for i in range(0xFFFF):
        instr = print_insn_mnem(addr)
        if instr != 'mov' or 'dword ptr' not in GetDisasm(addr):
            break

        value = get_operand_value(addr, 1)
        decoded_value = value ^ HARDCODED_XOR_KEY
        result += struct.pack("<I", decoded_value)
        addr = next_head(addr)

    result_orig = result

    if is_utf16_heur(result):
        result = result.decode('utf-16le')
    else:
        result = result.decode('latin-1')

    if all(ord(c) < 128 for c in result):
        result = result.rstrip('\x00')
    else:
        result = 'hex: ' + result_orig.hex()

    print('%x - %s' % (start_addr, result))
    set_cmt(start_addr, result, 0)

def decrypt_string_manual():
    start_addr = get_screen_ea()
    decrypt_string(start_addr)

def search_for_encrypted_strings():
    for head in Heads():
        flags = GetFlags(head)
        if isCode(flags):
            if print_insn_mnem(head) == 'xor' and 'dword ptr' in GetDisasm(head) and get_operand_value(head, 1) == HARDCODED_XOR_KEY:
                next = next_head(head)
                if print_insn_mnem(next) == 'add' and get_operand_value(next, 1) == 4:
                    prev = prev_head(head)
                    if 'mov     ecx' in GetDisasm(prev):
                        num = get_operand_value(prev, 1)
                        for i in range(num):
                            prev = prev_head(prev)
                        # print('Found the encryption string candidate: %x' % prev)
                        decrypt_string(prev)

ida_kernwin.add_hotkey(",", decrypt_string_manual)
search_for_encrypted_strings()

参考文献

  1. https://github.com/advanced-threat-research/DarkSide-Config-Extract