Как стать автором
Обновить
«Лаборатория Касперского»
Ловим вирусы, исследуем угрозы, спасаем мир

Взгляд с обратной стороны: как смотрит на код реверсер

Время на прочтение12 мин
Количество просмотров5.7K
Привет! Меня зовут Денис, я Lead Security Researcher в центре Global Research & Analysis Team (GReAT) — подразделении «Лаборатории Касперского», которое занимается целевыми вредоносами. Это значит, что их авторы не рассылают трояны всем подряд, а тщательно выбирают свои организации-жертвы. Иногда их «продукты» написаны интересно.

Мы в GReAT в буквальном смысле слова годами следим за командами, которые пишут такое, детально разбираем их зло, формируем отчеты для заказчиков, плюс иногда подкидывая идеи и продуктовым командам.

image

Эта статья написана по мотивам выступления на C++ Russia. Я хочу рассказать, как на код C++ смотрит реверсер и что он видит в этом комбайне прекрасном языке. Обычно разработчик идет от исходного кода к двоичному, а мы — наоборот. Ко мне и коллегам приезжают самплы — уже скомпилированные исполняемые (PE, ELF, etc.) файлы, возможно, какой-то байткод одного из intermediate languages или даже прошивка. И мы начинаем их разбирать. Как мне кажется, реверсеры и разработчики могли бы обогатить друг друга.

Я расскажу о реверсе С++ на трех примерах из реальной жизни.

  • Microcin: С с классами. Да, начнем даже не с С++. Вот таких разработок в нашей области действительно много. С с классами пока явно чемпион. Этот случай выделяется тем, что в итоге получилось заглянуть и во фрагменты исходников. Это очень редкий случай, обычно реверсеру такое не выпадает. За 8 лет с таким я сталкивался всего пару раз. Скриншоты, которые сделал программист, могут даже вызвать у вас ностальгию. Уровень разработчика целевых вредоносов бывает разным, иногда вот таким. Также в этом примере будет видно, как выглядит в бинаре самый примитивный вариант таблицы виртуальных функций vftable. Кратко остановимся на том, какие подходы мы с коллегами используем, чтобы анализировать кастомный код, не сильно путешествуя по стандартному рантайму (сигнатуры, построенные на коде функций, или же просто их сигнатуры).
  • ScrambleCross: С++ Linux ELF64. А вот этот пример как раз большая редкость. Под Linux таких развесистых троянов мало, тем более на современном С++. Посмотрим, к чему на нашей бинарной стороне приводит использование контейнеров (скажу сразу, ничего хорошего про ваши шаблоны мы с коллегами не думаем). Покажу, как мы работаем со структурами — это важная часть для нас, при разборе исполняемого файла важно правильно их разметить.
  • BluePants: C++ Windows PE+. Тоже современный С++, но под Windows. Еще более развесистый last stager, т. е. последний в цепочке заражения с функциональностью удаленного администрирования. Предыдущие стейджи, как правило, сильно многослойны и занимаются расшифровкой-антидетектом, эскалацией привилегий и т. п. В этом примере, как и в предыдущем, с нами RTTI, видимо, где-то по пути был dynamic_cast. А в случае BluePants из удобно читаемого есть еще и кастомный логгер.

Пример 1. Microcin: С с классами


Вероятнее всего, эти скриншоты с исходниками появились потому, что уважаемый разработчик, он же тестер, перед кем-то отчитывался о том, что текущую версию его решения, установленную на виртуалке, защитные продукты не детектируют. На самом деле это то, как обычно реверсер код_не_видит, но всегда интересно потом сверить свой анализ с настоящим исходником.

image

На момент появления этих скриншотов вредонос мы уже анализировали и помнили класс zmm. Понятно, что очень интересно было изучить и исходник на C с классами.
Здесь сначала идет создание некого уникального идентификатора (применен rand(), вызван без srand(), но GetTickCount и time(0) все же дадут ему условно уникальное значение). Пока что можно предположить создание сессионного ключа или чего-то похожего.

Функция Judge_Time — это планировщик. Изучать кастомные планировщики всегда очень забавно. По «команде» планировщика запускается некий Client_Processing. Больше в коде смотреть особо не на что — сверху гасятся ошибки, если планировщик не дал команду продолжать, то в бесконечном цикле следующая проверка повторится через пятисекундный таймаут, все.

