
Нередко в процессе реверс-инжиниринга мы сталкиваемся с STL-кодом, анализ которого на первый взгляд кажется затруднительным. Неопытный глаз может принять этот код за полезный и потратить время на анализ какого-нибудь конструктора.
На самом деле, здесь важно потратить время на то, чтобы распознать STL-контейнер по косвенным признакам, быстро понять, где какие данные лежат, типизировать их и идти дальше. В новой серии публикаций поговорим о самых распространенных контейнерах STL и начнем с std::vector.
Это Диана Фишман из экспертного центра безопасности Positive Technologies (PT ESC). Будет сложно.
Паттерн 1. Три последовательных записи указателей
std::vector — один из самых простых контейнеров. Он занимает в памяти 24 байта в 64-битных сборках. Если открыть исходный код std::vector в MSVC STL, мы увидим класс _Vector_val (рис. 1), который хранит ровно три указателя — начало (_Myfirst) и конец данных (_Mylast), а также конец выделенной памяти (_Myend).

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

Без исходника видно, что общая картина выглядит скомкано. Слева в ассемблерном коде мы видим кусок кода:
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 байта. Вот как выглядит вектор на стеке:

Это облегчает нам задачу. Мы пока предполагаем, что работаем с вектором, но все еще есть шанс, что мы захотим зареверсить sub_140002860. Посмотрим на второй паттерн, осознаем себя и не станем этого делать.
Паттерн 2: push_back и проверка на заполненность вектора
В C++ вектор push_back() — это встроенный метод, используемый для добавления нового элемента в конец вектора. Он автоматически изменяет размер вектора, если для размещения нового элемента недостаточно места. Стоит отметить, что это не единственный и не основной способ заполнения вектора, но в данном посте мы рассмотрим его как конкретную операцию, в которой проявляется основной паттерн.
Обычно, если используется данный метод, реализация в псевдокоде будет выглядеть примерно так:
if (_Mylast == _Myend) push_back_func(); else construct(_Mylast, value); ++_Mylast;
Код проверяет, есть ли место для нового элемента в векторе. Если нет, то оно выделяется и элемент добавится в конец вектора. Если есть, значение добавляется по адресу, на который сейчас указывает _Mylast.

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

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

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

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

Диана Фишман
Специалист группы проактивного поиска и анализа угроз @PT ESC
(Источник: https://t.me/ptescalator)
Wi11k4
Великолепная статья, а комментариев нету! Интересно было бы почитать про более сложные контейнеры, древовидные типо std::map. Как-то разбирал в игре коллекцию обьектов, кучи непонятных алокаций, катание байтов туда-сюда, около недели потратил, а оказалось всего-то рекурсивно можно было пробежать в обе стороны(в каждом объекте была ссылка на след. и пред. идущий объект) не ковыряясь в этих функциях :)