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