Перейдем к следующему куску кода, который заставил меня почувствовать в разработчике родную душу. Виндовый API позволяет понять день недели из коробки, но тут подрезан код безумной функции, считающей день недели по текущей дате. Похоже, здесь закрался stackoverflow-based programming в стиле определения знака float через строку.

image

На скриншот попало дерево проекта, и в нем видно, что zlib решили вкомпилировать, а не использовать как внешнюю библиотеку. Во вредоносах так бывает довольно часто — берут zlib, mbed TLS, OpenSSL. Если все это слинковано в финальный бинарник и пострипано, то приходится разбираться с 300 килобайтами кода (и это совсем не предел, даже для не Delfi-кода), из которых 250 — какой-нибудь OpenSSL с коллегами. Честно говоря, при реверсе гораздо удобнее, если они приходят извне — из библиотек.

Еще на следующем скриншоте нашего первого примера видна многопоточность. В целом, если мы не говорим про ботнеты (а наш случай целевого вредоноса принципиально иной), такие программы не работают у массы пользователей. Они не высоконагруженные, даже их серверная часть. С такой функциональностью асинхронность или многопоточность вряд ли нужны. Тем не менее они регулярно встречаются. Я думаю, причина в том, что у разработчиков накоплены библиотеки, привычные подходы, и они просто не хотят сбивать прицел.

image

Мне даже искренне интересно, как уровень разработки по этому скриншоту оценят на Хабре. Мне кажется, что это такая история получается немного из XX века. (Да, я стараюсь быть вежливым со всеми, получается не всегда.)

Закончим с исходным кодом — на Хабре его нужно было показать, и перейдем к обещанному «как видит реверсер» — что же у того вредоноса в собранном PE-файле:

image

Давайте сразу договоримся, что здесь и далее я привожу интерфейс декомпилятора и состояние кода уже после работы аналитика с ним. Т. е. уже причесанный код, в котором размечены структуры данных и даны осмысленные имена. Поверьте, до этого анализа картина была совсем не такой красивой. Как водится, временами для того, чтобы уговорить декомпилятор, приходилось переходить в дизассемблер. И второй момент: полное покрытие кода в рамках статьи мы не сделаем, будем концентрироваться на местах в нем, которые хочется подсветить. Поехали.

Помните прекрасную функцию выше, которая определяла день недели по дате? Из кода (его нет на скриншоте) видно, что загрузчик (а по функциональности это именно он) умеет устраивать себе выходные — в конфиге можно отметить любые два дня на неделе, когда он не будет работать. В случае этого сампла вредонос почти соблюдает шаббат — пропускает субботу и воскресенье. Разве что календарные, а не от заката. И нет, на национальность авторов или жертв мы не намекаем. Просто, скорее всего, разработчик не хотел генерировать трафик по выходным, чтобы защитные системы не среагировали на такую подозрительную активность.

Еще здесь сразу видно, что у классов и TCPConnect, и zmm оставлен RTTI. Это тот редкий случай, когда не нужно было давать объектам осмысленные имена вместо автосгенерированных — они были видны сразу.

Заглянем в их vftable и почти ничего не найдем — буквально по одному деструктору, которые мы не стали переименовывать:

image

И получили очень странные автоимена. Здесь видна работа двух механизмов, которые помогают нам откинуть библиотечные функции и сконцентрироваться на анализе кастомного кода:

  • поиск библиотечных функций по двоичным сигнатурам;
  • их же поиск по хэшу, посчитанному от прототипа, сигнатуры функции (не путать с двоичной сигнатурой).


В случае наших деструкторов оба механизма дали сбой. Это простительно, потому что деструкторы и у TCPConnect, и у zmm слишком малы и не уникальны.

В качестве аналогии можете представить старые антивирусы. Так что здесь важно, чтобы в функции были уникальные сигнатуры. В настройках можно выставить, что функции короче такой-то длины не стоит пытаться узнавать.

Справедливости ради — для более крупных кусков кода (и в основном для традиционных, близких к С компиляторов) механизм поиска библиотечных функций по бинарным сигнатурам очень помогает. Подключаются так называемые библиотеки типов — описания в проприетарном формате.

На этот раз не будем сильно углубляться в распознавание библиотечных функций и вернемся к нашему resolver-у на первом скриншоте. Тут важный момент для реверса: при их создании разработчик часто не хочет показывать, например, импорт сетевых функций. Работа с сетью у какого-нибудь калькулятора выглядит подозрительно. Поэтому часто реализуется менее явный динамический импорт — получение адресов API функций «на лету». В нашем resolver очень типичная картина — LoadLibrary и GetProcAddress.

