ESCalator
7.35K subscribers
554 photos
3 videos
18 files
223 links
Tips and tricks от команды экспертного центра безопасности Positive Technologies (PT ESC)
Download Telegram
Распознаем STL-код легко 😐

Нередко в процессе реверс-инжиниринга мы сталкиваемся с STL-кодом, анализ которого на первый взгляд кажется затруднительным. Неопытный глаз может принять этот код за полезный и потратить время на анализ какого-нибудь конструктора.

На самом деле здесь важно потратить время на то, чтобы распознать STL-контейнер по косвенным признакам, быстро понять, где какие данные лежат, типизировать их и идти дальше. В новой серии постов мы расскажем о самых распространенных контейнерах STL и начнем с std::vector.

Паттерн 1️⃣: три последовательных чтения/записи указателей

std::vector — один из самых простых контейнеров. Он занимает в памяти 24 байта в 64-битных сборках. Если открыть исходный код std::vector в MSVC STL, мы увидим класс _Vector_val (скриншот 1), который хранит ровно три указателя — начало (_Myfirst) и конец данных (_Mylast), а также конец выделенной памяти (_Myend).

Именно эта тройка и определяет схему памяти вектора. Компилятор расставляет их в памяти последовательно (смещения 0, 8, 16 байт в 64-битном коде). Поэтому в ассемблере мы всегда будем видеть обращения по этим смещениям — это наш главный ориентир.

Сразу посмотрим на простой пример на скриншоте 2. Не имея исходника, видно, что общая картина выглядит скомкано. Слева в ассемблерном коде мы видим кусок кода:

xorps xmm1, xmm1 ; и xmm1
movdqu xmmword ptr [rbp+57h+var_68], xmm1 ; записываем 16 нулевых байт по адресу var_68
xor r15d, r15d ; обнуляем r15d
mov [rbp+57h+var_58], r15 ; записываем 8 нулевых байт по адресу var_58


Компилятор обращается к членам вектора как к смещениям относительно базового адреса. Базовый адрес здесь в локальной переменной var_68. Первые 16 байт обнуляются: по смещению var_68 лежит _Myfirst, по смещению var_60_Mylast. Затем обнуляется _Myend, который находится по смещению var_58.

Таким образом, мы видим инициализацию пустого вектора, выполняемую вместо вызова конструктора по умолчанию. Проверяем размер вектора — 16 + 8 = 24 байта. На скриншоте 3 видно, как выглядит вектор на стеке.

Это облегчает нам задачу. Мы пока предполагаем, что работаем с вектором, но все еще есть шанс, что мы захотим зареверсить sub_140002860. Посмотрим на второй паттерн, осознаем себя и не станем этого делать.

Паттерн 2️⃣: push_back и проверка на заполненность вектора

В C++ вектор push_back() — это встроенный метод, используемый для добавления нового элемента в конец вектора. Он автоматически изменяет размер вектора, если для размещения нового элемента недостаточно места. Стоит отметить, что это не единственный и не основной способ заполнения вектора, но в данном посте мы рассмотрим его как конкретную операцию, в которой проявляется основной паттерн.

Обычно, если используется данный метод, реализация в псевдокоде будет выглядеть примерно так:

if (_Mylast == _Myend)
push_back_func();
else
construct(_Mylast, value);
++_Mylast;


Код проверяет, есть ли место для нового элемента в векторе. Если нет, то оно выделяется и элемент добавится в конец вектора. Если есть, значение добавляется по адресу, на который сейчас указывает _Mylast. Посмотрим в наш пример на скриншоте 4.

👀 Видим, что паттерн просматривается. Есть сравнение указателей _Mylast и _Myend, за которым следует вызов функции sub_140002860, в которую передаются наши указатели и целевое значение. Если мы зайдем в sub_140002860, первое, что мы увидим, будет вычисление размера вектора — size() = (_Mylast - _Myfirst) / sizeof(T) (скриншот 5). То есть функции, чтобы понять, нужно ли выделять новую память, требуется вычислить новый размер вектора и сравнить его с текущей вместимостью. Для нас это подсказка — мы смотрим в функцию push_back.

