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

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

Это Диана Фишман из экспертного центра безопасности Positive Technologies (PT ESC). Будет сложно.

Паттерн 1. Три последовательных записи указателей

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

Рис. 1
Рис. 1

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

Сразу посмотрим на простой пример:

Рис. 2
Рис. 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, по смещению var60 — _Mylast. Затем обнуляется _Myend, который находится по смещению var_58.

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

Рис. 3
Рис. 3

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

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

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

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

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

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

Рис. 4
Рис. 4

На рисунке видим, что паттерн просматривается. Есть сравнение указателей _Mylast и _Myend, за которым следует вызов функции sub_140002860, в которую передаются наши указатели и целевое значение. Если мы зайдем в sub_140002860, первое, что мы увидим, будет вычисление размера вектора — size() = (_Mylast - _Myfirst) / sizeof(T):

Рис. 5
Рис. 5

То есть функции, чтобы понять, нужно ли выделять новую память, требуется вычислить новый размер вектора и сравнить его с текущей вместимостью. Для нас это подсказка — мы смотрим в функцию push_back.

Теперь, когда понятно, что перед нами вектор, можем воссоздать его структуру и структуру, которая его использует.

Рис. 6
Рис. 6

Разметим их в IDA. Выглядит немного лучше и понятнее.

Рис. 7
Рис. 7

Несмотря на наличие и других конструкторов и методов у std::vector, знание его внутреннего расположения в памяти позволяет легко идентифицировать вектор в скомпилированном коде. В следующих своих публикациях продолжим рассказ о наиболее популярных контейнерах STL. Stay tuned&safe!

Диана Фишман

Специалист группы проактивного поиска и анализа угроз @PT ESC

(Источник: https://t.me/ptescalator)

Комментарии (1)


  1. Wi11k4
    23.04.2026 17:12

    Великолепная статья, а комментариев нету! Интересно было бы почитать про более сложные контейнеры, древовидные типо std::map. Как-то разбирал в игре коллекцию обьектов, кучи непонятных алокаций, катание байтов туда-сюда, около недели потратил, а оказалось всего-то рекурсивно можно было пробежать в обе стороны(в каждом объекте была ссылка на след. и пред. идущий объект) не ковыряясь в этих функциях :)