image

Результат работы — инициализированная таблица диспетчеризации в объекте. В нее постепенно резолвятся и заносятся адреса, Пока аналитик не пройдет весь этот код и не заполнит адреса, он не сможет понять, что происходит в коде дальше.

В первом сампле давайте посмотрим еще один забавный момент. Как я уже упоминал, по функциональности это загрузчик. Когда все адреса функций готовы и scheduler дает добро (будний день) на продолжение работы, он отправляется за следующим модулем, который получает в виде исполняемого файла. И начинается поиск экспорта из этого модуля. В нашем случае он называется TransFile.

image

Видите, разработчик здесь использует qsort и bsearch для сортировки найденных функций по именам и поиска нужной. Алгоритм, как мне кажется, неплох, но зачем использовать его там, где речь явно не идет о миллионах экспортов? Вероятно, автор где-то подрезал этот код или ему просто понравился подход. Когда TransFile найден, его запускают, работа загрузчика завершена.

Из этого примера видно, что разработчики даже целевых вредоносов далеко не всегда суперпрограммисты. Что они с большой вероятностью знают лучше среднего «гражданского» программиста — так это относительные кишки операционной системы: как в ней спрятаться, как закрепиться, подменить библиотеку и т. п.

Безусловно, бывают и очень интересные технические решения, но чаще всего просто наворачивают много этапов расшифровки-инжекта-загрузки. Один из давних классических способов, например, — взять совершенно легитимный софт, добавленный в white list, подложить в ту же директорию библиотеку со специфическим именем, она на старте пропатчит точку входа исходного процесса, чтобы легитимный софт не исполнялся дальше, и продолжит путь к финальному трояну. Через кучу таких распаковок, деобфускаций, расшифровок он в итоге оказывается в памяти системного процесса.

Достаточно С с классами, пойдемте ближе к C++.

Пример 2. ScrambleCross: С++ Linux ELF64


Второй пример — линуксовый. Здесь функция RunStageClient уже больше похожа на C++ — она красиво отображается в декомпиляторе. Нет, С++ не стал декомпилируемым, просто спасибо современным инструментам, которые делают предположения о том, как это могло бы выглядеть в исходнике. Как действительно выглядел исходник, мы узнаем, если попадается, допустим, необфусцированный сампл на C#.

image

В любом случае, здесь снова были оставлены символы. Все эти StageClient мы, можно сказать, получили «бесплатно», но не получили никаких структур. Это уже работа человека по эту сторону клавиатуры. Он должен объяснить декомпилятору, что там прилетает в виде аргумента. Перечислители для кодов ошибок здесь сделаны тоже вручную. Как правило, я их создаю, если количество кастомных кодов не переваливает за пару десятков.

Давайте посмотрим внутрь этого ELF-файла. В конструкторе ModuleMgr начинаются деревья. Предполагаю, что в исходниках это был словарик типа std::map. Но в ELF сама структура не сохранилась — ведь мы не на отладочную сборку смотрим, а на непострипанный файл. Так что создание ноды дерева уже с нас, изначально здесь были только оффсеты, не более того — никаких rbTree и _M_left.

image

Здесь видно, что у ModuleMgr есть метод — AddModule. Для удобства я сделал перечислитель с возможными ключами словаря (pShellMgr, pFileMgr).
Обратите внимание на vftable, например ShellMgr (у других модулей под управлением ModuleMgr он будет похож). Уже интереснее, чем в предыдущем примере с одними деструкторами и почти пустыми vftable.

image

OnModuleLoad, OnModuleUnload и т. д. будут дергаться менеджером по наступлении нужных событий. RTTI видно, кто на ком стоял — в частности, что ShellManager унаследовал от ModuleBase, а тот, в свою очередь, от Interface. Что нам нужно от vftable для ShellMgr в первую очередь — это сделать ссылку на эту таблицу первой же полянкой в структуре для ShellMgr. В итоге из кода можно будет красиво переходить на все методы OnModuleClose, OnLoaderOffline и т. д. Без этого разобраться в логике вредоноса будет затруднительно.