🤝 Теперь, когда понятно, что перед нами вектор, можем воссоздать его структуру (скриншот 6) и структуру, которая его использует, и разметить их в IDA (скриншот 7). Выглядит немного лучше и понятнее.

Несмотря на наличие и других конструкторов и методов у std::vector, знание его внутреннего расположения в памяти позволяет легко идентифицировать вектор в скомпилированном коде.

Stay tuned!

#tip #reverse
@ptescalator (X, Max)
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
16👍11🔥10🤯31
Путаница в уязвимостях WSUS: ставим все на свои места 🕷

Одной из самых актуальных уязвимостей в Windows Server Update Services (WSUS) стала критическая ошибка с идентификатором CVE-2025-59287 и оценкой CVSS 9.8. Она связана с десериализацией недоверенных данных в службе обновления Windows Server и позволяет неавторизованному удаленному злоумышленнику выполнить код на сервере, отправив специально сформированное событие.

В разборах эксплуатации шаги были указаны некорректно, так как их взяли из статьи. Однако позже авторы сами ее исправили и указали, что разбор относится к CVE-2023-35317, тогда как анализ CVE-2025-59287 перенесли в отдельную статью.

Это вызвало путаницу в многочисленных репостах, поэтому мы решили расставить все точки над i и заодно показать, как атакующий может восстановить оснастку после эксплуатации уязвимости.

❗️ Напомним технические детали эксплуатации

Условия для эксплуатации:

• Сервер Windows с включенной ролью WSUS Server (по умолчанию отключена)
• Неустановленные обновления KB5070879 / KB5070881 / KB5070882 / KB5070883 / KB5070884 / KB5070886 / KB5070887
• Сетевой доступ к портам 8530 (HTTP) или 8531 (HTTPS)
• Учетные данные не требуются

Техническая цепочка эксплуатации:

1️⃣ Получение конфигурации — злоумышленник отправляет запрос к /ReportingWebService/ReportingWebService.asmx для получения ServerID (скриншот 1)

2️⃣ Извлечение cookies — используя ServerID, выполняется запрос к /SimpleAuthWebService/SimpleAuth.asmx для получения AuthorizationCookie (скриншот 2)

3️⃣ Получение криптографических данных — запрос к /ClientWebService/Client.asmx для извлечения временных меток и зашифрованной нагрузки (скриншот 3)

4️⃣ Доставка полезной нагрузки — финальный запрос отправляет событие с вредоносным сериализованным объектом, созданным через ysoserial.net с гаджетом TextFormattingRunProperties, с использованием события SynchronizationCompletedCancel (скриншот 4)

При обработке события с ID 389 (SynchronizationCompletedCancel) WSUS пытается десериализовать XML с телом ошибки, что приводит к выполнению произвольного кода.

Также в процессе эксплуатации регистрируется ложный узел с указанным DNS, в рамках которого отправляется событие. Этот процесс легко автоматизировать — например, с помощью шаблона Nuclei (скриншот 5). Восстановление оснастки после тестирования — на скриншоте 6.

🗿 После успешной эксплуатации оснастка WSUS ломается. Это происходит потому, что GUI при отображении узлов парсит их события, а у ложного узла событие повреждено и не поддается десериализации. В результате отображение узлов вызывает исключение и интерфейс перестает работать.

Для восстановления необходимо удалить вредоносное событие из таблицы tbEventInstance в базе данных SUSDB. Однако прямой доступ к БД есть только у привилегированных пользователей, а служба WSUS часто запущена от имени Network Service.

При этом служба WSUS имеет доступ к функциям, используемым в оснастке, поэтому пользователь, от имени которого получена сессия (даже Network Service), может управлять зарегистрированными узлами. Это позволяет безопасно завершить эксплуатацию и сразу восстановить оснастку.

$wsus = Get-WsusServer

Get-WsusComputer -NameIncludes "test1337.test.local" | ForEach-Object {
$wsus.GetComputerTarget($_.Id).Delete()
}


Эта команда выполняет две важные функции:

• Удаляет зарегистрированный ложный узел из базы данных WSUS
• Автоматически очищает связанное с ним событие с ID 389, использованное для доставки полезной нагрузки

