Старый конь борозды не испортит: как стилер Pony крадет данные и где их потом искать
Если помните, недавно у нас выходила статья про молодой, но уже подающий надежды 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):
continue
print("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’ы и как можно упростить реагирование на инциденты с помощью реализованных нами инструментов.