Без RTTI vftable все равно бы существовала, но это были бы безымянные функции. Их нахождение в одной таблице позволило бы дать имена вроде ShellMgr::Method1 (уже хорошо), дать первому аргументу типа указатель на объект ShellMgr, а вот поименовать методы более осмысленно получилось бы только после анализа того, что происходит внутри.

Давайте теперь посмотрим на метод AddModule — выше он уже у нас вызывался. Здесь хорошо видно, чем платит программист за использование контейнера-словаря. В этом коде сначала происходит поиск, потом — в случае успеха — добавление и балансировка дерева (все не влезло на скриншот).

image

Частый актуальный вопрос к реверсеру: если структура дерева не приехала сама, то как понять, что происходит в таком вот AddModule? Как понять, кто тут родитель, кто цвет? Здесь отчасти помогает опыт — если вы уже видели деревья пять раз, то шестые будут на них похожи. Также помогает grep по документации и компиляция своих кусков кода с контейнерами, которые, кажется, есть в коде вредоноса.

В принципе, можно поставить себе на виртуалку разные версии STL, угадывать нужную библиотеку и опции компилятора и в итоге создать сигнатуры для распознаваний функций. Честно скажу, до этих высот я еще не добрался. В случае разных языков больше пользуюсь grep-ом по исходникам, получая нечто, что логично накладывается на какие-нибудь функторы или деревья.

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

image

Теперь о главной боли. Вот эту сигнатуру я подрезал из сэмпла с C++.

image

Шаблоны. В виде одной строки для меня это какое-то безумие. Ладно бы для меня — главное, что так же про шаблонизированный код думают и инструменты, которые пытаются понять, относится ли эта штука к стандартной библиотеке. В шаблоны же можно подставить разные типы данных — и указатель на char, и кастомную 200-байтовую структуру. И в этом месте сигнатуры страдают. И ничего хорошего реверсеры про STL и шаблоны не думают.

В общем, приходится работать руками. Я несу в VSCode, чтобы расставить все табы и понять, кто на ком стоял.

В DataExchanger ситуация ровно та же: вижу имена, а структуру не вижу и делаю сам. В принципе, ничего нового по сравнению с деревьями, которые рассматривали выше.

image

Давайте в этом примере еще разберемся с Function_base. Функтором я буду называть некую обертку вокруг того кода, который хочется дернуть, содержащую какие-то дополнительные данные.

image

Потребовалась еще одна дополнительная структура, в поиске которой помогла документация. Главную полянку я назвал content, а аргументом для нее служит указатель на StageClient — наш главный объект. Есть еще вызыватель и менеджер, посмотрим во второго.

image

Все, что нужно было сделать внутри менеджера — взять из документации коды для get_functor, clone_functor. После этого становится понятно, за что этот менеджер отвечает. Попутно призываю обращать внимание на любые new() и аналоги — очень ценно, когда видно, сколько памяти выделяется. Например, может быть видно, что происходит инициализация полянки вложенной структуры. Бывший абстрактный оффсет во внешней структуре сначала получит имя buf80. Главное, что несколько полей в ней теперь логически связаны. Потом работа с полями этой вложенной структуры позволит дать ей и осмысленное имя.

Пример 3. BluePants: C++ Windows PE+


Бывает, что проблема не в многочисленных обертках вокруг вредоноса, а в том, что архитектор на той стороне решает разбить его на множество модулей, управляя их запуском через оркестратор. Буквально любую функцию трояна он может оформить в виде самостоятельного модуля: запустить скрипт powershell, сделать снимок экрана и т. п. Таких модулей могут быть десятки, и в этом примере я покажу, как это может быть реализовано на C++.

Посмотрим на модуль-диспетчер:

image

Что хорошо — здесь появляется полезный незаконченный логгер. Глобальное статическое хранилище сообщений.

New(), помогающий понять размер структур, здесь у компилятора получился интересным: значение указателя на журнализатор плюс 8. Смотрится странно, но это же все внутри if, и мы попадем в это выделение памяти, только если она еще не выделена, т. е. если логгера еще нет. Иными словами, размер все равно понятен — 8 байт. Это 64-битный сампл, так что в этом месте журнализатор получает свой указатель на таблицу виртуальных функций и не более того.

Давайте теперь зайдем в фабрику RatCore.

image

Как и все основные объекты в этом сампле, он глобальный. Размер виден в new — он достаточно крупный. Иногда приходится размечать и такие структуры, и когда аналитик их получает, там еще нет никаких векторов и строк — одни оффсеты.