🧐 Почему это важно: без очистки в WSUS остаются артефакты — ложный узел и события в логах, что может нарушить нормальную работу службы.

Эта уязвимость демонстрирует, как критические компоненты инфраструктуры могут становиться точкой входа для злоумышленников. При проведении тестирования на проникновение важно не только атаковать, но и корректно восстанавливать систему после эксплуатации.

Рекомендации — в посте ниже 🔽

#offensive #nuclei #wsus #cve
@ptescalator (X, Max)
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥7💯63💋1
ESCalator
Путаница в уязвимостях WSUS: ставим все на свои места 🕷 Одной из самых актуальных уязвимостей в Windows Server Update Services (WSUS) стала критическая ошибка с идентификатором CVE-2025-59287 и оценкой CVSS 9.8. Она связана с десериализацией недоверенных…
Рекомендации по защите к посту выше 🆙

Установите обновленияKB5070879 / KB5070881 / KB5070882 / KB5070883 / KB5070884 / KB5070886 / KB5070887 в зависимости от версии Windows Server

Ограничьте сетевой доступ — WSUS не должен быть доступен из интернета. Используйте сегментацию сети для ограничения доступа только доверенным подсетям

Если невозможно обновить — временно отключите роль WSUS Server или заблокируйте порты 8530 / 8531 до установки патчей

#offensive #nuclei #wsus #cve
@ptescalator (X, Max)
6👍61🔥1💋1
Please open Telegram to view this post
VIEW IN TELEGRAM
5😁3🤔3🍾2
🫣 Скрываясь на виду: как PhantomCore маскирует свою активность с помощью легитимных инструментов

Команда PT ESC IR представила новое исследование, посвященное деятельности хакерской группы PhantomCore. Оно базируется на реальных кейсах расследования инцидентов, произошедших в период с осени 2025 года по первый квартал 2026 года, и содержит детальный разбор атак.

В материале поэтапно восстанавливается вся цепочка компрометации: от эксплуатации критических уязвимостей в TrueConf Server для получения первоначального доступа и методов продвижения по сети с кражей учетных данных с помощью легитимных DFIR-инструментов — до механизмов закрепления в инфраструктуре. На этом этапе атакующие комбинируют собственные бэкдоры (MacTunnelRAT, PhantomSscp, PhantomProxyLite) с инструментом для расследований Velociraptor, что позволяет им эффективно маскировать вредоносную активность под легитимные действия специалистов по информационной безопасности.

🚇 Одним из новых и интересных инструментов является MacTunnelRAT — PowerShell-скрипт, предназначенный для создания обратного SSH-туннеля. Он работает только на скомпрометированных хостах и расшифровывает список URL C2-серверов, используя MAC-адрес первого физического сетевого интерфейса.

function Get-MacAddressAsUUID {
try {
$adapter = Get-NetAdapter | Where-Object { $_.InterfaceType -eq 6 } | Select-Object -First 1
if ($adapter) {
return $adapter.MacAddress
}
return ""
} catch {
return ""
}
}


В обновленной версии появилась возможность обновления URL C2-серверов (на скриншоте).

Подробно про MacTunnelRAT и другие актуальные техники группы PhantomCore читайте в нашем блоге 😮

#dfir #ir #malware
@ptescalator
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥179👍7😁1
Почему IDA не сворачивает константы и как это исправить 👨‍💻

В последнее время в ВПО все чаще встречается обфускация, где константы заменяют на арифметические выражения. Подобный прием, например, есть в OLLVM.

😑 В чем тут проблема: часть констант, из которых собирается итоговое выражение, хранится в памяти. Из-за этого IDA по умолчанию не может нормально применить constant propagation и автоматически «свернуть» такие конструкции в исходные значения (скриншот 1).

Особенно неприятно, когда таким способом маскируют еще и адреса вызываемых функций. На практике это выливается в конструкции вида:

mov   <reg>, <const>
add <reg>, cs:<offset_*>
call <reg>


Для статического анализа это, конечно, лишняя головная боль 🧱

Но есть нюанс: если явно указать, что данные по адресу cs:<offset_*> — это константные qword/dword, то во время декомпиляции IDA сама подтянет значение из памяти, пересчитает выражение и свернет его обратно в исходную константу.

