Если помните, недавно у нас выходила статья про молодой, но уже подающий надежды data stealer Loki. Тогда мы подробно рассмотрели этот экземпляр (версия 1.8), получили представление о работе бота и освоили инструмент, облегчающий реагирование на события, связанные с этим ВПО. Для более полного понимания ситуации, давайте разберем еще одно шпионское ПО и сравним исследованных ботов. Сегодня мы обратим внимание на Pony более старый, но не менее популярный образец data stealerа. Никита Карпов, аналитик CERT-GIB, расскажет, как бот проникает на компьютер жертвы и как вычислить похищенные данные, когда заражение уже произошло.
Разбор функциональности бота
Впервые Pony был замечен в 2011 году и все еще продолжает использоваться. Как и в ситуации с Loki, популярность этого ВПО обусловлена тем, что несколько версий бота вместе с панелью администратора можно без проблем найти в сети. Например, здесь.
Экземпляр Pony, который мы будем изучать, защищен тем же самым упаковщиком, что и Loki, рассмотренный в предыдущей статье. По этой причине не будем еще раз останавливаться на процессе получения чистого ВПО и перейдем сразу к более интересным моментам. Единственное, что следует упомянуть перед разбором ВПО, ссылка на сервер, по которой мы определяем нужный PE-файл, оканчивается на gate.php, и это один из индикаторов Pony.
При исследовании дизассемблированного кода Pony обратим внимание на участок, содержащий главные функции. Интерес представляют две из них Initialize_Application и CnC_Func (названия функций переименованы в соответствии с их содержанием).
Ниже представлена функция Initialize_Application. Она отвечает за инициализацию необходимых элементов (библиотеки, привилегии и т.д.) и за похищение данных. В процессе работы ВПО несколько раз использует значение 7227 пароль к данному экземпляру бота. В Initialize_Application это значение используется для шифрования буфера, содержащего данные приложений, алгоритмом RC4.
Далее перейдем к декомпилированному коду функции CnC_Func и разберем ее алгоритм:
-
Буфер, полученный в результате работы функции Initialize_Application, передается в функцию BuildPacket, где собирается пакет данных для передачи на сервер.
-
По каждому URI из списка бот отправляет данные и ожидает подтверждения со стороны сервера. Если сервер не ответил 3 раза бот идет дальше.
-
После завершения первого списка CnC бот пытается загрузить и запустить дополнительное ВПО.
В общем доступе находится готовый билдер, который подтверждает функционал, полученный в процессе статического анализа декомпилированного кода. Пользователю предлагается ввести список URI, куда будут выгружаться похищенные данные, и список, откуда будет выгружаться дополнительное ВПО. Также пользователь может изменить пароль бота и имя дополнительного ВПО.
Pony атакует более сотни приложений, и, хотя у него есть функционал загрузчика, в основном Pony используется именно для похищения пользовательских данных. В таблице ниже перечислены все приложения, из которых бот может похитить данные.
ID |
Приложение |
ID |
Приложение |
ID |
Приложение |
0 |
System Info |
45 |
FTPGetter |
90 |
Becky! |
1 |
FAR Manager |
46 |
ALFTP |
91 |
Pocomail |
2 |
Total Commander |
47 |
Internet Explorer |
92 |
IncrediMail |
3 |
WS_FTP |
48 |
Dreamweaver |
93 |
The Bat! |
4 |
CuteFTP |
49 |
DeluxeFTP |
94 |
Outlook |
5 |
FlashFXP |
50 |
Google Chrome |
95 |
Thunderbird |
6 |
FileZilla |
51 |
Chromium / SRWare Iron |
96 |
FastTrackFTP |
7 |
FTP Commander |
52 |
ChromePlus |
97 |
Bitcoin |
8 |
BulletProof FTP |
53 |
Bromium (Yandex Chrome) |
98 |
Electrum |
9 |
SmartFTP |
54 |
Nichrome |
99 |
MultiBit |
10 |
TurboFTP |
55 |
Comodo Dragon |
100 |
FTP Disk |
11 |
FFFTP |
56 |
RockMelt |
101 |
Litecoin |
12 |
CoffeeCup FTP / Sitemapper |
57 |
K-Meleon |
102 |
Namecoin |
13 |
CoreFTP |
58 |
Epic |
103 |
Terracoin |
14 |
FTP Explorer |
59 |
Staff-FTP |
104 |
Bitcoin Armory |
15 |
Frigate3 FTP |
60 |
AceFTP |
105 |
PPCoin (Peercoin) |
16 |
SecureFX |
61 |
Global Downloader |
106 |
Primecoin |
17 |
UltraFXP |
62 |
FreshFTP |
107 |
Feathercoin |
18 |
FTPRush |
63 |
BlazeFTP |
108 |
NovaCoin |
19 |
WebSitePublisher |
64 |
NETFile |
109 |
Freicoin |
20 |
BitKinex |
65 |
GoFTP |
110 |
Devcoin |
21 |
ExpanDrive |
66 |
3D-FTP |
111 |
Frankocoin |
22 |
ClassicFTP |
67 |
Easy FTP |
112 |
ProtoShares |
23 |
Fling |
68 |
Xftp |
113 |
MegaCoin |
24 |
SoftX |
69 |
RDP |
114 |
Quarkcoin |
25 |
Directory Opus |
70 |
FTP Now |
115 |
Worldcoin |
26 |
FreeFTP / DirectFTP |
71 |
Robo-FTP |
116 |
Infinitecoin |
27 |
LeapFTP |
72 |
Certificate |
117 |
Ixcoin |
28 |
WinSCP |
73 |
LinasFTP |
118 |
Anoncoin |
29 |
32bit FTP |
74 |
Cyberduck |
119 |
BBQcoin |
30 |
NetDrive |
75 |
Putty |
120 |
Digitalcoin |
31 |
WebDrive |
76 |
Notepad++ |
121 |
Mincoin |
32 |
FTP Control |
77 |
CoffeeCup Visual Site Designer |
122 |
Goldcoin |
33 |
Opera |
78 |
FTPShell |
123 |
Yacoin |
34 |
WiseFTP |
79 |
FTPInfo |
124 |
Zetacoin |
35 |
FTP Voyager |
80 |
NexusFile |
125 |
Fastcoin |
36 |
Firefox |
81 |
FastStone Browser |
126 |
I0coin |
37 |
FireFTP |
82 |
CoolNovo |
127 |
Tagcoin |
38 |
SeaMonkey |
83 |
WinZip |
128 |
Bytecoin |
39 |
Flock |
84 |
Yandex.Internet / Ya.Browser |
129 |
Florincoin |
40 |
Mozilla |
85 |
MyFTP |
130 |
Phoenixcoin |
41 |
LeechFTP |
86 |
sherrod FTP |
131 |
Luckycoin |
42 |
Odin Secure FTP Expert |
87 |
NovaFTP |
132 |
Craftcoin |
43 |
WinFTP |
88 |
Windows Mail |
133 |
Junkcoin |
44 |
FTP Surfer |
89 |
Windows Live Mail |
Взаимодействие с сервером
Рассмотрим подробнее сетевое взаимодействие Pony. Как мы уже говорили, Pony сначала выгружает похищенные данные приложений на удаленный сервер, и индикатором такой коммуникации служит gate.php. После этого Pony просматривает второй список ссылок, откуда он пытается загрузить дополнительное ВПО на зараженный компьютер.
Для подтверждения того, что сервер получил и прочитал данные, бот должен получить в ответ строку STATUS-IMPORT-OK, иначе бот считает, что сервер не получил данные.
Данные, передаваемые на сервер, надежно защищаются шифрованием и компрессией. Защиту данных определяет заголовок, который идет перед ними. Стандартная защита пакета выглядит так:
-
Данные в чистом виде с заголовком PWDFILE0.
-
Сжатые данные с заголовком PKDFILE0. Для сжатия используется библиотека aPLib, работа которой основана на алгоритме компрессии LZW.
-
Зашифрованные данные с заголовком CRYPTED0 и ключом в виде пароля, например, 7227 или PA$$. Для шифрования используется алгоритм RC4.
-
Зашифрованные алгоритмом RC4 данные, ключ указан в первых 4 байтах.
Размер |
Значение |
Описание |
0x4 |
rc_4key |
Ключ для верхнего уровня шифрования |
0x12 |
REPORT_HEADER (PWDFILE0/ PKDFILE0/ CRYPTED0) |
Заголовок отчета о похищенных данных (normal/packed/crypted) 8 байт заголовок, и 4 байта контрольная сумма CRC32 |
0x4 |
Версия отчета |
Версия отчета о похищенных данных (константное значение 01.0) |
0x4 |
Размер модуля |
Заголовок модуля, присутствует у каждого модуля |
0x8 |
ID заголовка модуля (chr(2).chr(0)."MODU".chr(1).chr(1)) 2 байта, ключевое слово MODU, 1 байт, 1 байт |
|
0x2 |
ID модуля |
|
0x2 |
Версия модуля |
|
- |
Название системы пользователя |
Модуль module_systeminfo (module id = 0x00000000) Содержит информацию о системе пользователя |
0x2 |
Система x32 или x64 |
|
- |
Страна пользователя |
|
- |
Язык системы пользователя |
|
0x2 |
Является ли пользователь администратором |
|
- |
Значение MachineGuid из приложения WinRAR |
|
- |
Список модулей всех приложений |
По аналогии с модулем module_systeminfo записаны данные всех приложений |
Парсер сетевых коммуникаций
Как и для Loki, напишем парсер на Python, используя следующие библиотеки:
-
Dpkt для поиска пакетов, принадлежащих Pony, и работы с ними.
-
aPLib для декомпрессии данных.
-
Hexdump для представления данных пакета в хексе.
-
JSON для записи найденной информации в удобном виде.
Рассмотрим основные части алгоритма работы скрипта:
for ts, buf in pcap: eth = dpkt.ethernet.Ethernet(buf) if not isinstance(eth.data, dpkt.ip.IP): ip = dpkt.ip.IP(buf) else: ip = eth.data if isinstance(ip.data, dpkt.tcp.TCP): tcp = ip.data try: if tcp.dport == 80 and len(tcp.data) > 0: # HTTP REQUEST if str(tcp.data).find('POST') != -1: http += 1 httpheader = tcp.data continue else: if httpheader != "": pkt = httpheader + tcp.data req += 1 request = dpkt.http.Request(pkt) parsed_payload['Network'].update({'Request method': request.method}) uri = request.headers['host'] + request.uri parsed_payload['Network'].update({'CnC': uri}) parsed_payload['Network'].update({'User-agent': request.headers['user-agent']}) if uri.find("gate.php") != -1: parsed_payload['Network'].update({'Traffic Purpose': "Exfiltrate Stolen Data"}) parse(tcp.data, debug) elif uri.find(".exe") != -1: parsed_payload['Network'].update({'Traffic Purpose': "Download additional malware"}) print(json.dumps(parsed_payload, ensure_ascii=False, sort_keys=False, indent=4)) parsed_payload['Network'].clear() parsed_payload['Malware Artifacts/IOCs'].clear() parsed_payload['Compromised Host/User Data'].clear() parsed_payload['Applications'].clear() print("----------------------") if tcp.sport == 80 and len(tcp.data) > 0: # HTTP RESPONCE resp += 1 response = dpkt.http.Response(tcp.data) if response.body.find(b'STATUS-IMPORT-OK') != -1: AdMalw = True print('Data imported successfully') else: print('C2 did not receive data') print("----------------------") except(dpkt.dpkt.NeedData, dpkt.dpkt.UnpackError): continueprint("Requests: " + str(req))print("Responces: " + str(resp))
Поиск пакетов, связанных с Pony, аналогичен поиску пакетов Loki. Ищем все HTTP-пакеты. Парсим запросы, в которых находится информация бота. Остальные запросы фиксируются, но данные в них не обрабатываются. Если в ответ на запрос получена строка STATUS-IMPORY-OK отмечаем успешную выгрузку данных. Во всех других случаях считаем, что сервер не получил данные. Если после выгрузки данных найдены HTTP-запросы с URI, оканчивающимся на .exe отмечаем загрузку дополнительного ВПО.
Рассмотрим функцию, отвечающую за снятие всей защиты с данных и импорт модулей:
def process_report_data(data, debug): index = 0 if len(str(data)) == 0: return False elif len(str(data)) < 12: return False elif len(str(data)) > REPORT_LEN_LIMIT: return False elif len(str(data)) == 12: return True if verify_new_file_header(data): rand_decrypt(data) report_id = read_strlen(data, index, 8) index += 8 if report_id == REPORT_CRYPTED_HEADER: parsed_payload['Malware Artifacts/IOCs'].update({'Crypted': report_id.decode('utf-8')}) decrypted_data = rc4DecryptText(report_password, data[index:len(str(data))]) data = decrypted_data index = 0 report_id = read_strlen(data, index, 8) index += 8 if report_id == REPORT_PACKED_HEADER: parsed_payload['Malware Artifacts/IOCs'].update({'Packed': report_id.decode('utf-8')}) unpacked_len = read_dword(data, index) index += 4 leng = read_dword(data, index) index += 4 if leng < 0: return False if not leng: return "" if index + leng > len(str(data)): return False packed_data = data[index:index + leng] index += leng if unpacked_len > REPORT_LEN_LIMIT or len(str(packed_data)) > REPORT_LEN_LIMIT: return False if not len(str(packed_data)): return False if len(str(packed_data)): data = unpack_stream(packed_data, unpacked_len) if not len(str(data)): return False if len(str(data)) > REPORT_LEN_LIMIT: return False index = 0 report_id = read_strlen(data, index, 8) index += 8 if report_id != REPORT_HEADER: print("No header") return False version_id = read_strlen(data, index, 3) index += 8 if version_id != REPORT_VERSION: return False parsed_payload['Malware Artifacts/IOCs'].update({'Data version': version_id.decode('utf-8')}) hexdump.hexdump(data) report_version_id = version_id parsed_payload['Applications'].update({'Quantity': 0}) while index < len(data): index = import_module(data, index, debug) return data
После снятия обязательного шифрования, определяем метод снятия следующего уровня защиты в зависимости от заголовка. Если присутствует дополнительное шифрование с заголовком CRYPTED0 скрипт пытается подставить стандартный ключ, и при несоответствия ключа запрашивает файл ВПО, в котором находит используемый в этом боте пароль. Если заголовок данных PWDFILE0 начинаем импорт модулей приложений.
Для расшифровки мы использовали алгоритм RC4:
def rc4DecryptHex(key, pt): if key == '': return pt s = list(range(256)) j = 0 for i in range(256): j = (j + s[i] + key[i % len(key)]) % 256 s[i], s[j] = s[j], s[i] i = j = 0 ct = [] for char in pt: i = (i + 1) % 256 j = (j + s[i]) % 256 s[i], s[j] = s[j], s[i] ct.append(chr(char ^ s[(s[i] + s[j]) % 256])) decrypted_text = ''.join(ct) data = decrypted_text.encode('raw_unicode_escape') return data
Результат работы парсера представлен ниже. Парсер успешно снял шифрование, произвел декомпрессию и нашел похищенные данные. Следует отметить, что у каждого модуля есть несколько типов представления данных, в зависимости от найденной ботом информации. В нашем примере бот похитил данные Outlook и записал их с типом 7. На первый запрос сервер ответил боту, а остальные коммуникации не несли полезной информации.
В заключение давайте сравним исследованные data stealer'ы Pony и Loki и подведем итог. Список атакуемых приложений и у Pony, и у Loki примерно одинаков, но функционал Loki, особенно в новых версиях, шире, чем у Pony. Pony защищает все передаваемые данные в несколько уровней, что не дает определить без специального инструмента, какие именно данные похитил бот. Loki, в свою очередь, передает все данные в открытом виде, но без знания структуры запросов разобрать эти данные тоже довольно сложно.
Надеемся, эти две статьи помогли разобраться, какую опасность несут данные data stealerы и как можно упростить реагирование на инциденты с помощью реализованных нами инструментов.