Но мы в декомпиляторе смотрим на файл, с которым аналитик уже поработал. Видим, какие поля здесь инициализируются. Любой такой конструктор показывает не только общий размер структуры, но и набор полей, с них удобно начинать разметку. Но, конечно, сразу про вектора и строки понятно не станет, только уже дальше по коду.

А вот кусок, с которым я возился долго.

image

Здесь есть некая таблица диспетчеризации, это имя я достал из vftable.
В RatCore мы видели нечто похожее — снова фабрика с начальной инициализацией полей, но есть и особенность.

image

Видим очередной статический объект, new() и размер, завязанный на нулевой указатель. Новое по сравнению с двумя предыдущими примерами — две vftable. Почему таблицы две?


Выше в примере мы видели интерфейс, унаследованный от него ModuleBase и дальше, в свою очередь, уже от него ShellMgr, FileMgr. А тут мы видим множественное наследование. На уровне бинарного файла в RTTI оно будет выглядеть так:

image

Есть шаблонизированный Singleton (в этот раз там DispatcherTable) и еще отдельно DispatchInterface. Отсюда и две vftable. Поэтому когда аналитик будет создавать структуру, то оставит под них уже не одну 8-байтную полянку, а две.

Вернемся на предыдущий скриншот.

image

Вторая строка здесь — вызов DispatcherTable::Append. И снова это были просто оффсеты, но благодаря тому, что обе vftable были добавлены, этот Append и удалось найти.

Создав объект DispatcherTable, аналогично мы достали и AppendKey:

image

Дальше мне пришлось сильно поколдовать над Binder из STL. Как и в примере выше, я уносил этот фрагмент в VSCode, чтобы понять хотя бы, кто на ком стоит. В итоге удалось разобраться, что будет аргументами, а что — самой вызываемой функцией: Rat::CoreGetConfig. Важно было понять, где ключ, с которым такие методы добавляются в словарь. На этапе загрузки плагинов и вызова функций это очень поможет понять, что происходит. Плагины, кстати, тоже возвращают свои объекты этому оркестратору, после чего из их vftable оркестратор дергает методы.

Вместо итогов


Любим ли мы вредоносы на С++? Нет. Больше всего мы любим хороший plain C. Но когда C++ попадается — не беда, жить можно, как в примерах выше. За целевыми вредоносами мы наблюдаем годами. Забавно видеть, как из некоторых логирование со временем уходит. Видимо, их разработчики считают, что отладили продукт, и убирают соответствующие строки кода.

Все примеры выше специально подобраны с RTTI. Хуже ли С++ без RTTI для реверсера, чем все остальное? В принципе, нет. У всех более современных компиляторов наблюдается тенденция к статической линковке. И вот если в рантайме есть дженерики, распознавать которые автоматически непросто, то дело усложняется. C++ с этой точки зрения не самый плохой компилятор. Есть еще Delphi, Rust, Nim и Go. Иногда такие языки выбирают для обертки над следующим модулем, просто пытаясь сбить детект. Тогда достать следующий модуль не так сложно, как разобрать полноценный троян.

Хорошие ли программисты малварщики? Мне из моего опыта кажется, что, как правило, не очень. Но, безусловно, есть «приятные» исключения. Могли ли малварщики не использовать C++ в перечисленных случаях? Конечно, могли бы. Задача создания утилиты удаленного администрирования не настолько требовательна к производительности, как создание компьютерной игры. Мне кажется, они просто выбирают привычный язык и свои уже готовые библиотеки. Иногда эти наработки оказываются на С++. Что ж, будем работать с тем, что есть.

Ну и напоследок: если вам тоже интересно копаться в подобных задачах, приходите работать в нашу команду Core Technologies and B2B Development, где прямо сейчас мы ищем C++ разработчиков. Процесс найма у нас максимально упрощен – так что уже через пару дней сможете увидеть, как наши подходы реализованы изнутри :)

А если вдруг вы не до конца уверены в своих силах, то можете сперва проверить свои знания C++ в нашей игре про умный город.


>>>

Видео моего выступления на С++ Russia-2022:

Теги:
Хабы:
+8
Комментарии12

Публикации

Информация

Сайт
www.kaspersky.ru
Дата регистрации
Дата основания
Численность
5 001–10 000 человек
Местоположение
Россия