Перед тем как явно проставлять таким данным const, стоит убедиться, что значение действительно readonly. Один из простых способов — посмотреть на перекрестные ссылки: если все обращения к адресу идут только на чтение, то это хороший кандидат на const (скриншот 2).

Заодно полезно отфильтровать значения, которые на самом деле являются указателями, чтобы не испортить типизацию (скриншот 3).

😎 Все это можно автоматизировать небольшим IDAPython-скриптом (IDA 9.0+), который проходит по сегменту .data (или любому другому нужному сегменту), находит значения, используемые только на чтение, исключает те, что похожи на адреса, и явно проставляет им const byte/word/dword/qword в зависимости от размера.

В результате псевдокод становится заметно чище и понятнее (скриншот 4).

#tips #reverse
@ptescalator (X, Max)
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10👏6👍51
ESCalator
Почему IDA не сворачивает константы и как это исправить 👨‍💻 В последнее время в ВПО все чаще встречается обфускация, где константы заменяют на арифметические выражения. Подобный прием, например, есть в OLLVM. 😑 В чем тут проблема: часть констант, из которых…
А вот и этот IDAPython-скрипт:

import ida_bytes
import ida_segment
import ida_typeinf
import idautils
import ida_xref
import idaapi
from collections import namedtuple

PTR_SIZE = 8 if idaapi.inf_is_64bit() else 4


TypeInfo = namedtuple('SizeInfo', ['get_data', 'typename', 'badaddr'])

SIZE_TO_TYPE_INFO_MAP = {
1: (TypeInfo(ida_bytes.get_byte, "byte", 0xFF)),
2: (TypeInfo(ida_bytes.get_word, "word", 0xFFFF)),
4: (TypeInfo(ida_bytes.get_dword, "dword", 0xFFFFFFFF)),
8: (TypeInfo(ida_bytes.get_qword, "qword", 0xFFFFFFFFFFFFFFFF))
}


def apply_const_type(ea: int) -> bool:
flags = ida_bytes.get_flags(ea)
if not ida_bytes.is_data(flags):
return False

item_size = ida_bytes.get_item_size(ea)
type_info = SIZE_TO_TYPE_INFO_MAP.get(item_size)
if type_info is None:
return False

if type_info.get_data(ea) == type_info.badaddr:
return False

typeinfo_str = "const " + type_info.typename
tif = ida_typeinf.tinfo_t(typeinfo_str)

ida_bytes.clr_op_type(ea, 0)
return ida_typeinf.apply_tinfo(ea, tif, 1)


def is_addr_readonly(target_addr: int) -> bool:
flags = ida_bytes.get_full_flags(target_addr)
return ida_bytes.has_xref(flags) and \
all(ref.type == ida_xref.dr_R for ref in idautils.XrefsTo(target_addr))


def apply_const_type_if_readonly(target_addr: int) -> bool:
return apply_const_type(target_addr) if is_addr_readonly(target_addr) else False


def data_is_offset(ea:int) -> bool:
item_size = ida_bytes.get_item_size(ea)
if item_size != PTR_SIZE:
return False

type_info = SIZE_TO_TYPE_INFO_MAP.get(item_size)
if type_info is None:
return False

value = type_info.get_data(ea)
return (idaapi.getseg(value)) is not None


def main():
print("Casting readonly memory values to consts ")
print("---------------------------------")

segment_name = ".data"

seg = ida_segment.get_segm_by_name(segment_name)
if not seg:
print("No {segment_name} segment")
return

ea = seg.start_ea
fixed_consts_num = 0

while ea < seg.end_ea:
ea_size = ida_bytes.get_item_size(ea)
if ea_size <= 0:
ea += 1
continue

if not data_is_offset(ea) and apply_const_type_if_readonly(ea):
fixed_consts_num += 1

ea += ea_size

print("---------------------------------")
print(f"Done. fixed = {fixed_consts_num} consts")
print("Decompile again to view results: F5")


if __name__ == "__main__":
main()


#tips #reverse
@ptescalator (X, Max)
👍11🤯6🔥3😁21
Please open Telegram to view this post
VIEW IN TELEGRAM
🤡8😁52🔥2🌭1