113 50 20MB
Russian Pages [191]
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
От исходного кода к двоичным файлам: путь программы на C В этой главе мы изучим основы того, как компиляторы упаковывают двоичные файлы EXE из кода C, и методы выполнения системных процессов. Эти основные понятия помогут вам понять, как Windows компилирует C в программы и связывает их между системными компонентами. Вы также поймете структуру программы и рабочий процесс, которым должны следовать анализ вредоносного ПО. В этой главе мы рассмотрим следующие основные темы: - Простейшая программа для Windows на C - Компилятор C – генерация ассемблерного кода - Ассемблер – преобразование ассемблерного кода в машинный код - Компиляция кода - Компоновщик Windows — упаковка бинарных данных в формат Portable Executable (PE) - Запуск скомпилированных исполняемых файлов PE как динамических процессов Простейшая программа Windows на C Любое программное обеспечение разработано с учетом определенной функциональности. Эта функциональность может включать в себя такие задачи, как чтение внешних входных данных, их обработка в соответствии с ожиданиями инженера или выполнение определенной функции или задачи. Все эти действия требуют взаимодействия с базовой операционной системой (ОС). Программа, чтобы взаимодействовать с базовой ОС, должна вызывать системные функции. Практически невозможно разработать осмысленную программу, не использующую никаких системных вызовов. Кроме того, в Windows программисту при компиляции программы на C необходимо указать подсистему (подробнее об этом можно прочитать на https://docs.microsoft.com/enus/cpp/build/reference/subsystem-specify-subsystem); windows и console , вероятно, являются двумя наиболее распространенными. Давайте рассмотрим простой пример программы на C для Windows:
Здесь представлена максимально упрощенная программа на языке C для Windows. Его цель — вызвать функцию USER32!MessageBox() в точке входа функции main(), чтобы открыть всплывающее окно с
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
информационным заголовком и приветственным содержимым. Компилятор C - генерация ассемблерного кода Что интересно понять в предыдущем разделе, так это причину, по которой компилятор понимает этот код C. Во-первых, основная задача компилятора — преобразовать код C в код сборки в соответствии с соглашением о вызовах C/C++, как показано на рис. 1.1:
Для удобства и практичности следующие примеры будут представлены с инструкциями x86. Однако методы и принципы, описанные в этой книге, являются общими для всех систем Windows, а примеры компиляторов основаны на коллекции компиляторов GNU (GCC) для Windows (MinGW). Поскольку различные системные функции (и даже сторонние модули) имеют ожидаемый доступ в памяти к уровню памяти ассемблерного кода, существует несколько основных соглашений о вызовах двоичного интерфейса приложения (ABI) для простоты управления. Заинтересованные читатели могут обратиться к документам Microsoft о передаче аргументов и именовании (https://docs.microsoft.com/enus/cpp/cpp/argument-passing-and-naming-conventions). Эти соглашения о вызовах в основном касаются нескольких вопросов: - Позиция, в которой параметры размещаются по порядку (например, в стеке, в регистре, таком как ECX, или смешиваются для ускорения работы) - Объем памяти, занимаемый параметрами, если параметры необходимо сохранить - Занятая память, которая будет освобождена вызывающим или вызываемым абонентом Когда компилятор генерирует ассемблерный код, он распознает соглашения о вызовах системы, упорядочивает параметры в памяти в соответствии со своими предпочтениями, а затем вызывает адрес
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
памяти функции с помощью команды call. Следовательно, когда поток переходит к системной инструкции, он может правильно получить параметр функции по ожидаемому адресу памяти. Возьмем в качестве примера рис. 1.1: мы знаем, что функция USER32!MessageBoxA предпочитает соглашения о вызовах WINAPI. В этом соглашении о вызовах содержимое параметра помещается в стек справа налево, а память, освобождаемая для этого соглашения о вызовах, выбирает вызываемый объект. Таким образом, после помещения в стек 4 параметров, занимающих 16 байт в стеке (sizeof(uint32_t) x 4), код будет выполнен в USER32!MessageBoxA. После выполнения запроса функции вернитесь к следующей строке инструкции Call MessageBoxA с ret 0x10 и освободит 16 байт памяти из стека (т. е. xor eax, eax). Книга посвящена только тому, как компилятор генерирует сингл-чип инструкции и инкапсулирует программу в исполняемый файл. Он не включает важные части продвинутой теории компилятора, такие как создание семантического дерева и оптимизация компилятора. Эти части зарезервированы для читателей, чтобы изучить их для дальнейшего обучения. В этом разделе мы узнали о соглашении о вызовах C/C++, о том, как параметры размещаются в памяти по порядку и как освобождается память после завершения программы. Ассемблер — преобразование ассемблерного кода в машинный код В этот момент вы можете заметить, что что-то не так. Микросхемы процессоров, которые мы используем каждый день, не способны выполнять текстовый ассемблерный код, а вместо этого преобразуются в машинный код соответствующего набора инструкций для выполнения соответствующих операций с памятью. Таким образом, в процессе компиляции ранее упомянутый ассемблерный код преобразуется ассемблером в машинный код, понятный чипу. На рис. 1.2 показано распределение динамической памяти 32-битного PE:
Поскольку чип не может напрямую анализировать такие строки, как ПРИВЕТ МИР или ИНФО, данные (такие как глобальные переменные, статические строки, глобальные массивы и т. д.) сначала сохраняются в отдельной структуре, называемой секцией. Каждый раздел создается со смещенным адресом, где он должен быть размещен. Если позже коду потребуется извлечь ресурсы,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
идентифицированные в течение этих периодов компиляции, соответствующие данные можно получить по соответствующим адресам смещения. Вот пример: - Вышеупомянутая информационная строка может быть выражена как \x69\x6E\x66\x6F\x00 в коде ASCII (всего 5 байтов с нулем в конце строки). Двоичные данные этой строки можно хранить в начале раздела .rdata. Точно так же строка приветствия может храниться рядом с предыдущей строкой по адресу раздела .rdata со смещением +5. - На самом деле вышеупомянутый вызов MessageBoxA API не понимается чипом. Поэтому компилятор создаст структуру таблицы адресов импорта, которая является разделом .idata, для хранения адреса системной функции, которую хочет вызвать текущая программа. Когда это необходимо программе, соответствующий адрес функции может быть извлечен из этой таблицы, что позволяет потоку перейти к адресу функции и продолжить выполнение системной функции. - Вообще говоря, компилятор обычно хранит содержимое кода в секции .text. - Каждый отдельный запущенный процесс не имеет только одного PE-модуля. Либо *.EXE, либо *.DLL, смонтированные в памяти процесса, упакованы в формате PE. - На практике каждому модулю, загружаемому в память, должен быть присвоен базовый адрес образа для хранения всего содержимого модуля. В случае 32-разрядного *.EXE базовый адрес образа обычно равен 0x400000. - Абсолютным адресом каждого фрагмента данных в динамической памяти будет базовый адрес образа этого модуля + смещение раздела + смещение данных на разделе. В качестве примера возьмем базовый адрес образа 0x400000. Если мы хотим получить информационную строку, ожидаемое содержимое будет размещено по адресу 0x402000 (0x400000 + 0x2000 + 0x00). Точно так же ПРИВЕТ будет находиться по адресу 0x402005, а указатель MessageBoxA будет храниться по адресу 0x403018. Нет никакой гарантии, что на практике компилятор сгенерирует разделы .text, .rdata и .idata или что они будут использоваться для этих функций. Большинство компиляторов следуют ранее упомянутым принципам выделения памяти. Компиляторы Visual Studio, например, не создают исполняемые программы с разделами .idata для хранения таблиц указателей функций, а скорее с разделами .rdata, доступными для чтения и записи. Здесь лишь приблизительное понимание свойств блочной и абсолютной адресации в динамической памяти; не нужно зацикливаться на понимании содержания, атрибутов и того, как правильно их заполнить на практике. В следующих главах будет подробно объяснено значение каждой структуры и то, как ее спроектировать самостоятельно. В этом разделе мы узнали о преобразовании операций машинного кода во время выполнения программы, а также о различных разделах и смещениях данных, хранящихся в памяти, к которым можно получить доступ позже в процессе компиляции. Компиляция кода Как упоминалось ранее, если код содержит непонятные чипу строки или текстовые функции, компилятор должен сначала преобразовать их в абсолютные адреса, понятные чипу, а затем сохранить
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
их в отдельных разделах. Также необходимо перевести текстовый сценарий в собственный код или машинный код, который сможет распознать чип. Как это работает на практике? В случае Windows x86 инструкции, выполняемые на ассемблере, транслируются в соответствии с набором инструкций x86. Текстовые инструкции переводятся и кодируются в машинный код, который понимает чип. Заинтересованные читатели могут выполнить поиск x86 Instruction Set в Google, чтобы найти полную таблицу инструкций или даже закодировать ее вручную, не полагаясь на компилятор. После того, как компилятор завершит вышеупомянутую упаковку блока, следующим этапом будет извлечение и кодирование текстовых инструкций из скрипта, одна за другой, в соответствии с набором инструкций x86, и запись их в раздел .text, который используется для хранения машинного кода. Как показано на рис. 1.3, пунктирная рамка — это ассемблерный код текстового типа, полученный в результате компиляции кода C/C++:
Вы можете видеть, что первая инструкция — это push 0, которая помещает 1 байт данных в стек (сохраняется как 4 байта), а 6A 00 используется для представления этой инструкции. Инструкция push 0x402005 одновременно помещает в стек 4 байта, поэтому push 68 50 20 40 00 используется для достижения более длинной инструкции push. call ds:[0x403018] — это адрес из 4 байтов и длинный вызов машинного кода, FF 15 18 30 40 00, используется для представления этой инструкции. Хотя на рис. 1.3 показано распределение памяти динамического файла msgbox.exe, файл, созданный компилятором, еще не является исполняемым PE-файлом. Скорее, это файл под названием Common Object File Format (COFF) или объектный файл, как его называют некоторые люди, который представляет собой файл-оболочку, специально предназначенный для записи различных секций, создаваемых компилятором. На следующем рисунке показан файл COFF, полученный путем компиляции и сборки исходного кода с помощью команды gcc -c и просмотра его структуры с помощью известного инструмента PEview.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Как показано на рис. 1.4, в начале COFF-файла имеется структура IMAGE_FILE_HEADER для записи количества включенных секций:
В конце этой структуры находится целый массив IMAGE_SECTION_HEADER для записи текущего местоположения и размера содержимого каждого раздела в файле. В конце этого массива тесно связано основное содержание каждого раздела. На практике первый раздел обычно будет содержимым раздела .text. На следующем этапе компоновщик отвечает за добавление в загрузчик приложения экстра фрагмента COFF-файла, который станет нашей общей EXE-программой. В случае систем с чипом x86 принято менять местами указатель и цифру на бит в памяти при кодировании. Эта практика называется прямым порядком байтов, в отличие от строки или массива, которые должны располагаться от младшего к старшему адресу. Расположение данных из нескольких байтов зависит от архитектуры микросхемы. Заинтересованные читатели могут обратиться к статье Как писать независимый от порядков байтов код на C (https://developer.ibm.com/articles/au-endianc/). В этом разделе мы узнали о COFF, который используется для записи содержимого в память различных секций, записанных компилятором. Компоновщик Windows — упаковка бинарных данных в формат PE В предыдущем разделе мы предполагали некоторое распределение памяти во время компиляции программы. Например, база образа EXE-модуля по умолчанию должна иметь адрес 0x400000, чтобы исполняемый контент должен быть размещен. Раздел .text должен располагаться по адресу 0x401000 над базовым образом. Как мы уже говорили, раздел .idata используется для хранения таблицы адресов импорта, поэтому возникает вопрос, кто или что отвечает за заполнение таблицы адресов импорта? Ответ заключается в том, что в каждой ОС есть загрузчик приложений, который предназначен для
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
корректного выполнения всех этих задач при создании процесса из статической программы. Однако есть много информации, которая будет известна только во время компиляции, а не разработчику системы, например: - Требуется ли программе включить рандомизацию размещения адресного пространства (ASLR) или предотвращение выполнения данных (DEP)? - Где находится функция main(int, char) в разделе .text, написанная разработчиком? - Какая часть общей памяти используется исполнительным модулем во время динамической фазы? Поэтому Microsoft представила формат PE, который, по сути, является расширением файла COFF, с дополнительной опциональной структурой заголовка для записи информации, необходимой загрузчику программы Windows для корректировки процесса. В следующих главах основное внимание будет уделено игре с различными структурами формата PE, чтобы вы могли написать исполняемый файл вручную сами на коленке. Все, что вам нужно знать сейчас, это то, что исполняемый файл PE имеет некоторые ключевые особенности: - Код: обычно хранится в виде машинного кода в разделе .text. - Таблица импорта: чтобы позволить загрузчику заполнить адреса функций и позволить программе правильно их получить. - Необязательный заголовок: эта структура позволяет загрузчику читать и знать, как исправить текущий динамический модуль. Вот пример на рисунке 1.5:
msgbox.exe — это минималистичная программа для Windows, состоящая всего из трех разделов: .text, .rdata и .idata. После динамического выполнения загрузчик системного приложения последовательно извлекает содержимое трех секций и записывает их каждую по смещению 0x1000, 0x2000 и 0x3000
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
относительно текущего PE-модуля (msgbox.exe). В этом разделе мы узнали, что загрузчик приложения отвечает за исправление и заполнение содержимого программы для создания статического файла программы в процессе. Запуск статических PE-файлов как динамических процессов К этому моменту у вас есть общее представление о том, как минимальная программа генерируется, компилируется и упаковывается компилятором в исполняемый файл на статической фазе. Итак, следующий вопрос: что делает ОС, чтобы запустить статическую программу? На рис. 1.6 показана структура процесса преобразования исполняемой программы из статического в динамический процесс в системе Windows:
Обратите внимание, что это отличается от процесса хатчинга последней версии Windows. Ради пояснения мы проигнорируем процессы повышения привилегий, механизм патча и генерацию ядра, а поговорим только о том, как правильно анализируется и запускается статическая программа. В системах Windows все процессы должны запускаться родительским процессом путем прерывания системной функции для перехода на уровень ядра. Например, родительский процесс в настоящее время пытается запустить команду cmd. exe /c whoami, которая представляет собой попытку превратить статический файл cmd.exe в динамический процесс и назначить его параметры /c whoami. Итак, что же происходит во всем процессе? Как показано на рисунке 1.6, это шаги: 1. Родительский процесс делает запрос к ядру с помощью CreateProcess, указывая на создание нового процесса (дочернего процесса).
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
2. Затем ядро создаст новый контейнер процессов и заполнит контейнер исполняемым кодом с сопоставлением файлов. Ядро создаст поток для назначения этому дочернему процессу, который обычно называют основным потоком или потоком GUI. В то же время ядро также организует блок памяти в динамической памяти Userland для хранения двух структурных блоков: блока среды процесса (PEB) для записи текущей информации о среде процесса и блока среды потока (TEB) для записи информации о среде потока. Подробная информация об этих двух структурах будет полностью представлена в главе 2 «Память процесса — сопоставление файлов, синтаксический анализатор PE, tinyLinker и Hollowing» и в главе 3 «Вызов динамического API — информация о потоке, процессе и среде». 3. Функция экспорта NtDLL, RtlUserThreadStart, является основной функцией маршрутизации для всех потоков и отвечает за необходимую инициализацию каждого нового потока, например за создание структурированной обработки исключений (SEH). Первый поток каждого процесса, то есть основной поток, выполнит NtDLL!LdrInitializeThunk на уровне пользователя и войдет в функцию NtDLL!LdrpInitializeProcess после первого выполнения. Это исполняемый программный загрузчик, отвечающий за необходимое исправление загружаемого в память PE-модуля. 4. После того, как загрузчик выполнения завершает исправление, он возвращается к текущей записи выполнения (AddressOfEntryPoint), которая является основной функцией разработчика. С точки зрения кода поток можно рассматривать как человека, ответственного за выполнение кода, а процесс можно рассматривать как контейнер для загрузки кода. Уровень ядра отвечает за сопоставление файлов, то есть процесс размещения содержимого программы на основе предпочтительного адреса в период компиляции. Например, если базовый адрес образа равен 0x400000, а смещение .text равно 0x1000, то процесс сопоставления файлов, по сути, сводится к простому запросу блока памяти по адресу 0x400000 в динамической памяти и записи фактического содержимого .text по адресу 0x401000. На самом деле функция-загрузчик (NtDLL!LdrpInitializeProcess) после выполнения напрямую не вызывает AddressOfEntryPoint; вместо этого задачи, исправленные загрузчиком и точкой входа, рассматриваются как два отдельных потока (на практике будут открыты два контекста потока). NtDLL!NtContinue будет вызвана после исправления и передаст задачу в запись для продолжения выполнения в качестве расписания задачи потока. Точка входа выполнения записывается в NtHeaders→OptionalHeader. AddressOfEntryPoint структуры PE, но не является прямым эквивалентом основной функции разработчика. Это только для вашего понимания. Вообще говоря, AddressOfEntryPoint указывает на функцию CRTStartup (C++ Runtime Startup), которая отвечает за ряд необходимых подготовительных действий C/C++ по инициализации (например, преобразование аргументов в удобные для разработчика вводы) перед вызовом основной функции разработчика. В этом разделе мы узнали, как EXE-файлы превращаются из статических в динамически работающие процессы в системе Windows. С процессом и потоком и необходимыми действиями по инициализации программа готова к запуску. Итоги
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В этой главе мы объяснили, как ОС преобразует код C в ассемблерный код с помощью компилятора и в исполняемые программы с помощью компоновщика. Следующая глава будет основана на этой структуре и проведет вас через практический опыт работы со всей блок-схемой в нескольких лабах C/C++. В следующих главах вы изучите тонкости проектирования формата PE, создав компактный загрузчик программ и самостоятельно написав исполняемую программу.
Память процесса — сопоставление файлов, парсер PE, tinyLinker и Hollowing В главе 1 «От исходного кода к двоичным файлам — путешествие программы на языке C» мы узнали, как код на C/C++ можно упаковать в виде исполняемого файла в операционной системе. В этой главе мы объясним процесс сопоставления файлов, создадим компактный компилятор, подключим вредоносное ПО к системным службам и заразим игровые программы. В этой главе мы рассмотрим следующие основные темы: • Память статического содержимого PE-файлов • Пример парсера PE • Динамическое сопоставление файлов • Пример заражения PE (PE Patcher) • пример tinyLinkerа • Примеры холовинга процессаы • PE-файлы в HTML Примеры программ Примеры программ, упомянутые в этой главе, доступны на GitHub, где вы можете скачать упражнения: https://github.com/PacktPublishing/Windows-APT-Warfare/tree/main/chapter#02. Статическое содержимое PE-файлов В главе 1 «От исходного кода к двоичным файлам — путешествие программы на C» мы упомянули процесс, с помощью которого компилятор создает законченную исполняемую программу. Понятно, что исходный код на C/C++ после компиляции в основном разбивается на блоки и сохраняется. Эти блоки должны быть размещены по правильному адресу во время динамического выполнения. Затем мы можем начать выяснять, что компоновщик создаст в виде исполняемого файла. На рис. 2.1 показана упрощенная статическая структура PE, которую вам необходимо понять:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Автор перечислил некоторые ключевые поля, на которые будет ссылаться загрузчик приложения. Во-первых, вся организация памяти начинается с области заголовка DOS (IMAGE_DOS_HEADER), где .e_magic всегда должно быть равно строке MZ (то есть IMAGE_DOS_SIGNATURE), которая является действительным заголовком DOS. Большинство полей в структуре DOS больше не используются в текущей архитектуре Windows NT. Поэтому вам нужно только помнить, что поле .e_lfanew указывает на RVA, начальную точку структуры заголовков NT. Заголовки NT Структура заголовков NT также имеет поле для проверки правильности, которым является поле .Signature. Поле .Signature всегда должно быть равно строке PE\x00\x00 (то есть IMAGE_NT_SIGNATURE). Заголовки NT в основном содержат две важные структуры, заголовок файла и необязательный заголовок, которые описаны на рис. 2.2:
Давайте посмотрим на эти две структуры: • Заголовок файла (IMAGE_FILE_HEADER): Эта структура является заголовком файла COFF, созданным ассемблером, и записывает такую информацию, как текущее сообщение компиляции; рассмотрим
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
следующий пример: 1. Запись Machine — это машинный код текущей программы как x86, x64 или ARM. 2. Запись NumberOfSections — это количество секций в файле. 3. TimDataStamp записывает точное время компиляции этой программы. 4. SizeOfOptionalHeader — это фактический размер структуры дополнительного заголовка сразу после структуры IMAGE_FILE_HEADER. На практике, поскольку размер всего заголовка NT фиксирован, фиксированное значение этого поля обычно равно 0xE0 (32-разрядное) или 0xF0 (64-разрядное). 5. Characteristics записывают текущие свойства всего PE-модуля, например, является ли он 32-разрядным, DLL-модулем, исполняемым или нет, а также содержит ли он информацию о перенаправлении. • Необязательный заголовок: эта структура представляет собой информацию о записи, добавляемую компоновщиком на последнем этапе компиляции, которая используется для предоставления загрузчику приложения необходимой информации, используемой для восстановления файла программы до состояния, готового для нормального выполнения процесса: 1. ImageBase записывает адрес памяти, по которому PE-модуль должен быть загружен во время компиляции (по умолчанию 0x400000 или 0x800000). 2. SizeOfImage записывает, сколько места в памяти должно быть использовано поверх базы образа, чтобы полностью сохранить все содержимое раздела во время динамического выполнения. 3. SizeOfHeaders записывает, сколько места занимают заголовки DOS + заголовки NT + заголовки разделов (массив). 4. AddressOfEntryPoint записывает первую точку входа программы после ее компиляции. Эта точка входа обычно указывает на начало функции в разделе .text. 5. FileAlignment выполняет статическое выравнивание разделов, которое по умолчанию равно 0x200 в 32-разрядных версиях. Мы уже упоминали, что разделы являются блочными. Затем, когда раздел сохраняется в статический файл, если статический раздел не заполнен, его необходимо заполнить до тех пор, пока он не будет заполнен, что делает его блочной структурой. Возьмем, к примеру, статическое выравнивание раздела 0x200. Если .data в настоящее время составляет всего 3 байта, то для хранения этих 3 байтов будет запрошен блок размером 0x200 байт. Если .data в данный момент имеет 0x201 байт, то он будет дополнен до раздела 0x400 байт. 6. SectionAlignment динамически выравнивает разделы, и по умолчанию это 0x1000 в 32-битной системе. 7. DataDirectory — это таблица, которая записывает начальную точку и размер 15 полей, в которых каждый элемент используется для записи различных деталей программы: #00 – Экспорт каталога
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
#01 – Каталог импорта #02 – Каталог ресурсов #03 – Каталог исключений #04 – Аутентикод каталога безопасности #05 – Таблица релокации базы #06 – Каталог отладки #07 — Данные, специфичные для архитектуры x86 (в настоящее время отбрасываются) #08 – Глобальная таблица смещения указателя (в настоящее время отброшена) #09 – Локальное хранилище потоков (TLS) #10 – Загрузчик каталога конфигурации #11 – Связанный каталог импорта в заголовках #12 – Импорт таблицы адресов #13 – Дескрипторы импорта отложенной загрузки #14 – Дескриптор времени выполнения COM Это 15 полей таблиц для всех PE-структур. Выделены некоторые из таблиц, которые будут рассмотрены более подробно в Главе 5 - Дизайн загрузчика приложений, включая методы реализации и атаки. Те из вас, кто проницателен, возможно, заметили, что #01 и #12 относятся к похожим вещам. Функция экспорта модуля DLL, на который ссылается каждый столбец в #12, записана в массивах IMAGE_IMPORT_DESCRIPTOR в таблице в #1. Различия будут объяснены более подробно в Главе 5 Дизайн загрузчика приложений. Важная заметка Полная информация о необязательном заголовке доступна повсюду в Интернете; однако большинство полей не так важны. Поэтому в этой книге перечислены только самые важные элементы для загрузчиков приложений. Заголовки секций В предыдущем подразделе мы упомянули, что в процессе компиляции исходный код преобразуется в несколько блочных разделов. Каждый блок имеет различный начальный адрес, размер содержимого и адрес в выделенной памяти, поэтому для записи этой информации необходимо использовать общий метод описания. В случае структуры PE для записи всех деталей используется IMAGE_SECTION_HEADER COFF. Конец структуры заголовков NT является отправной точкой массива заголовков разделов (как
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
показано на рис. 2.3):
Поскольку структура заголовков NT всегда имеет фиксированный размер, учитывая любой PE-контент, возможно, легко вручную перейти от заголовка DOS (в строке MZ) к массиву заголовков разделов. Причина, по которой его называют массивом заголовков разделов, заключается в том, что он содержит набор заголовков разделов (IMAGE_SECTION_HEADER). Например, если вы просканируете NT Header→FIle Header→NumberOfSections и обнаружите, что текущий модуль имеет три раздела, то общее использование памяти массива заголовков этого раздела будет равно sizeof(IMAGE_SECTION_HEADER) * 3. Заголовок каждого раздела (IMAGE_SECTION_HEADER) — это запись, позволяющая системе понять, откуда брать разделы из статической PE-структуры, какого размера их делать, куда их записывать в динамическую память и какого размера их записывать. Как показано на рис. 2.3, записываются важные атрибуты заголовков разделов: • PointerToRawData: это смещение содержимого текущего раздела, сохраненного в статическом файле, чтобы мы могли извлечь содержимое этого раздела из этой начальной точки. • SizeOfRawData: это размер файла, сохраненного в PointerToRawData, чтобы мы могли правильно определить начальную и конечную точки содержимого раздела. • VirtualAddress: это относительное смещение (адрес подкачки) базового адреса размещеноого образа. Предыдущие два атрибута дадут нам полное содержимое текущего раздела; затем пришло время записать виртуальный адрес, на который ссылается VirtualAddress. • VirtualSize: здесь фиксируется, сколько места должно быть выделено в динамическом пространстве для хранения содержимого раздела. • Characteristics: здесь указывается, доступна ли секция для чтения, записи или выполнения, что определяется во время компиляции. Эти три атрибута могут сочетаться друг с другом в любой комбинации и не исключают друг друга. Например, .text обычно доступен для чтения и выполнения (не для записи), а .rdata (данные только для чтения) будет доступен только для чтения. На рис. 2.3 автор отметил смещение = 0 (начало программы) в заголовке DOS, а EoF (конец файла)
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
намеренно выровнен с последним разделом содержимого. Это означает, что все разделы контента выровнены с помощью выравнивания файлов, а затем плотно склеены вместе без каких-либо зазоров. В современных компиляторах теоретический размер PointerToRawData + SizeOfRawData будет равен размеру файла, который вы вычислили с помощью WinAPI-функции GetFileSize или ftell, а размер всей программы на диске будет суммой (a) и (b): • (a) Размер заголовка DOS + заголовки NT + заголовки разделов (массив) после выравнивания общего размера выравнивания файлов • (b) Сумма размеров каждого раздела после выравнивания файлов Знание этого - очень важно для анализа вредоносных программ, независимо от того, хотите ли вы написать червя или создать отдельный компоновщик. Кроме того, вы могли заметить, что в заголовке раздела записаны два поля: SizeOfRawData для статического хранилища и Misc.VirtualSize для динамического хранилища. С точки зрения программирования, если всем глобальным переменным не присваиваются начальные значения, а выполняются динамически, и значения записываются в глобальные переменные после того, как они были обработаны, то для раздела .data или .bss может возникнуть следующая ситуация: нет ссылки на начальные значения в статическом содержимом, но выделяется место в динамической памяти. Это приводит к ситуации, когда SizeOfRawData равен 0, но VirtualSize имеет значение. В этом разделе мы узнали о деталях статической структуры PE, включая массивы заголовков NT и разделов, а также о функциях соответствующих полей сведений. Это поможет нам понять анализ вредоносных программ. Пример PE-парсера Этот пример взят из проекта PE Parser. Его можно найти в папке Chapter#2 общедоступного проекта этой книги на GitHub. Для экономии места мы извлекли только выделенный код; вы должны обратиться к полному исходному коду проекта для получения более подробной информации. Это простой инструмент, написанный на C/C++, который может считывать любое содержимое EXE в память с помощью функций fopen и fread и сохранять его в указателе ptrToBinary, как показано на рис. 2.4:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Давайте рассмотрим предыдущий код более подробно: • Строки 2-7: Заголовок DOS должен присутствовать в начале программы. Мы можем получить смещение заголовка NT из его поля e_lfanew, а затем добавить это смещение к базовому адресу всего двоичного файла. Таким образом, мы успешно получили заголовки DOS и NT. • Строка 4: Проверяем, является ли магический номер заголовка DOS MZ, а магический номер заголовков NT равен PE\x00\x00. • Строки 10-14: свойство «Необязательный заголовок» можно получить после того, как мы получим действительные заголовки NT. Это печатает базовый адрес образа, количество байтов и его динамическую запись в текущей динамической фазе анализируемой программы. • Строки 18-21: Поскольку за заголовками NT будет следовать массив заголовков раздела, мы можем получить адрес первого заголовка раздела, просто добавив начальную точку заголовков NT к фиксированному размеру всего заголовка NT. Затем мы можем выполнить итерацию цикла for, чтобы распечатать информацию для каждого заголовка раздела. На рис. 2.5 показано содержимое раздела, отображаемое известным инструментом анализа PE-bear, что согласуется с результатами, напечатанными нашим PE Parser, разработанным на C. Этот результат подтверждает правильность нашего понимания структуры PE:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В этом разделе мы использовали программу PE Parser для получения списка адресов различных разделов программы. Результаты согласуются с известными инструментами анализа, подтверждая наше понимание. Динамическое сопоставление файлов В этом разделе мы обсудим, как статический PE-файл создается как новый процесс и как программный файл отображается и монтируется в его динамическую память. На рис. 2.6 показан упрощенный процесс отображения статической PE-программы в памяти:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В левой части рис. 2.6 показан контейнер для содержимого памяти, а в правой части показан статический PE-файл, который еще не был запущен и находится на диске. Ниже приводится систематическое объяснение процесса, с помощью которого операционная система монтирует свои статические файлы в динамические: 1. Во-первых, система проверяет адрес ImageBase записи дополнительного заголовка в заголовках NT (в настоящее время 0x400000), который является адресом, который, как ожидается, будет размещаться в динамической памяти во время компиляции программы. Обратите внимание, что если защита ASLR и функция релокации включены одновременно, это может быть случайная база образа. 2. Затем система проверяет SizeOfImage дополнительного заголовка в заголовках NT и обнаруживает, что для хранения всего модуля в адресе ImageBase требуется всего 0xDEAD байтов. Затем система запросит пространство 0xDEAD по адресу 0x400000. 3. Затем системе необходимо скопировать заголовки DOS, NT и всех разделов на адрес ImageBase. Общий размер этих трех разделов можно найти в области SizeOfHeaders дополнительного заголовка, которая равна 0x400. Все данные в статическом файле (смещение = 0 ~ 0x400) затем будут скопированы по адресам ImageBase +0 ~ +0x400. 4. Мы можем просканировать FileHeader→NumberOfSections из статического файла PE, чтобы получить количество разделов, а из массива заголовков разделов мы можем перечислить содержимое каждого раздела (PointerToRawData) и относительный виртуальный адрес (RVA), который ожидает раздел для записи в динамическую память. Следующим шагом является запись каждого блока в цикле по соответствующему динамическому адресу. Это полный процесс сопоставления файлов. 5. По завершении код и данные помещаются в динамическое пространство так, как ожидает компилятор. Затем основной поток выполняет загрузчик приложения из NtDLL!LdrpInitializeProcess,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
чтобы исправить программу, и возвращается к записи выполнения (0x401234). Таким образом, вся программа может быть успешно выполнена. В этом разделе мы узнали, как сопоставить статический файл PE с каждым разделом заголовка NT. В следующем разделе мы проиллюстрируем это на практическом примере. Пример заражения PE (PE Patcher) В этом примере рассматривается проект PE_Patcher. Его можно найти в папке Chapter#2 общедоступного проекта этой книги на GitHub. Для экономии места мы извлекли только выделенный код; пожалуйста, обратитесь к полному проекту, чтобы просмотреть полный исходный код. Имея любой исполняемый файл (например, установщик игры) и определенный вредоносный код (шеллкод), мы можем использовать то, что мы узнали, чтобы заразить установщик игры, чтобы игрок думал, что он запускает установщик игры, но вместо этого запускал наш бэкдор. В этом разделе мы узнаем, как заразить обычную программу шелл-кодом в виде червя. Основная идея состоит в том, чтобы поместить вредоносный раздел в обычную программу для хранения вредоносного кода и указать запись программы на вредоносный код, чтобы зараженная программа запускала наш вредоносный код сразу после выполнения. На рис. 2.7 показан распространенный в Интернете шеллкод, функция которого состоит в том, чтобы открывать всплывающее окно BrokenByte при его запуске:
Другие шелл-коды, такие как загрузка вредоносных программ, обратные оболочки, модули внедрения памяти и т. д., можно легко найти в Интернете. Вам может быть интересно, что делать, если вы хотите написать специальный шелл-код, которого нет в Интернете. Не волнуйтесь; Глава 3, Динамический вызов API — Информация о потоке, процессе и среде, и Глава 4, Техника шелл-кода — Экспорт функций, помогут вам изучить различные способы написания собственного шелл-кода для Windows! На рис. 2.8 показан код PE Patcher. Он считывает входной аргумент пользователя argv[1] в основной записи, чтобы указать на путь к обычной заражаемой программе, и readBinFile (который внутренне
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
считывает все двоичное содержимое с помощью fread) для извлечения и сохранения содержимого файла зараженной программы в переменную buff:
Далее, в строках 49-51, MACRO определяет три функции: 1. getNtHdr(buf): определяет начальную точку двоичного файла PE и указатель для возврата заголовков NT. 2. getSectionArr(buf): То же, что и getNtHdr(buf), но используется для получения начальной точки массива заголовков разделов. 3. P2ALIGNUP(число, выравнивание): используется для добавления значения num к соответствующему выравниванию в блочной форме. Например, P2ALIGNUP(0x30, 0x200) получит 0x200, а P2ALIGNUP(0x201, 0x200) получит 0x400. Продолжая строки 53-56, SectionAlignment сообщает нам, сколько выравниваний следует использовать для выравнивания кода на странице блока, а FileAlignment определяет, сколько байтов следует использовать для выравнивания размера раздела. Как мы упоминали ранее, размер содержимого каждого раздела, сохраненного в конце двоичного файла PE, будет таким же, как размер всего двоичного файла PE, рассчитанный с помощью WinAPI GetFileSize. Таким образом, если мы хотим вставить дополнительный раздел в этот двоичный файл PE для хранения шелл-кода, это означает, что мы должны добавить пробел P2ALIGNUP (размер вредоносного кода, FileAlignment) в конце двоичного файла PE, чтобы было достаточно места для хранения шелл-кода. Затем мы должны использовать malloc для выделения места для записи памяти зараженной программы и memcpy для копирования содержимого обычной программы. Далее давайте посмотрим на строки 60-64 кода:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Нам нужно создать новый заголовок раздела, чтобы записать, где раздел шеллкода должен быть записан в динамическую память. В противном случае загрузчик приложения не сможет записать шелл-код в память на этапе выполнения. Мы можем создать новый заголовок раздела, извлекая последний заголовок раздела, используемый обычной программой. Мы будем использовать пространство заголовка следующего раздела из этого заголовка раздела для записи данных раздела, и оно будет успешно добавлено. Здесь предполагается, что в самой программе все еще достаточно места для заголовка раздела, чтобы мы могли писать. В строках 67-68 кода мы ввели имя нового раздела как 30cm.tw и заполнили поле VirtualSize байтами P2ALIGNUP (размер вредоносного кода, SectionAlignment), необходимыми для того, чтобы шелл-код записывался в динамическом пространстве. Далее нам нужно заполнить новый раздел для RVA динамического относительного смещения модуля PE, то есть VirtualAddress. Расчет понятен: если предыдущие 0x1000, 0x2000 и 0x3000 уже заняты .text, .data и .idata, то наш VirtualAddress должен быть 0x4000. Итак, смотрим на строку 69 кода: RVA, записываемый на новую секцию, будет равен RVA предыдущего раздела + количество байтов динамической памяти, занимаемых предыдущим разделом. В строке 72 кода пишем в поле PointerToRawData. Мы сказали, что статический PE-бинарник будет плотно сгруппирован в разделы, поэтому последний раздел в конце исходной программы будет лучшим местом для размещения шелл-кода. В строках 73-74 кода новая секция содержит «исполняемый» шеллкод, поэтому необходимо присвоить этой секции атрибуты «читаемый», «записываемый» и «исполняемый». Атрибут исполняемого файла предназначен для предотвращения самоизменяющегося поведения некоторого шелл-кода, такого как динамическая распаковка или расшифровка, чтобы шелл-код оставался полностью отображаемым, обычно используется как MSFencode (инструмент шифрования в Metasploit). Наконец, мы создаем новый заголовок раздела, и нам нужно увеличить NumberOfSections + 1 в FileHeader, чтобы загрузчик приложения знал о новом разделе. Далее смотрим строки 76-99 кода:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строке 77 кода мы добавили заголовки секций, так что нам нужно выполнить memcpy шеллкод в PointerToRawData, конец последней секции текущей программы. В строке 84 кода, поскольку мы добавили новый фрагмент шелл-кода, размещаемого в динамическую память, мы должны помнить об исправлении SizeOfImage. Как мы упоминали ранее в разделе «Динамическое сопоставление файлов», SizeOfImage — это размер пространства, занимаемого программой от ImageBase до последнего раздела. Поэтому максимальным пределом динамического пространства памяти, занимаемого распределением памяти, будет VirtualAddress + VirtualSize последней секции (то есть секции, которую мы только что создали). В строке 89 кода, после всех предыдущих дополнений и модификаций, уже можно предположить, что раз программа запустилась (то есть запустилась по правильному отображению файлов), она должна иметь возможность прикасаться к нашему шеллкоду по адресу новой секции. Затем мы можем просто указать текущую AddressOfEntryPoint на RVA нового раздела и перехватить поток программы для запуска нашего шелл-кода. Мы будем использовать старую игру Pikachu Volleyball в качестве демонстрации:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 2.11 слева показана игра Pikachu Volleyball после выполнения picaball.exe. Однако файл picaball_infected.exe, сгенерированный инструментом PE Patcher, будет отображать всплывающее окно сразу после срабатывания шелл-кода. Это подтверждает, что мы действительно вставили шелл-код в игру. В этом разделе мы использовали программу PE Patcher, чтобы проиллюстрировать, как добавить новый заголовок раздела, вставить шелл-код и указать запись программы на вредоносный код, чтобы активировать его. Пример ТиНиЛинкера Этот пример взят из проекта tinyLinker. Его можно найти в папке Chapter#2 общедоступного проекта этой книги на GitHub. Для экономии места мы извлекли только выделенный код; на полный исходный код следует ссылаться, если вы хотите просмотреть полный проект для подробного чтения. Теперь, когда вы узнали, как сгенерировать компоновщик для исполняемого файла, теперь вам нужно узнать, как создать компоновщик программы PE с нуля. В этом разделе мы применим практический подход к этому:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мы предполагаем, что простой исполняемый файл должен иметь как минимум три заголовка структуры, то есть заголовок DOS, заголовки NT и заголовки разделов соответственно. (Обратите внимание, что заголовок файла и необязательный заголовок являются частью заголовков NT). Содержимое этого раздела добавляется к концам этих заголовков. В строках 26-31 кода вычисляется размер всей программы. В строке 26 размер трех заголовков выравнивается как блок на основе FileAlignment. В строке 30 мы вычисляем количество байтов, необходимых для сохранения шелл-кода в виде секции. Затем в строке 31 мы запрашиваем блок памяти с помощью calloc для полного двоичного файла полного PE-файла. Полный размер будет равен сумме размера заголовка раздела (выровненного) + суммы содержимого раздела (выровненного). В строке 34 мы знаем, что отправной точкой двоичного файла PE будет заголовок DOS, поэтому мы можем указать, что в настоящее время подготовленная память должна быть заголовком DOS, а затем заполнить строку MZ (e_magic), которая должна быть у действительного заголовка DOS. Мы предполагаем, что заголовки NT будут следовать за концом заголовка DOS, поэтому смещение (начальная точка) заголовков NT, на которое указывает e_lfanew, будет равно концу заголовка DOS.
Теперь нам нужно сгенерировать заголовки NT. В строках 39-40 кода первое, что мы должны сделать, это сделать его законным заголовком NT, поэтому должна быть установлена волшебная строка (PE\x00\x00). Затем мы настраиваем заголовок файла с правильной информацией, включая код, скомпилированный в машинный код i386 (32-разрядный), исполняемые файлы с 32-разрядной структурой и т. д. Далее мы заполняем один раздел (NumberOfSections), который у нас сейчас есть только для сохранения кода.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Теперь давайте посмотрим на строки 41-52 кода:
В строках 44-51 кода у нас есть раздел, созданный для хранения содержимого шелл-кода, подробности которого упоминались в предыдущем разделе при вводе PE-инфекции. Разница лишь в том, что на этот раз во всей программе есть только один раздел для сохранения шелл-кода. Таким образом, наш новый раздел RVA может напрямую заполнять выравнивание раздела с помощью 0x1000 (ни один из предыдущих разделов не занимает места). Адрес статического файла, содержащего содержимое разделов, будет в конце памяти, занимаемой тремя типами заголовков разделов, что является отправной точкой нашего единственного содержимого раздела. Чтобы запускать шелл-код непосредственно при запуске программы, нам просто нужно управлять Address Of EntryPoint.
Наконец, нам нужно заполнить необязательную информацию заголовка, чтобы помочь загрузчику приложения понять, как правильно загрузить программу. Во-первых, поле Magic должно быть заполнено 32-битной или 64-битной опциональной структурой заголовка. Наш новый раздел VirtualAddress является отправной точкой RVA для «раздела кода» в поле BaseOfCode, поэтому заполните свойство VirtualAddress нового раздела. Поле «Subsystem» — это поле для графического интерфейса или консоли, которое задается в параметре компоновщика проекта в Visual Studio C++. Если вы хотите иметь консольный интерфейс, вы можете использовать IMAGE_SUBSYSTEM_WINDOWS_ CUI (3); в противном случае используйте
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
IMAGE_SUBSYSTEM_WINDOWS_GUI (2) для отсутствия интерфейса консоли. После того, как мы закончили заполнять это, мы используем fwrite для создания всего двоичного файла PE в poc.exe и запускаем его. Как показано на рис. 2.16, мы можем написать компоновщик на C/C++, который без труда сгенерирует новый PE-файл с нуля:
Это доказывает, что наша теория применима на практике. Инструмент PE-bear справа показывает, что сгенерированный файл poc.exe имеет только один раздел размером 30cm.tw и что внутри него хранится сам шелл-код. В этом разделе мы использовали практическую программу tinyLinker, чтобы проиллюстрировать, как вручную составлять различные заголовки и разделы в программе. Примеры холовинга процесса Этот пример взят из проекта RunPE. Его можно найти в папке Chapter#2 общедоступного проекта этой книги на GitHub. Для экономии места мы извлекли только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть все детали проекта. В этом разделе показано, как методы сопоставления файлов могут быть злонамеренно использованы хакерами на передовой. Эта техника использовалась Ocean Lotus, вьетнамской национальной киберармейской организацией. Этот пример был адаптирован из проекта RunPE с открытым исходным кодом (github.com/Zer0Mem0ry/RunPE) для демонстрационных целей. После понимания всего процесса от статического маппинга до маппинга файлов, вы, возможно, задумались над следующим вопросом: если мы запускаем программу, подписанную цифровыми подписями от известных и действительных компаний (например, пакет обновлений Microsoft, установщик в крупной компании и т. д.), и заменим смонтированный PE-модуль вредоносным модулем, можем ли мы запустить вредоносное ПО как доверенную программу? Да, это ядро знаменитой техники атаки со скрытием процессов (RunPE):
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 2.17 показан весь процесс атаки с точки зрения распределения памяти. Мы знаем, что процесс — это, по сути, PE-файл, отображаемый в память, независимо от того, монтирует ли он модуль EXE или модуль DLL. Таким образом, если в памяти имеется несколько PE-модулей, какой модуль является текущим процессом? Ответ кроется в блоке Process Environment Block (PEB). Когда новый процесс генерируется на уровне ядра, в дополнение к отображению статических файлов также создается PEB. В поле ImageBaseAddress PEB будет храниться базовый адрес образа основного потока выполнения. Затем, когда основной поток выполняет функцию NtDLL!LdrpInitializeProcess, он идентифицирует основной исполнительный модуль как PE-модуль выше PEB->ImageBaseAddress. На основе этого PE-модуля детали таблицы адресов импорта, таблицы адресов экспорта, перенаправления и т. д. будут правильными. Привилегия выполнения вернется к записи основного модуля после завершения выполнения. Если мы сможем сопоставить вредоносный модуль с памятью до того, как исполняемый загрузчик начнет модифицировать исполняемый файл и заменить адрес основного модуля PEB>ImageBaseAddress из исходного модуля базовым адресом образа, который в настоящее время выбрасывается вредоносным ПО, то мы можем успешно захватить нормальный процесс выполнения программы. Как показано на рис. 2.17, исходный модуль отображается в файле по адресу 0x400000, а модуль вредоносного ПО монтируется по адресу 0xA00000. Теперь нам просто нужно заменить поле ImageBaseAddress базовым адресом образа вредоносного ПО до запуска исполняемого загрузчика.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Теперь давайте посмотрим на код:
В строках 92-104 точка входа вредоносного ПО проверяет, является ли имя текущего исполняемого файла GoogleUpdate. exe (сервис фонового обновления Google). Если это так, появится всплывающее окно в результате нашего успешного захвата; в противном случае функция RunProtableExecutable запускается в строке 102. Эта функция попытается вставить свой собственный PE-файл из MapFileToMemory с помощью обработки процесса и подделать его как процесс GoogleUpdate. Далее, давайте посмотрим на строки 30-50:
В строке 40 показан трюк, специфичный для Windows. При создании нового процесса WinAPI CreateProcess позволяет нам установить флаг CREATE_SUSPENDED и монтировать любую программу как процесс. Однако в настоящее время основной поток приостановлен и еще не выполнен в загрузчике приложения. Если вам интересно, вы можете извлечь содержимое регистров в области контекста потока. Вы обнаружите, что EIP (счетчик программ) основного потока в приостановленном в данный момент процессе указывает на функцию маршрутизации общего потока, NtDLL!RtlUserThreadStart. Первый
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
параметр этой функции фиксируется в регистре EAX и содержит адрес, по которому поток должен вернуться после завершения необходимой инициализации. Второй параметр фиксируется в регистре EBX и содержит адрес PEB, сгенерированный ядром в процессе. В строках 46-50 кода мы используем GetThreadContext для получения информации о регистре основного потока приостановленного в данный момент процесса GoogleUpdate. Затем мы пытаемся использовать VirtualAllocEx для запроса памяти SizeofImage в ImageBase, чтобы мы могли сопоставить файлы вредоносной программе в этом пространстве памяти. Давайте посмотрим на строки 51-69 кода:
Строки 52-61 кода имитируют поведение ядра с точки зрения сопоставления файлов. Сначала копируются заголовки DOS, NT и разделов; затем каждый раздел заполняеься на правильный адрес процесса в цикле for для завершения процесса сопоставления файлов. Далее, поскольку регистр EBX области основного потока в настоящее время содержит структуру PEB, мы можем использовать WriteProcessMemory для записи PEB + 8 (смещение PEB→ImageBaseAddress в 32-битном значении равно смещению + 8), чтобы изменить текущий основной PE-модуль с модуль GoogleUpdate на вредоносный модуль. В регистре EAX будет храниться информация о том, куда перейдет основной поток после необходимых исправлений (то есть после исправления ошибки загрузчиком приложения). Мы изменили этот регистр, чтобы он стал адресом входа нашего вредоносного модуля. На этом этапе вы можете быть еще новичком в структуре PEB и нервничать. Не бойся. В главе 3 «Вызовы динамических API — поток, процесс и информация о среде» есть отдельный раздел «Блок среды процесса» (PEB), в котором представлена вся структура PEB. Наконец, мы записываем все исправления, которые мы только что сделали, в регистры с помощью SetThreadContext и возобновляем основной поток с помощью ResumeThread. На рис. 2.21 показаны результаты:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В левом нижнем углу предыдущего рисунка известный инструмент судебной экспертизы Process Explorer показывает, что после запуска вредоносной программы RunPE создается процесс с именем GoogleUpdate. Вместо запуска бинарника GoogleUpdate отображается всплывающее окно, содержащее наше вредоносное ПО. Мы также подтвердили, что цифровая подпись не повреждена и действительна для проверки. Было доказано, что метод атаки вообще не модифицировал какой-либо статический код и был достигнут путем простой замены основного модуля в динамической фазе, чтобы обмануть загрузчик приложения. Этот метод часто используется для атаки на белые списки антивирусного программного обеспечения или корпоративной защиты. Эти белые списки часто настраиваются с определенными системными службами или имеют цифровые подписи, которые обеспечивают определенную степень иммунитета от того, чтобы считаться вредоносным ПО. Вот почему это популярная техника, используемая крупными киберсилами. В этом разделе мы использовали программу RunPE, чтобы показать, как смонтированный PE-модуль был заменен вредоносным модулем, который можно использовать для подмены загрузчика приложения без изменения какого-либо статического кода. Результаты показывают, что цифровая подпись не повреждена для проверки, а заменена вредоносной программой. PE-файлы в HTML До сих пор вы должны понимать, что PE-файл — это просто спецификация пакета, которая указывает загрузчикам системы и приложений распылять содержимое каждого ожидаемого раздела во время компиляции.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Однако tinyLinker — это компоновщик, который мы реализовали вручную. Те из вас, кто имеет опыт в этой области, знают, что нам не нужно использовать все поля в структуре PE для создания исполняемого файла. Это означает, что реальный исполняемый файл занимает всего несколько полей в структуре PE для создания исполняемого EXE-файла, и система полностью способна правильно запиисывать содержимое отдельных разделов в правильное динамическое пространство. Исследователь Осанда Малит (https://osandamalith.com/2020/07/19/hacking-the-world-with-html/) рассмотрел вопрос: поскольку PE-файлы могут быть загружены и выполнены корректно только с несколькими полями, как насчет оставшееся неиспользованное пространство в структуре PE? На рис. 2.22 мы видим, что важные и неуничтожаемые поля в заголовке PE, а также оставшиеся неотмеченные поля теперь можно заполнить легко:
Те из вас, кто заинтересован, могут обратиться к проекту с открытым исходным кодом Осанды Малит, PE2HTML: внедряет HTML/PHP/ASP в PE (https://github.com/OsandaMalith/PE2HTML). Этот инструмент автоматически заполняет неиспользуемое пространство PE-файла содержимым отображаемых текстовых сценариев (например, HTML/PHP/ASP). Это не нарушит ни выполнение самой программы PE, ни нормальную работу отображаемого текстового скрипта. В этом разделе мы объяснили, что в структуре PE все еще есть много неиспользуемых полей, которые можно использовать для заполнения сценариев, и что инструмент с открытым исходным кодом PE2HTML, разработанный Осандой Малит, может делать это автоматически и не влияя на работу программы. Краткое содержание В этой главе мы узнали об упрощенной статической структуре PE, включая заголовки DOS, заголовки NT и заголовки разделов, и заменили эти заголовки практическими программами для запуска вредоносных программ. Это первый шаг к самостоятельному созданию вредоносных программ. В следующей главе динамические вызовы API будут объяснены более подробно, чтобы вы поняли, как выполнять изменение параметров и создавать динамические модули.
Динамический вызов API — информация о потоке, процессе и среде
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В этой главе мы изучим основы вызовов Windows API на ассемблере x86. Сначала мы узнаем о блоке среды потока (TEB) и блоке среды процесса (PEB), а также о том, как злоумышленники используют эти функции во вредоносном программном обеспечении. К концу этой главы вы должны лучше понять, как компилятор выполняет динамические вызовы с помощью соглашений о вызовах, чтобы программа работала так, как мы ожидаем. Имея эти основы, вы можете шаг за шагом продвигаться к цели написания собственного шелл-кода для Windows. Например, вызов API Windows, которого нет в нашем исходном коде, позволяет избежать обнаружения антивирусом имен API, занесенных в черный список. В этой главе мы рассмотрим следующие основные темы: • Соглашение о вызове функций • Блок среды потока (TEB) • Блок среды процесса (PEB) • Примеры подделки параметров процесса • Примеры перечисления загруженных модулей без API • Примеры маскировки и сокрытия загруженных библиотек DLL Примеры программ Примеры программ, упомянутые в этой главе, доступны на веб-сайте GitHub, где вы можете загрузить упражнения по следующему URL-адресу: https://github.com/PacktPublishing/Windows-APT-Warfare/tree/main/chapter#03 Соглашение о вызове функций В предыдущих главах мы узнали, что компилятор сохраняет фрагменты кода в разных разделах в зависимости от функции исходного кода. Например, код преобразуется в машинный код и сохраняется в разделе .text, данные сохраняются в разделе .data или .rdata, а таблица адресов импорта (IAT) сохраняется в разделе .idata, как показано на Рисунок 3.1:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Шеллкод — это краткий скрипт машинного кода. Когда мы можем захватить программный счетчик потока, такой как регистры EIP или RIP или адрес возвврата, мы можем управлять им в шелл-коде для выполнения конкретных и точных задач (вызов определенного набора системных API). Обычное поведение (например, загрузка и выполнение вредоносного ПО, реверс коннект к оболочке, всплывающие окна и т. д.) достигается путем вызова системного API. Однако, в отличие от PE-программ, шелл-код не запускается с помощью ядра для сопоставления файлов или исправления загрузчика приложений, поэтому новичкам гораздо сложнее писать шелл-код, чем разрабатывать на C/C++. Основная трудность заключается в том, как вызывать системные функции, не полагаясь на IAT. Это нормально. Давайте начнем с простых понятий шаг за шагом, и как только мы твердо установим представление о том, как работает операционная система, мы обнаружим, что написать шелл-код совсем несложно. Соглашение о вызовах На рис. 3.2 мы снова представляем поведение вызова MessageBoxA в исходном коде C/C++:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это поведение вызова преобразуется в вызов ассемблера на основе соглашения о вызовах C++, как показано в нижней части рисунка 3.2 (соответствует одному вызову и четырем инструкциям push). Параметры вызова системной функции показывают, что есть четыре параметра, которые в соответствии с соглашением о вызовах WINAPI (32-разрядное) помещаются в стек в порядке справа налево. После того, как функция завершает свое поведение, она отвечает за получение области параметров, занимаемой вершиной стека, а затем переходит к следующей строке инструкции, xor eax, eax. Приведенный здесь пример — MessageBoxA, соглашение о вызовах которого заключается в использовании правила WINAPI. Однако не только эта функция, но и большинство API-интерфейсов Windows, создранных Microsoft для разработчиков, следуют правилам WINAPI. Соглашение о вызовах WINAPI в 32-битной системе — это правило stdcall: параметр помещается в стек, вызываемый объект очищает стек после завершения функции, а возвращаемое значение помещается в регистр EAX. В то время как в соглашении о вызовах x64 (64-разрядная версия) параметры занимают RCX → RDX → R8 → R9 → стек по порядку. Поэтому перед вызовом функции вы должны проверить, каково соглашение о вызовах. В противном случае высок риск непреднамеренного поведения, такого как сбой, неполучение параметра при не освобождении стека и переход к пустому указателю. На практике эти проблемы автоматически планируются компилятором C/C++ во время компиляции в соответствии с соглашением о вызовах функции, которую вы хотите вызвать. Это объясняется в правилах передачи аргументов и именования от Microsoft. (https://docs.microsoft.com/en-us/cpp/cpp/argument-passing-and-naming-conventions ), в котором объясняется историческое происхождение и более подробные сведения об этих правилах. Для краткости заинтересованные читатели могут исследовать это самостоятельно, так как это не слишком сложно. Соглашение о вызовах в основном касается решения трех вещей: (а) куда поместить параметры, (б) кто отвечает за повторное использование памяти параметров и (в) где хранить возвращаемое значение.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Как показано на рис. 3.3, исходный код msgbox_new.c здесь адаптирован из исходного кода msgbox.c из главы 1 - От исходного кода к двоичным файлам — путешествие программы на C, скомпилировано MinGW:
Вы можете ясно видеть, что две строки текущего адреса MessageBoxA распечатываются после выполнения, и MessageBoxA успешно вызывается для отображения строки приветствия в окне сообщения. В строках 13-15 кода WinAPI GetProcAddress используется для получения адреса функции MessageBoxA в памяти (где хранится фактический машинный код этой функции) и сохранения его в переменной get_MessageBoxA. Для сравнения, функция MessageBoxA (функция, сохраняемая в IAT при компиляции) распечатывается непосредственно printf. Если вы внимательно посмотрите, то обнаружите, что MessageBoxA, извлеченный из IAT, совпадает с результатом, который мы распечатали с помощью GetProcAddress. Это означает, что каждая строка функции, которую мы пишем в исходном коде, понимается компилятором (при динамическом выполнении) как соответствующая началу адреса машинного кода функции. Затем мы возвращаемся к строке 9 кода, в которой ключевое слово typedef определяет тип функции. Существует функция соглашения о вызовах, WINAPI, с четырьмя параметрами типов HWND → char → char → UINT, и возвращаемое значение этого вызова функции является int, и имя этого типа функции, def_MessageBoxA. В строках 17-18 кода мы получаем адрес MessageBox с помощью GetProcAddress, а затем преобразуем его в тип указателя функции, который мы только что определили, def_MessageBoxA и сохраняем как переменную msgbox_a. Затем вы можете использовать msgbox_a напрямую как вызов MessageBoxA. Это классический пример вызова указателя на функцию. Заинтересованные читатели могут использовать GoogleFunction Pointer в качестве ключевого слова, чтобы найти всевозможные
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
интересные варианты, поэтому мы не будем представлять их все здесь. В этом разделе мы снова проиллюстрировали соглашение о вызовах C++, а также продемонстрировали с помощью новой программы, что написанный нами код может быть понят компилятором. Это классический пример вызова указателя на функцию. Изучив этот пример вызова указателя функции, вы, должно быть, обнаружили, что если мы сможем найти адрес системной функции, сохранить параметры в соответствии с соглашением о вызовах и вызвать функцию, не полагаясь на IAT, сгенерированный компилятором (т.е. , GetProcAddress, LoadLibrary, GetModuleHandle и другие Win32 API), то мы успешно написали шелл-код, не так ли? Это верно! Итак, поговорим о том, как найти адрес образа функции без Win32 API, а затем найти правильный адрес функции по адресу образа без Win32 API. Блок среды потока (TEB) TEB — одна из неопубликованных структур Microsoft. Содержимое, показанное на рис. 3.4, взято из недокументированных 32-битных структур PEB и TEB (bytepointer.com/resources/tebpeb32.htm):
Это частичное содержимое TEB после 32-битного реверс-инжиниринга. Общий размер TEB достигает 0xFF8. Однако для пояснения мы упомянем только 0x30 байт в начале, а остальные части предназначены для внутренней реализации Windows. Как мы упоминали в главе 2, Память процесса — сопоставление файлов, синтаксический анализатор PE, tinyLinker и Hollowing, при создании каждого процесса в памяти процесса должен храниться PEB для записи сведений о создаваемом процессе. А что с тредами? Да. Давайте возьмем концепцию многопоточности, которую вы изучали в классе операционной системы. Если в одном и том же процессе параллельно выполняется несколько потоков, пространство стека, используемое для хранения параметров, не может совместно использоваться одновременно. То есть каждый поток может использовать только соответствующий стек для хранения параметров. По таким причинам необходимо, чтобы каждый поток имел свой собственный TEB, чтобы позволить потокам запоминать имеющуюся у них информацию. Таким образом, в процессе есть только один PEB, но несколько TEB одновременно.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 3.4 показан пример структуры TEB и ее смещений для 32-битной системы. ExceptionList по адресу +0x00 хранит цепочку структурированной обработки исключений (SEH), которая является специальным механизмом обработки исключений только для 32-разрядной версии Windows и позволяет разработчикам попытаться перехватить исключение в C/C++. StackBase и StackLimit в +0x04 и +0x08 соответственно записывают диапазон стеков, которые может использовать текущий поток, а ClientId в +0x20 напрямую кэширует числовой идентификатор текущего процесса и потока. По сути, это поле, из которого разработчик получает Windows API. Фокус находится на Self в +0x18 и ProcessEnvironmentBlock в +0x30. Во-первых, поле Self по адресу +0x18 указывает на адрес текущего TEB, а ProcessEnvironmentBlock по адресу +0x30 указывает на адрес PEB процесса, которому принадлежит поток. Это позволяет нам получить текущий статус процесса. На рис. 3.5 показано текущее содержимое памяти TEB (динамический адрес 0x01004000) с помощью команды TEB() средства отладки x64dbg:
Текущий диапазон доступного стека потока составляет 0x012FC000 ~ 0x01300000 по адресу +0x04 и +0x08, в то время как поле Self по адресу +0x18 (выделено на рисунке) всегда будет указывать на начальную точку текущего TEB и, следовательно, равно 0x01004000. Между тем +0x30 показывает, что текущий TEB хранится по адресу 0x01001000. В 32-битных системах все предыдущие поля TEB можно получить напрямую, добавив смещение соответствующего поля в сегмент FS (один из регистров, а не упомянутый ранее раздел). Например, если вы хотите получить StackBase, вы можете получить его из fs:[0x04], а текущую начальную точку структуры TEB можно получить из fs:[0x18]. В 64-битных системах можно получить содержимое нужного поля через сегмент GS. Для получения более подробной информации, пожалуйста, обратитесь к общедоступной информации на сайте geoffchappell.com (https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/pebteb/teb/index.htm). В этом разделе мы подробно описали некоторые структуры TEB и объяснили содержимое каждой структуры с точки зрения динамических адресов выполнения. Блок среды процесса Одной из основных тем этой книги является структура PEB. На рис. 3.6 показано некоторое содержание структуры PEB, но для краткости включены только основные моменты. Полную структуру можно найти в неопубликованном блоке Process-Environment-Block (https://www.aldeid.com/wiki/PEB-ProcessEnvironment-Block), указанном на веб-сайте ALDEID:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 3.6 показан единственный блок информации о состоянии для текущего процесса. Он содержит такую информацию, как «BeingDebugged» по адресу +0x02, значение, которое разработчик возвращает внутренне при использовании WINAPI IsDebuggerPresent для проверки того, выполняется ли отладка. ImageBaseAddress по адресу +0x08, появившийся ранее в методе очистки процессов, используется для записи того, какой EXE-файл является основным PE-модулем текущего процесса. ProcessParameters по адресу +0x10 в 32-битной структуре PEB записывает информацию о параметрах, унаследованных текущим процессом, когда он пробуждается родительским процессом. Более подробная информация представлена на рисунке 3.7:
На рис. 3.7 показаны параметры процесса. ConsoleHandle наследуется от консоли родительского процесса, поэтому мы можем обновить черное окно родительского процесса при печати текста с помощью printf. Функции перенаправления StdIn, StdOut и StdErr также популярны среди разработчиков и позволяют им вызывать сторонние исполняемые файлы для получения результата.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Следующие три ключевых поля представлены строковой структурой UNICODE_STRING: • CurrentDirectoryPath записывает текущий рабочий каталог, указанный родительским процессом. Если не указано, в качестве родительского процесса указывается текущий рабочий каталог. • ImagePathName записывает полный путь к текущему EXE-файлу. • CommandLine записывает параметры, заданные родительским процессом, для пробуждения текущего процесса. Структура LDR по адресу +0x0c на рис. 3.6 — это главный символ, который мы собираемся представить, который записывает все модули, загруженные в текущем процессе, в виде структуры данных. Перейдем к анализу структуры данных LDR:
В предыдущей главе мы упоминали, что NtDLL!LdrpInitializeProcess — это функция загрузчика приложений, которая отвечает за исправление указателя функции, на который ссылаются PE-модули, и загрузку системных модулей, которые нам нужно использовать, в память. Структура в PEB→LDR — это структура PEB_LDR_DATA, как показано на рисунке 3.8. Длина по адресу +0x00 — это текущий размер структуры PEB_LDR_DATA; если структура была заполнена и инициализирована, то в поле Initialized по адресу +0x04 будет установлено значение true, чтобы указать, что она готова для запроса. В конце PEB_LDR_DATA вы можете увидеть три последовательных набора двухсторонних последовательностей со структурой LIST_ENTRY. Поскольку они объединены в цепочку, узлы в середине связаны вместе как LDR_DATA_TABLE_ENTRY, используемые для записи информации о каждом загруженном модуле.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Эти три набора — InLoadOrderModuleList, InMemoryOrderModuleList и InInitializationOrderModuleList, все из которых представляют собой связанные списки использования информации загруженного модуля, но разница заключается в порядке, в котором просматривается информация модуля. InLoadOrderModuleList перечисляет модули в том порядке, в котором они были загружены, InMemoryOrderModuleList перечисляет модули в порядке их базового адреса образа от самого низкого до самого высокого, а InInitializationOrderModuleList перечисляет модули в том порядке, в котором они были инициализированы. На практике, с точки зрения поиска адреса образа, существует небольшое разнообразие, которое вы хотели бы использовать для его обхода, и вы можете действовать по своему усмотрению. Давайте взглянем на структуру PEB_LDR_DATA на рис. 3.9:
Во-первых, длина по адресу +0x00 говорит о 0x30, что означает, что 32-битная структура PEB_LDR_Data текущей версии Windows 10 Enterprise расширилась до размера 0x30. Это связано с тем, что структура бесконечно масштабируема для Windows. Для параметра Initialized at +0x04 установлено значение True, что означает, что его можно использовать для запросов. Далее вы можете увидеть следующее: • InLoadOrderModuleList по адресу +0x0c записывает значение Flink 0x02FD3728, а значение Blink равно 0x02FD4EA0. • InMemoryOrderModuleList по адресу +0x14 записывает Flink как 0x02FD3730 и Blink как 0x02FD4EA8. • InInitializationOrderModuleList по адресу +0x1c записывает Flink как 0x02FD3630 и Blink как 0x02FD4EB0. Flink и Blink указывают на структуру LDR_DATA_TABLE_ENTRY:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В разделе «Динамическое сопоставление файлов» в главе 2 мы поняли, что сопоставление статического модуля с динамическим важно в отношении базового адреса образа, на который он заполняется, объема памяти, используемого для распы заполнения, и адреса точки входа (addressOfEntryPoint) модуля. Поэтому на рис. 3.10 показана структура LDR_DATA_TABLE_ENTRY, которая используется для хранения подробной информации о статическом модуле, размещенном в динамической памяти. Мы перечислили только важные части для чистоты: • DllBase по адресу +0x18 — это адрес базы образа для текущего модуля, подлежащего заполнению. • EntryPoint at +0x1c — это адрес AddressOfEntry модуля PE. • SizeOfImage at +0x20 — количество байтов, занимаемых модулем в динамическом пространстве. • Флаги по адресу +0x30 записывают состояние текущего загружаемого модуля. Это поле используется, чтобы позволить системе запускать загрузчик приложений для определения хода загрузки и состояния монтирования текущего модуля: – Когда это LDRP_STATIC_LINK, это означает, что это модуль, который монтируется при создании процесса. Это может быть модуль, который записан в IAT как требующий монтирования, или это может быть системный модуль, автоматически монтируемый KnownDlls. – Когда это LDRP_IMAGE_DLL, это означает, что это модуль DLL, который монтируется в процесс. – Когда это LDRP_ENTRY_PROCESSED, это означает, что модуль DLL не только смонтирован, но и что его функция входа была вызвана (инициализация завершена). • LoadCount по адресу +0x34 записывает количество импортов текущего модуля. Будь то ссылка на DLL в IAT или динамический вызов для загрузки модуля с помощью функции LoadLibrary, значение каждой дополнительной ссылки будет +1. Когда число достигает 0, это означает, что никому не нужно обращаться к модулю, и он будет освобожден из памяти и восстановлен.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
FullDllName по адресу +0x24 — это полный путь к этому модулю, который также является полным путем к PE-модулю, полученному разработчиком с помощью Win32 API GetModuleFileName, а BaseDllName по адресу +0x2c — это имя файла этого модуля. Возьмите C:\Windows\System32\Kernel32.dll в качестве примера. Текст, сохраненный FullDllName, представляет собой полную строку C:\Windows\System32\Kernel32.dll, а текст, сохраненный BaseDllName, представляет собой только чистое имя файла, такое как Kernel32.dll. Мы упоминаем, что каждый бит загруженной информации хранится в структуре LDR_DATA_TABLE_ENTRY и связан вместе как цепочка узлов в проходимой строке. Ссылаясь на рисунок 3.8, первые три элемента структуры — это InLoadOrderLinks, InInitializationOrderLinks и InInitializationOrderLinks, которые позволяют нам получить предыдущую или следующую структуру LDR_DATA_TABLE_ENTRY для просмотра информации о текущем динамическом модуле. Не будет ли предварительной информации слишком много? Давайте нарисуем схему, чтобы понять это быстро:
На рис. 3.11 показано распределение памяти, посещаемой PEB→LDR на этапе динамического выполнения. Видно, что LDR в PEB указывает на PEB_LDR_DATA, структура которого рассматривается как заголовок цепочки, а три PE-файла, a.exe, ntdll.dll и Kernel32.dll, монтируются на этапе динамического выполнения. И InLoadOrderModuleList, и InMemoryOrderModuleList могут перечислять все смонтированные PE-модули в текущем процессе, за исключением того, что первый перечисляется в соответствии с порядком монтирования, а второй — в соответствии с адресом памяти. InInitializationOrderModuleList используется для записи информации об импортированных модулях, поэтому запись текущего EXE-файла отсутствует. Ранее мы упоминали, что PEB используется для записи информации об EXE-файлах, выполняемых с текущим процессом. Обычно первый модуль, загруженный в процесс, будет EXE-файлом, и первый модуль, загруженный в динамику, также будет EXE-файлом. Следовательно, первый узел InLoadOrderModuleList получит EXE-модуль. При нормальных обстоятельствах PEB→ImageBase обычно
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
будет основой образа первого модуля узла в цепочке InLoadOrderModuleList. В этом разделе мы подробно описали частичную структуру PEB, связанные параметры и подробную структуру данных LDR, а также содержимое каждой структуры с точки зрения динамических адресов выполнения. На данный момент у вас должно быть общее представление о TEB и PEB, так что давайте попробуем их использовать! Примеры подделки параметров процесса Следующий пример — строка masqueradeCmdline, которую можно найти в папке Chapter#3 проекта GitHub, общедоступного в репозитории этой книги. В целях экономии места в этой книге извлекается только код основных моментов; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть полный проект. Многие Red Teams или злоумышленники, которые проводят атаки на локальные машины, часто сталкиваются с антивирусным программным обеспечением, продуктами защиты конечных точек или мониторингом регистрации событий и ожидают, что их команды атаки не будут обнаружены или не отслеживаются. Техника очистки процессов (RunPE), которую мы рассмотрели в главе 2, предложила идею: если мы создадим дочерний процесс с фиктивными параметрами, а фактическое выполнение считывает заданные нами параметры атаки, может ли это обойти локальный мониторинг антивируса? Например, программы-вымогатели часто используют команду vssadmin delete shadows /all /quiet для удаления резервных данных пользователя. Каждое антивирусное программное обеспечение будет строго проверять, содержит ли параметр процесса программы vssadmin предыдущую команду, чтобы избежать такого рода атак:
Ранее мы упоминали, что PEB→ProcessParameters указывает на RTL_USER_PROCESS_PARAMETERS, который содержит информацию о параметрах дочернего процесса на момент его создания. Мы также
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
знаем, что когда API CreateProcess вызывается с CREATE_SUSPENDED, поток дочернего процесса приостанавливается перед входом в функцию загрузчика приложения, а регистр EBX указывает на PEB, сгенерированный ядром. EXE будет вызван после того, как функция загрузчика исправит EXE-модуль. Это означает, что мы можем заменить текущие параметры дочернего процесса правильными параметрами, которые будут выполняться, пока поток приостановлен, а затем возобновить выполнение потока, чтобы добиться подделки параметров. В строке 21 кода мы генерируем большое количество строк A с 260 байтами неиспользуемых параметров. Причина такой длины заключается в том, чтобы подготовить достаточно места в памяти для записи фактического содержимого, которое мы хотим выполнить позже. В строках 24-25 кода мы используем CreateProcess для создания 32-разрядной команды cmd.exe, которая поставляется с Windows в качестве состояния приостановки потока. Только что сгенерированный параметр мусора будет использоваться в качестве параметра, переданного текущему cmd.exe, а GetThreadContext будет использоваться для получения временного содержимого текущего приостановленного потока. В строках 28-32 кода после извлечения содержимого PEB дочернего процесса с помощью ReadProcessMemory мы можем получить адрес структуры RTL_USER_PROCESS_PARAMETERS текущего дочернего процесса в поле ProcessParameters структуры PEB. После получения адреса структура RTL_USER_PROCESS_PARAMETERS снова считывается с помощью ReadProcessMemory. Буфер CommandLine (структура UNICODE_STRING) содержит 260 байт адреса текстового параметра, упомянутого ранее. Следовательно, мы можем просто перезаписать текстовый параметр, который хотим выполнить, с помощью WriteProcessMemory и возобновить работу потока, чтобы добиться эффекта подделки параметра. Мы используем Process Monitor, популярный инструмент регистрации событий, используемый исследователями, в качестве примера для отслеживания результатов этого примера, как показано на рис. 3.13:
Здесь мы возьмем в качестве примера Process Monitor, любимый исследователями инструмент
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
мониторинга регистрации событий, и проследим за результатами этого примера после выполнения (см. рис. 3.5). Результаты показывают, что в журналах событий Process Monitor оставляет только действие по созданию cmd.exe AAAAAAAAAAAAAAA и не записывает команду whoami, которую мы фактически передали в cmd.exe, а фактически считанные и выполненные параметры не соответствуют тому что фиксируется средством мониторинга. Этот пример предназначен только для образовательных целей, и есть много других улучшений, необходимых для практических атак. В настоящее время проект поддерживается независимо от публичного проекта автора на GitHub (github.com/aaaddress1/masqueradeCmdline), и заинтересованные читатели могут ознакомиться с ним для получения более подробной информации. В этом разделе мы проверили подделку параметров с помощью реального проекта masqueradeCmdline, который показал, что фактически прочитанные и выполненные параметры не совпадают с записанными инструментом мониторинга, доказав, что мы действительно можем добиться подделки параметров. Это тематическое исследование, чтобы вы поняли, как работает антивирус. Если вас интересует эта серия атак, есть книга Нира Йегошуа «Методы обхода антивируса: изучите практические приемы и тактики борьбы, обхода и уклонения от антивирусного программного обеспечения», в которой основное внимание уделяется этому. Примеры перечисления загруженных модулей без API В настоящее время антивирус всегда проверяет, использует ли программа API, которым можно легко злоупотребить, чтобы определить, является ли она вредоносной, например, используя LoadLibraryA для монтирования Kernel32.dll для получения его ImageBase. Таким образом, если мы сможем получить адрес Kernel32.dll, не используя LoadLibraryA, мы сможем избежать обнаружения антивирусом и заставить его думать, что мы не пытаемся использовать Kernel32 DLL. Следующий пример — это исходный код ldrParser.c, который находится в открытом доступе в папке Chapter#3 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть полный проект. Как упоминалось ранее, распределение записей на этапе динамического выполнения PEB→LDR позволяет нам перечислить информацию о загруженном модуле, поэтому первым шагом является получение текущего адреса PEB. На рис. 3.14 показан исходный код ldrParser.c:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Поскольку мы упомянули, что динамический адрес структуры PEB можно получить из fs:[0x30] в 32-битном (или gs:[0x60] в 64-битном), в строке 88 кода __readfsdword (которое может быть используется путем включения ) может напрямую получить содержимое fs:[+n], поэтому мы можем прочитать адрес структуры PEB по адресу 0x30. В строках 89-90 кода переменная &PEB→Ldr→InMemoryOrderModuleList может получить адрес InMemoryOrderModuleList в структуре LIST_ENTRY в PEB_LDR_DATA и записать это как переменную заголовка. Когда мы переходим от каждого узла LDR_DATA_TABLE_ENTRY к последнему, следующий за последним будет возвращаться к источнику (т. е. туда, куда указывает переменная заголовка). Таким образом, запись может помочь нам узнать, что каждый узел был посещен один раз. Затем Flink в заголовке указывает на первую структуру LDR_DATA_TABLE_ENTRY, которая является первым узлом, который мы перечисляем. Цикл for продолжается строками 92-99 кода: 1. Во-первых, если перечисляемый в данный момент узел не является переменной заголовка, это означает, что мы еще не вернулись к структуре PEB_LDR_DATA (источник). Следовательно, мы можем продолжить перечисление по строке. 2. Как показано на рис. 3.10, когда вы выбираете Flink InMemoryOrderLinks в +0x08 для выбора следующего узла, вы получаете адрес структуры LDR_DATA_TABLE_ENTRY в +0x08. Это означает, что при выборе InInitializationOrderLinks at +0x10 для перечисления также будет получена структура узла с адресом +0x10. Итак, в строке 93 кода мы можем использовать функцию CONTAINING_RECORD, чтобы вычесть смещение InMemoryOrderLinks и получить правильный адрес структуры LDR_DATA_TABLE_ENTRY по адресу +0x00. 3. Мы используем StrIStrW, чтобы проверить, является ли текущий модуль тем, который нам нужен. Если да, то возвращается базовый адрес образа, записанный в его структуре DllBase. При входе в функцию main мы можем искать базу образов библиотеки Kernel32.dll, установленную в памяти, с помощью только что созданной нами функции GetModHandle, как показано на рис. 3.15:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Этот базовый адрес образа используется для замены LoadLibrary или GetModuleHandle в Win32 API и передается функции GetProcAddress для поиска адреса функции экспорта WinExec в его модуле, который затем можно использовать для вызова калькулятора (calc.exe) с правильным соглашением о вызовах:
Затем мы видим, что после того, как ldrParser был успешно скомпилирован и запущен MinGW, он перечисляет текущий установленный модуль Kernel32.dll и получает адрес своей экспортной функции WinExec, которая затем может успешно вызвать калькулятор, как показано на рис. 3.16. В этом разделе мы используем реальную программу для разбора и перечисления модулей, загруженных LDR, не полагаясь на API. Результаты показывают, что мы можем успешно получить текущее имя узла и запустить указанную программу. Примеры маскировки и сокрытия загруженных DLL Следующий пример — это код module_disguise.c в папке Chapter#3 проекта GitHub, который общедоступен в репозитории этой книги. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть все детали проекта.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В предыдущем разделе вы видели, что мы можем просканировать структуру PEB→LDR в динамической памяти, чтобы получить желаемый базовый адрес образа функционального модуля. Следующий вопрос заключается в том, может ли информация, записанная в этих динамических модулях, быть подделана для злонамеренного использования. Ответ положительный. В этом разделе мы разрабатываем две функции: renameDynModule и HideModule. Первый используется для маскировки информации о динамическом модуле запутанными путями и именами, а второй используется для сокрытия указанного динамически загружаемого модуля из записи. На рис. 3.17 показана функция renameDynModule, которая имеет только один входной параметр для имени динамического модуля, который мы хотим подделать:
В строках 91-94 мы импортируем функцию экспорта ntdll, RtlInitUnicodeString, чтобы заменить текст, сохраненный в нашей входящей структуре UNICODE_STRING. В строках 99-106 это цикл for, используемый для перечисления динамической структуры LDR_DATA_TABLE_ENTRY, представленной в предыдущем разделе. Разница в том, что на этот раз, после того, как мы находим указанный блок информации о модуле, функция RtlInitUnicodeString подделывает BaseDllName и FullDllName, записанные в информации о модуле, в exploit.dll и C:\Windows\System32\exploit.dll. Функция HideModule используется для полного скрытия указанного динамического модуля путем удаления записи модуля из списка PEB->LDR, как показано на рис. 3.18:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 120-136 кода каждая структура LDR_DATA_TABLE_ENTRY представляет собой двустороннюю цепочку узлов, которые связаны туда и обратно. Итак, когда мы хотим скрыть текущий узел, нам просто нужно подключить Blink следующего узла к предыдущему узлу, а Flink предыдущего узла к следующему узлу. Это имеет тот же эффект, что и исключение текущего узла. Затем может быть достигнута функция скрытия любого заданного модуля. На рис. 3.19 показаны две вышеупомянутые самостоятельно разработанные функции, вызываемые для входа в основную функцию:
Модуль KERNEL32.DLL подделывается, чтобы он выглядел как файл exploit.dll с помощью renameDynModule. USER32.dll, необходимый модуль для вызова функции MessageBox, скрыт от записи с помощью HideModule. MessageBoxA используется, чтобы доказать, что USER32.dll действительно все еще существует в текущем процессе.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мы использовали известный китайский инструмент цифровой криминалистики Huorong Sword, чтобы проанализировать детали информации о модуле после запуска программы маскировки:
Вы можете видеть, что оригинальная библиотека KERNEL32.DLL расположена по адресу 0x77150000, и на данный момент Huorong Sword распознает ее как файл exploit.dll. Между тем, USER32.dll должен находиться по адресу 0x775C0000, но Huorong Sword не знает о существовании этого модуля. Это подтверждает успех нашей программы маскировки. В этом разделе мы замаскировали и спрятали загруженные библиотеки DLL с помощью программы disguise и изменили информацию в структуре LDR_DATA_TABLE_ENTRY, используя renameDynModule и HideModule. Результаты показывают, что это действительно может привести к ошибочным суждениям со стороны криминалистических инструментов, подтверждая осуществимость нашего мышления. Краткое содержание Многие из современных антивирусных программ, решений для мониторинга и защиты конечных точек, а также решений для мониторинга журналов событий предназначены для повышения производительности за счет анализа только информации о памяти без проверки того, было ли содержимое подделано. В этой главе мы изучили основы вызовов Windows API в x86, включая TEB и PEB, а также поддельные параметры, поддельные и скрытые загруженные библиотеки DLL и многое другое. При правильном понимании основ и тактики, используемой злоумышленниками, мы можем лучше понять популярные методы преследования, предпочитаемые киберармией первой линии. В следующей главе мы собираемся продолжить изучение того, как анализировать отдельные модули DLL в памяти и получать желаемый адрес API, не вызывая API Windows. Мы также узнаем, как хакеры пишут шелл-код Windows в x86 для выполнения конкретных атак.
Техника шеллкода — парсинг экспортированных функций В этой главе мы узнаем, как получить желаемый адрес API из загруженных модулей библиотеки
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
динамической компоновки (DLL), чтобы мы могли овладеть знаниями, необходимыми для написания шелл-кода для выполнения в памяти Windows. Для этого мы сначала изучим структуру экспортной таблицы адресов (EAT) в PE, создадим собственный синтаксический анализатор DLL и напишем новый шелл-код Windows с нуля в x86. После того, как мы закончим эту главу, мы сможем разработать генератор шелл-кода для Windows на Python, который позже мы сможем использовать для достижения желаемой функциональности. В этой главе мы рассмотрим следующие основные темы: • EAT в PE • Примеры анализатора файлов DLL • Примеры написания шеллкода в x86 • Генератор шелл-кода на Python. Примеры программ Примеры программ, упомянутые в этой главе, доступны на веб-сайте GitHub. Читатели могут загрузить упражнения по следующему URL-адресу: https://github.com/PacktPublishing/Windows-APTWarfare/tree/main/chapter%2304. EATs в PE В главе 3 «Вызов динамического API — информация о потоке, процессе и среде» мы успешно изучили динамическую память, чтобы получить базу образа нужного системного модуля. Эти загруженные PE-модули также загружаются в динамическую память посредством сопоставления файлов. Как только мы получим адрес DLL, мы можем использовать GetProcAddress API, чтобы получить адрес конкретной функции, которую он экспортирует. Итак, здесь возникает новый вопрос: есть ли разница в бинарной структуре между PE-программами с функциями экспорта (DLL) и PE-программами без функций экспорта? На рис. 4.1 показан dllToTest.c, пример упрощенного исходного кода модуля DLL в папке Chapter#4 проекта GitHub:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строке 16 кода находится стандартная функция входа в DLL. Когда этот модуль DLL впервые монтируется в процесс, глобальная строковая переменная sz_Message изменяется на Hello Hackers! Вернемся к строкам с 10 по 14 кода: мы разработали пять нефункциональных экспортируемых функций: func01, func02, func03 и так далее. Все эти функции вызовут окно сообщения с функцией MessageBoxA для отображения содержимого измененной строки sz_Message. Затем мы можем скомпилировать исходный код dllToTest.c этого модуля DLL в demo.dll с пятью экспортированными функциями, используя MinGW. Затем мы можем смонтировать этот модуль с помощью встроенной в Windows команды rundll32.exe и вызвать функцию экспорта модуля, func01, чтобы увидеть окно, показанное на рис. 4.2:
В этот момент у читателей должен возникнуть вопрос: а не должна ли в компиляторе быть хотя бы одна таблица, которая сообщает нам, какие функции экспортируются из текущего модуля DLL?
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Давайте посмотрим на рисунок 4.3:
Мы используем инструмент PE-bear для просмотра таблицы с именем DataDirectory в разделе Optional Hdr заголовков NT в структуре PE. DataDirectory — это таблица, содержащая большое количество информации, которую PE выполняет динамически и на которую можно ссылаться в динамической памяти, сохраняя свой относительный виртуальный адрес (RVA) в табличном формате. На рис. 4.3 таблица DataDirectory содержит различные типы информационных записей. Например, 0x7000 указывает на каталог экспорта, 0x8000 указывает на каталог импорта, а информация о подписи Authenticode, подписанная цифровой подписью, затем сохраняется в структуре данных, на которую указывает каталог безопасности. Если это программа .NET с управляемым кодом, в месте, на которое указывает заголовок .NET, будет храниться заголовок структуры, специфичный для .NET. Давайте сначала разберемся, какие внешние функции доступны для текущего модуля DLL. Мы начнем с информации, хранящейся по адресу 0x7000, указывающей на каталог экспорта. На рис. 4.4 показаны результаты PE-bear:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Результаты показывают, что вся структура EAT сохраняется в виде отдельного блока в разделе .edata после компиляции. Это содержимое доступно в статическом PE-файле по смещению 0x2800 и загружается в динамической фазе по адресу RVA 0x7000. Рассмотрим подробнее содержание структуры EAT:
На рис. 4.5 представлена информация, полученная после разбора структуры EAT в PE-bear: • В поле TimeDataStamp указано время компиляции этого модуля DLL. • В поле Name записывается имя модуля при его компиляции и создании. • Поле NumberOfFunctions записывает, сколько экспортированных функций доступно для использования в этом исходном коде во время компиляции.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• Поле NumberOfNames записывает, сколько экспортированных функций доступно для отображения имен. • Поле AddressOfFunctions содержит набор RVA, указывающих на массив DWORD (32-разрядный). Этот массив содержит все смещения RVA функции экспорта. • Поле AddressOfNames содержит набор RVA, указывающих на массив DWORD (32-разрядный). Этот массив содержит смещения RVA для имен экспортируемых функций, которые могут отображать имена. • Поле AddressOfNameOrdinals содержит массив WORD (16-разрядный). Этот массив содержит порядок функций, соответствующий функции экспорта для отображаемых имен во время компиляции. Возьмите этот модуль в качестве примера. Хотя исходный код называется dllToTest.c, исходный код компилируется и экспортируется как demo.dll, поэтому этот EAT записывает имя модуля во время компиляции как demo.dll (а не dllToTest.dll). Многие антивирусы и службы Windows используют эту функцию, чтобы определить, был ли модуль DLL взломан или заменен, то есть если имя текущего статического файла DLL не совпадает с именем файла во время компиляции. В качестве примера возьмем исходный код на рис. 4.1. Пять экспортируемых функций, от func01 до func05, были специально разработаны. При компиляции компилятор упорядочивает экспортированные функции в объявленном порядке и присваивает каждой функции порядковый номер. Этот номер можно использовать в качестве идентификатора для экспортируемых функций. Внутренним функциям модуля DLL не назначается порядковый номер функции. Поэтому неэкспортированная функция tryToSleep не влияет на функции func04 и func05, как показано на рис. 4.5. Есть две переменные, NumberOfFunctions и NumberOfNames, для хранения информации, связанной с количеством экспортируемых функций. Почему нам нужно создать две разные переменные для хранения числа? Причина этого в том, что в C/C++ экспорт функции не обязательно требует имени функции и может быть выполнен путем экспорта анонимной функции. Если вам нужно сделать вызов функции, вы можете найти адрес функции без поиска имени функции, вместо этого ища порядковые номера функции. Заинтересованные читатели могут обратиться к общедоступному документу Microsoft «Экспорт из DLL с использованием файлов DEF» (docs.microsoft.com/en-us/cpp/build/exporting-from-adll-using-def-files). На рис. 4.6 показан простой эксперимент:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мы уже знаем, что функция экспорта func01 имеет порядковый номер функции, равный 1, а второй параметр GetProcAddress может не только передавать имя функции в текстовом виде, но также напрямую передавать порядковый номер функции для запроса адреса экспорта. Следовательно, мы можем использовать GetProcAddress, чтобы получить адрес func01 с порядковым номером 1 и использовать его в качестве указателя функции для выполнения, который успешно выдает результат выполнения func01. Это конкретное использование часто используется хакерами, чтобы обойти механизм статического сканирования антивирусного программного обеспечения. Возьмем в качестве примера популярный хакерский инструмент mimikatz:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 4.7 показано содержимое импортированных функций, отображаемое инструментом mimikatz и проанализированное PE-bear. На рисунке видно, что mimikatz импортирует системный модуль Cabinet.dll. Однако, глядя на нижнюю часть этого рисунка, вы не знаете, на какие имена функций экспорта ссылаются в этом модуле DLL. Все, что известно, это то, что экспортированные функции с порядковыми номерами B, E, A и D были импортированы соответственно. EAT в Cabinet.dll показан на рис. 4.8:
Порядковые номера B, E, A и D соответствуют функциям FCIAddFile, FCIDestroy, FCICreate и FCIFlushCabinet. Этот подход, заключающийся только в записи использования порядковых номеров функций в таблице адресов импорта, а не в сохранении предшествующих литеральных имен функций в самом PE-файле, может использоваться для обхода некоторых распространенных механизмов сканирования статических функций. Однако наша цель состоит в том, чтобы иметь возможность
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
расшифровывать GetProcAddress самостоятельно, поэтому нам необходимо понять механизм между AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals. Давайте воспользуемся диаграммой, чтобы прояснить отношения в памяти:
На рис. 4.9 показано динамическое распределение памяти модуля DLL demo.dll, когда он монтируется в процесс a.exe. Секция .text используется для хранения содержимого кода, поэтому содержимое машинного кода от func01 до func05 хранится в диапазонах от 0x1010 до 0x1050, что означает, что текущие адреса этих пяти функций в динамической памяти будут 0xA01010, 0xA01020, 0xA01030, 0xA01040. и 0xA01050, а # числа перед fun01 до func05 представляют текущие порядковые номера функций. Секция .rdata содержит данные только для чтения. На рис. 4.9 имена текстовых функций от func01\x00 до func05\x00 хранятся в RVA 0x2000, 0x2007, 0x200E, 0x2015 и 0x201C (соответствующих адресам 0xA02000, 0xA02007, 0xA0200E, 0xA02015, и 0xA0201C в динамической памяти соответственно) . Мы упоминали, что первый элемент таблицы DataDirectory в разделе NT Headers → OptionalHeader модуля DLL позволяет получить смещение RVA в структуре IMAGE_EXPORT_DIRECTORY. Как показано на рис. 4.9, эта структура хранится в разделе .edata по адресу 0x3000. Таким образом, мы можем получить доступ к содержимому этой структуры в динамической памяти по адресу 0xA03000. В EAT есть три важных массива, которые описываются отдельно: • AddressOfNames содержит общее число экспортированных именованных функций в виде массива DWORD, который представляет собой смещение RVA для каждого имени функции (текстовый тип). Обратитесь к рисунку 4.9. RVA, сохраненный в этом массиве, представляет собой адрес имени функции, сохраненного в разделе .rdata. Например, третий элемент, 0E 20 00 00 (индекс = 2), хранится в форме с прямым порядком байтов как 0x200E, что указывает на массив строк func03\x00 в разделе .rdata. • AddressOfNameOrdinals содержит общее число порядковых номеров функций в виде массива WORD,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
который упорядочен в соответствии с порядковым номером функции, соответствующим имени функции, хранящемуся в AddressOfNames, для облегчения перекрестных ссылок на имена функций в одном и том же индексе. Возьмите func03\x00 в качестве примера. Он расположен по индексу = 2 в массиве AddressOfNames, поэтому мы можем продолжить получать номер функции, соответствующий этому имени функции по индексу = 2 в массиве AddressOfNameOrdinals, который в настоящее время равен 4. • AddressOfFunctions содержит смещения RVA общего числа функций NumberOfFunctions именованных и анонимно экспортированных функций в виде массива DWORD, и этот массив отсортирован в порядке номеров функций. Следовательно, с помощью функции func03\x00 с порядком 4, упомянутой ранее, мы можем затем получить func03 RVA 0x1030 (30 10 00 00) с индексом = 4 в массиве AddressOfFunctions. Внимательные читатели заметят, что порядковый номер функции явно сохраняется от 1 до n, но тот, который сохраняется в массиве AddressOfNameOrdinals, становится от 0 до (n-1). Поскольку первый элемент в массиве C/C++ обычно начинается с 0, а не с 1, правильным термином для того, что сохраняет AddressOfNameOrdinals, является индексный массив AddressOfFunctions. Однако читателям нужно только помнить, что если они хотят передать индекс из AddressOfNameOrdinals в GetProcAddress как порядковый номер функции, преобразование будет просто +1 к порядковому номеру функции. Важная заметка На рис. 4.9 в качестве примера используется DLL, созданная MinGW. Соглашение о компиляции состоит в том, чтобы хранить всю структуру EAT в отдельном разделе .edata, тогда как другие компиляторы могут не выдавать результаты с разделом .edata. Например, EAT, сгенерированный набором инструментов компиляции Visual Studio C++, помещается в конец .text (используется для хранения содержимого кода). Однако если модуль DLL имеет экспортированную функцию, он должен иметь структуру EAT, чтобы третья программа могла получить экспортированную функцию. В этом разделе мы подробно объяснили EAT в PE и RVA, хранящиеся в таблице DataDirectory. Мы используем инструмент PE-bear для анализа информации в EAT и объяснения значения каждой функции. Эта информация полезна для анализа и изменения модулей DLL. Примеры анализатора файлов DLL Следующие примеры взяты из проекта peExportParser в папке Chapter#4 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть полный проект, который общедоступен в репозитории этой книги. Давайте применим то, что мы узнали, на практике и попробуем просканировать весь модуль DLL на наличие именованных функций в чисто статической ситуации. Поскольку анализ будет выполняться в чисто статическом состоянии, первая проблема будет состоять в том, чтобы весь EAT содержал все свои данные в виде RVA (т. е. динамических отображенных в файл смещений). Поэтому нам нужно создать функцию, которая поможет нам автоматизировать преобразование RVA обратно в смещения относительно текущего содержимого статического файла для правильного захвата данных. На рис. 4.10 показана простая функция rvaToOffset, которая помогает нам в этом процессе:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В главе 2 «Память процессов — сопоставление файлов, синтаксический анализатор PE, tinyLinker и Hollowing» мы упомянули, что метод размещения содержимого каждой секции в динамической памяти, как и ожидалось, называется процессом сопоставления файлов. Например, секция .text располагается по смещению 0x200 для текущего статического содержимого программы и размещается по смещению 0x1000 после сопоставления файлов. Если мы наблюдаем функцию во время динамического выполнения RVA 0x1234 (в .text), то мы можем сделать вывод, что эта функция хранится в статическом содержимом программы по адресу 0x434 (0x200 + 0x234). Таким образом, функция на рис. 4.10 разработана с учетом того, что, получив RVA, мы повторяем цикл for для перечисления заголовков каждого раздела и того, в каком разделе находится RVA после сопоставления файлов. Затем RVA вычитается из динамически отображаемого базового адреса раздела, чтобы получить смещение относительно раздела. Добавление начальной точки смещения раздела в статическое содержимое дает нам правильное положение RVA. На рис. 4.11 показан вход основной функции:
В строках с 47 по 49 кода, после чтения всего файла DLL, мы сначала извлекаем OptionalHeader из части NT Headers этой PE-структуры, запрашиваем таблицу DataDirectory для RVA экспортированной функции IMAGE_DIRECTORY_ENTRY_EXPORT и преобразовываем ее в статическую программу
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
смещение содержимого с помощью функции rvaToOffset, которую мы только что разработали. В строках 52-53 кода теперь, когда у нас есть смещение EAT, мы можем правильно прочитать структуру EAT IMAGE_EXPORT_DIRECTORY, добавив базовый адрес статического содержимого. Далее имя периода компиляции, записанное в EAT, преобразуется из RVA в смещение, и добавляется статический базовый адрес содержимого для корректной печати имени периода компиляции DLL-файла. В строках 56-59 кода, следуя тому же методу, мы можем получить адрес статического содержимого из массива AddressOfNames и распечатать имя каждого экспортированного имени функции в цикле for. Далее, давайте посмотрим на силу этого инструмента на практике:
Чтобы проверить правильность и надежность нашего метода расчета, мы изменили проект на 64-разрядный и создали файл peExportParser.exe. Мы использовали этот инструмент, чтобы попытаться разобрать C:\Windows\System32\kernel32.dll для анализа экспортированных функций. Результаты, показанные на рис. 4.12, оказались такими же успешными, как мы и ожидали: все экспортированные имена функций проанализированы и пронумерованы корректно. Функция динамического сканирования в PE В этом подразделе мы соберем воедино все полезные советы из предыдущих глав. Следуя методике обхода PEB → LDR для поиска адресов системных модулей без Windows API, описанной в главе 3, мы сможем аналогичным образом находить адреса системных функций без GetProcAddress. Этот метод основан на чистом динамическом анализе PE-структуры, называемом методом PE-сканирования. Это хорошо известный метод, используемый в шеллкоде для поиска адресов API. Следующий пример — это исходный код dynEatCall.c в папке Chapter#4 проекта GitHub. В целях экономии места в этой книге показывается только выделенный код; пожалуйста, обратитесь к полному
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
исходному коду, чтобы увидеть все детали проекта. На рис. 4.13 показан вход основной функции:
Эта точка входа точно такая же, как основная функция файла ldrParser.c в главе 3, как показано на рис. 3.15. Единственное отличие состоит в том, что вместо использования GetProcAddress мы теперь используем нашу собственную функцию GetFuncAddr для поиска адреса функции. На рис. 4.14 показан полный дизайн функции GetFuncAddr:
В строках 104-107 кода входящий динамический адрес модуля DLL анализируется в формате PE, чтобы найти EAT RVA, записанный в разделе «Необязательный заголовок» → «DataDirectory». В строках 110-115 кода мы идентифицируем три важных набора указателей массива, AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals, в EAT, чтобы найти правильный адрес в динамической памяти, а затем использовать их для обхода до нужного адреса функции.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 118-125 кода используйте цикл for для извлечения каждого из экспортированных имен функций по порядку и используйте stricmp для проверки того, что текущее имя функции является тем, которое мы ищем. Если это так, порядковый номер функции, соответствующий имени функции, берется из AddressOfNameOrdinals и используется в качестве индекса для запроса AddressOfFunctions для правильного RVA функции. Затем добавьте текущий базовый адрес модуля DLL, чтобы получить правильный динамический адрес функции. На рис. 4.15 показаны результаты dynEatCall, скомпилированного и выполненного с помощью MinGW (32-разрядная версия):
Как видите, dynEatCall запускает и анализирует файл kernel32.dll, чтобы экспортировать функцию WinExec с порядковым номером функции 0x0601, и успешно вызывает WinExec, чтобы открыть инструмент калькулятора. Инструмент PE-bear также используется для анализа экспортированной функции kernel32.dll, WinExec (см. интерфейс за окном консоли), и подтверждает, что порядковый номер равен 0x601, что доказывает правильность и надежность нашего процесса вычислений. Важные заметки dynEatCall .c компилируется и выполняется в 32-битном MinGW. Поэтому, если читатели используют 64-разрядную среду Windows, полный путь к kernel32.dll должен быть C:\Windows\SysWoW64\kernel32.dll, а не C:\Windows\System32\kernel32.dll. Это следует отметить особо. Общие модули DLL для 32-разрядной версии Windows хранятся в папке C:\Windows\System32. Однако 64-разрядные программы должны быть обратно совместимы с 32-разрядными программами, поэтому существует два каталога: System32 и SysWoW64. System32 используется для сохранения 64-битных модулей DLL, а SysWoW64 — для сохранения 32-битных модулей DLL.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Windows 32 на Windows 64 (WoW64) — это архитектура, специфичная для Windows. Она предназначен для эмуляции транслятора, обратно совместимого с 32-битными исполняемыми форматами, а также может работать в 64-битных средах Windows. Она также отвечает за преобразование 32-битных системных прерываний в 64-битные системные прерывания, чтобы отправить их в 64-битное ядро для нормального анализа и выполнения. В этом разделе мы представили две программы собственной разработки, peExportParser и dynEatCall.c, для проверки информации в экспортированной таблице функций из предыдущего раздела. Проект peExportParser использует функцию rvaToOffset для вычисления RVA как фактического смещения и анализа структуры PE для получения информации EAT. dynEatCall.c использует нашу собственную функцию GetFuncAddr для поиска адреса функции, а не системную функцию GetProcAddress. Результаты подтверждают правильность наших знаний и расчетов относительно EAT. Примеры написания шеллкода на x86 Теперь, когда мы рассмотрели реализацию Windows PE для распределения статической и динамической памяти, а также способы успешного вызова указателя системной функции, мы начнем этот раздел с дальнейшего обсуждения использования того, что мы узнали, для разработки 32-битного шелл-кода с помощью команды x86 самостоятельно. Следующий пример — это исходный код 32b_shellcode.asm в папке Chapter#4 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному проекту для получения полного исходного кода. Поскольку это демонстрация разработки 32-битного шелл-кода, нам нужно использовать компилятор, который поможет нам перевести сценарий x86 в машинный код, который может прочитать чип. Читателям рекомендуется попрактиковаться в этом разделе, загрузив ассемблер x86 с открытым исходным кодом Moska (github.com/aaaddress1/moska), написанный автором этой книги, который может скомпилировать любой x86-скрипт на основе движка Keystone и выдать 32-битный ассемблер. bit *.EXE, чтобы читатель дважды щелкнул по нему и проверил выполнение шелл-кода. Важная заметка Многие учебники по написанию x86 предназначены только для того, чтобы студенты начали работу с языком ассемблера, и поэтому только учат вас, как использовать системные прерывания. Например, nasm научит вас писать 16-разрядные программы на ассемблере для MSDOS, в которых состояние памяти не распределяется, как описано в этой книге. Однако наша цель — написать шелл-код, который можно будет применять в реальном мире. Поэтому рекомендуется использовать инструмент Moska, разработанный автором, или Visual Studio C++ со встроенным _asm, встроенным в язык ассемблера, для практики написания шеллкода, а для регулярного использования инструмента сборки автор рекомендует инструмент yasm с открытым исходным кодом с поддержкой для синтаксиса Intel. Мы видим ассемблерный код 32b_shellcode.asm. Он запускается как шеллкод и пытается найти базовый адрес образа DLL-модуля kernel32 в текущей памяти и просматривает структуру PE, чтобы найти в ней адрес функции FatalExit. Весь сценарий разделен на три части для индивидуальной интерпретации. В первой части мы видим
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
следующее:
В разделе «Блок среды потока» главы 3 мы объяснили, что в 32-разрядной версии Windows регистр раздела fs[+n] может напрямую запрашивать данные по смещению TEB, равному +n. Мы знаем, что правильный адрес для 32-битной структуры PEB можно получить по адресу TEB +0x30 в 32-битной Windows, а затем мы можем получить поле LDR по адресу PEB +0x0C. Мы также объяснили, что двустороннюю цепочку LDR_DATA_TABLE_ENTRY можно получить из InLoadOrderModuleList(+0x0c), где каждый узел представляет собой структуру LDR_DATA_TABLE_ENTRY для записи информации о смонтированном модуле. 32-битная структура LDR_DATA_TABLE_ENTRY может получить базовый адрес образа DLL (DllBase) и имя модуля DLL (BaseDllName), хранящееся в форме UNICODE_STRING со смещением +0x18 и +0x2C соответственно. В строках 8-13 кода мы видим цикл, который проползает по вышеупомянутой цепочке, находит узел LDR_DATA_TABLE_ENTRY модуля kernel32 и записывает его поле DllBase в регистр eax. Извлекаем DllBase в регистр eax, затем из поля BaseDllName → Buffer получаем имя модуля в виде массива расширенных символов (wchar_t*), и проверяем, является ли текущий массив строк Kernel32 (сравнивая, является ли байт 0x0C ASCII 0x33 для числа 3). Если нет, продолжайте извлекать InLoadOrderLinks->Flink из структуры LDR_DATA_TABLE_ENTRY с +0 в качестве текущего узла анализа, пока не найдете. Во второй части, когда у нас есть базовый адрес образа DLL, пришло время начать обход EAT. На рис. 4.17 показан процесс обхода таблицы функций экспорта:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 15-19 кода базовый адрес образа DLL должен содержать структуру IMAGE_DOS_HEADER, и мы можем получить поле e_lfanew по адресу +0x3C, которое содержит текущую структуру PE, смещение IMAGE_NT_HEADERS. Затем RVA элемента DataDirectory 0 (т. е. EAT) можно получить из IMAGE_NT_HEADERS +0x78 и вместе с базовым адресом образа DLL, тремя важными полями: AddressOfNames (+0x20), AddressOfNameOrdinals (+0x24) и AddressOfFunctions (+ 0x1C) структуры IMAGE_EXPORT_DIRECTORY в текущей памяти. Далее в строках 23-29 кода нам нужно узнать, какой индекс является именем экспортируемой функции, которую мы ищем. Мы используем ebp в качестве счетчика, чтобы отслеживать количество элементов, которые мы уже перечислили. Мы знаем, что смещение каждого имени хранится в виде 4-байтового массива DWORD, поэтому мы можем перечислить все экспортированные имена функций из базового адреса массива AddressOfNames + индекс 4 *, пока не найдем FatalExit API с соответствием ASCII (0x74697845 равно значение ASCII Exit), а затем остановитесь. В третьей части мы видим следующее:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 31-38 кода мы уже сохранили имя экспортируемой функции, которую хотим, в счетчике ebp (индексе). Затем мы можем получить порядковый номер функции, соответствующей этому текстовому имени функции, из массива AddressOfNameOrdinals (WORD) и использовать этот порядковый номер в качестве индекса для запроса массива AddressOfFunctions для получения правильной функции RVA. После добавления RVA в DllBase мы можем получить правильный адрес экспорта. Далее кладем строку 30см. tw (0x0077742e, 0x6d633033) поверх стека с помощью push и вызова указателя функции FatalExit, чтобы получить успешное всплывающее сообщение. На рис. 4.19 показан результат преобразования файла 32b_shellcode.asm в последовательности машинного кода с использованием авторского инструмента с открытым исходным кодом Moska и имитации компоновщика для загрузки шелл-кода в виде файла .exe. Результатом запуска a.exe является успешное всплывающее окно с текстовым сообщением 30cm.tw:
Важная заметка Ради единообразия с точки зрения смещения памяти и макета вся книга проиллюстрирована с использованием 32-битной PE-структуры, но концепции и алгоритмы аналогичны. Таким образом, заменив смещение на 64-битное смещение PE и изменив PEB togs[0x60], читатели могут легко написать 64-битный шеллкод самостоятельно. В этом разделе мы использовали реальный исходный код 32b_shellcode.asm, чтобы проиллюстрировать, как шаг за шагом сопоставить базовый адрес образа DLL и найти EAT для функции экспорта FatalExit. Мы использовали инструмент с открытым исходным кодом автора, Moska, для компиляции и загрузки шелл-кода, чтобы проверить его работоспособность. Мы доказали, что можем разработать 32-битный шелл-код вручную, используя то, что узнали в предыдущих главах. Генератор шелл-кода на Python Теперь мы попытались сами написать минималистичный 32-битный шеллкод, и читатели увидят большое количество структурных смещений, которые необходимо помнить в процессе. На практике это может затруднить процесс разработки при наличии сложных требований к задаче. По этой причине
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
многие бесплатные инструменты сообщества были разработаны для автоматизации генерации шелл-кода, например, Metasploit. В этом разделе мы попытаемся разработать более удобный инструмент, который может генерировать шелл-код непосредственно из кода C/C++. Следующий пример — это исходный код shellDev.py из папки Chapter#4 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть все детали проекта:
В главе 1 «От исходного кода к двоичным файлам — путь программы на языке C» мы упоминали, что в процессе компиляции есть как минимум три процесса: компиляция, сборка и компоновка. Сначала исходный код C/C++ компилируется в скрипт на языке ассемблера. Далее следует компиляция в блоки машинного кода и ресурсов (инкапсулированных в COFF). Наконец, он загружается в исполняемый файл с помощью компоновщика. Однако на практике наш шелл-код — это сам машинный код, поэтому нам не нужен компоновщик для участия в процессе загрузки. Мы просто упаковываем содержимое машинного кода, чтобы создать исполняемый шелл-код в виде открытого текста. Заинтересованные читатели могут обратиться к инструменту автора с открытым исходным кодом, shellDev.py (github.com/aaaddress1/shellDev.py), чтобы сделать это. Просто напишите пример C/C++ для автоматической генерации 32- и 64-битного шелл-кода без необходимости вручную организовывать какие-либо структуры памяти и смещения. На рис. 4.21 слева показан исходный код примера C/C++, а справа инструмент shellDev.py, написанный на Python, который автоматически вызывает компилятор MinGW и генерирует шелл-код:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Все основы разработки этого инструмента были полностью объяснены в первых четырех главах этой книги, поэтому мы не будем занимать место, объясняя их. Заинтересованные читатели могут прочитать исходный код этого инструмента с открытым исходным кодом напрямую. В этой главе мы представили инструмент с открытым исходным кодом, написанный на Python, shellDev.py, который автоматизирует создание шелл-кода. По сути, он берет концепции из глав с 1 по 3 и связывает их вместе в Python, уменьшая необходимость для программистов вручную упорядочивать любые структуры памяти или смещения. Краткое содержание В этой главе мы подробно узнали о функциях экспорта и о том, как создать собственный анализатор DLL, не полагаясь на функцию Windows GetProcAddress, просмотреть динамическую память в поисках функций экспорта и написать собственный шелл-код Windows. Наконец, мы можем даже разработать генератор шелл-кода через Python. Обладая этими знаниями и навыками, мы сможем в будущем разрабатывать собственные инструменты для тестирования на проникновение, а не ограничиваться уже разработанными инструментами.
Дизайн загрузчика приложений В этой главе мы узнаем, как простой загрузчик приложений может выполнять файлы EXE в памяти, не создавая дочерний процесс. Мы узнаем, как импортировать таблицу адресов в структуру PE и писать программы на C для их анализа. Затем мы узнаем, как перехватывать вызовы Windows API, заменять поведение API вредоносным кодом и выполнять боковую загрузку DLL на примерах. В этой главе мы рассмотрим следующие основные темы: • Импорт таблицы адресов в PE • Пример анализатора импорта API • Примеры захвата IAT • Пример боковой загрузки DLL Импорт таблицы адресов в PE Как мы упоминали в главе 1 «От исходного кода к двоичным файлам — путь программы на языке C»,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
при выполнении программы выполняется следующая процедура. Сначала создается новый процесс и в него загружается статическое содержимое в виде карты файлов; затем первый поток этого процесса вызывает функцию загрузчика, расположенную в ntdll.dll. После внесения необходимых исправлений в PE-модуль, смонтированный в памяти, функция входа EXE-модуля может быть выполнена, и программа будет нормально работать как процесс. В этой главе мы более подробно рассмотрим загрузчик приложений, который по умолчанию входит в состав операционной системы. Этот вариант можно использовать для разработки упаковщика программ, бесфайловых атак, поэтапных полезных нагрузок (например, поэтапных полезных нагрузок в Metasploit) и так далее. Давайте сначала вернемся к основам. Рисунок 5.1 идентичен рисунку 1.3 и иллюстрирует программу, которая выводит всплывающее сообщение с MessageBoxA:
Скомпилированная программа будет иметь как минимум три блока контента: • Секция .text используется для хранения машинного кода после компиляции исходного кода. • Секция .rdata используется для хранения статического текста или данных. Например, +0x05 содержит текст hi как массив строк ASCII. • Секция .idata используется для хранения набора массивов указателей функций. Например, +0x18 предназначен для хранения правильного адреса текущей системной функции MessageBoxA. После сопоставления статического содержимого с базой образа 0x400000 секция .text размещается по адресу 0x401000, секция .rdata размещается по адресу 0x402000, а секция .idata размещается по адресу
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
0x403000. Следовательно, после сопоставления файла поведение вызова функции ds:[0x403018] получает правильный адрес функции MessageBoxA из 0x403018 и вызывает его. Как вы можете себе представить, прочитав здесь, как только мы монтируем файл программы в память с помощью File Mapping (обсуждается в главе 2) как загруженный модуль и записываем правильные указатели API в массивы указателей функций в .idata, это позволяет программе получить желаемый API для вызова в исполнении. Это позволяет нам запускать любую PE-программу непосредственно в памяти, не создавая другой объект процесса. Итак, давайте объясним, как анализировать таблицу указателей функции импорта. Прежде всего, где находится глобальная таблица указателей функций импорта (массив адресов функций импорта)? На рис. 5.2 показана страница символов при отладке msgbox.exe с помощью x64dbg. В левой части показаны все PE-модули, смонтированные в данный момент в памяти, с их текущим базовым адресом образа (т. е. msgbox.exe в настоящее время смонтирован по адресу 0x400000), а в правой части перечислены все адреса импорта или экспорта для текущего PE-модуля:
Здесь мы видим, что адреса функций импорта, fprintf, free, fwrite, getchar и т. д., каждый раз увеличивались с 0x4071BC до 0x4071E8 с шагом 4 байта (DWORD). Результат шестнадцатеричного дампа показывает, что 0x4071BC содержит адрес функции fprintf 0x768A4C90 в форме с прямым порядком байтов, а 0x4071C0 содержит свободный адрес функции 0x76878570. Кроме того, текущий адрес MessageBoxA — 0x76CD2270. Вы должны заметить, что этот массив представляет собой ту же самую таблицу указателей глобальной функции импорта, созданную компилятором, как упоминалось ранее. Но как просканировать все поля функций импорта на символической странице PE-структуры? На рис. 5.3 показано распределение динамической памяти таблицы адресов импорта (IAT) для msgbox.exe:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Второй элемент (IMAGE_DIRECTORY_ENTRY_IMPORT) в структуре DataDirectory заголовков NT указывает на относительный виртуальный адрес (RVA) всей таблицы импорта. В начале таблицы импорта находится набор массивов IMAGE_IMPORT_DESCRIPTOR, и каждая структура IMAGE_IMPORT_DESCRIPTOR используется для записи информации DLL импорта. Возьмем рис. 5.3 в качестве примера: текущий msgbox.exe импортирует в три модуля: USER32.dll, KERNEL32.dll и MSVCRT.dll. Во время компиляции создается таблица имен импорта (INT), каждый элемент которой записывает имя функции импорта в структуру IMAGE_IMPORT_BY_NAME. IAT — это глобальная таблица указателей функций импорта, о которой мы упоминали ранее. Она отвечает за запись всех функций, импортируемых текущей программой, и каждое поле записывает текущий адрес системной функции в IMAGE_THUNK_DATA (4-байтовая структура, используемая в качестве переменной-указателя) для последующих фаз выполнения. Код в разделе .text можно получить из этих полей. Например, поле со смещением+0 в таблице функций используется для хранения адреса функции MessageBoxA, поэтому программа может использовать инструкцию call ds: [0x403600] для получения адреса функции и ее вызова. Вы сразу заметите, что на рис. 5.3 есть две таблицы, которые выглядят совершенно одинаково. Действительно, таблица имен подсказок (HNT) содержит точно такое же содержимое, что и IAT при статическом анализе. Однако во время динамического выполнения мы сказали, что каждое поле в IAT будет исправлено загрузчиком приложения с правильным адресом системной функции (вместо RVA имени функции импорта), тогда как HNT не исправлено загрузчиком. Эта функция позволяет нам создавать дамп динамической памяти любого процесса и при этом знать, какие системные функции импортируются этой программой. Мы часто используем эту функцию в функции Fix IAT Table в инструменте упаковки. В этом разделе мы узнали о содержимом IAT в структуре PE и характеристиках различных полей IAT в динамической памяти. Пример анализатора импорта API Следующий пример — это исходный код iat_parser.cpp в папке Chapter#5 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код. Пожалуйста, обратитесь к полному исходному коду, чтобы прочитать полный проект.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Попробуем написать инструменты для анализа того, какие системные функции импортируются в EXE-программы. На рис. 5.4 показана функция входа в iat_parser.cpp:
В строках 44-50 кода мы сначала считываем всю программу в память с помощью fopen, и получаем размер глобального IAT и его RVA из 13-го элемента (т. е. IMAGE_DIRECTORY_ENTRY_IAT) в DataDirectory. Поскольку каждое поле в глобальном IAT является правильным адресом системной функции, который указан в разделе .text и будет указывать на RVA структуры хранения имени системной функции (IMAGE_IMPORT_BY_NAME) в INT, каждое поле, следовательно, является переменной IMAGE_THUNK_DATA. Мы просто делим размер IAT на размер IMAGE_THUNK_DATA, чтобы узнать, сколько всего API будет импортировано в эту программу. В строках 53-59 кода мы можем извлечь RVA структуры IMAGE_IMPORT_BY_NAME, на которую указывает каждое из предыдущих полей, используя цикл for, и преобразовать RVA в смещение, соответствующее статическому содержимому, чтобы узнать, какое имя системной функции поле соответствует. Результаты iat_parser.cpp показаны на рис. 5.5:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
iat_parser.cpp компилируется и выполняется для перечисления системных функций, импортированных в IAT статического содержимого msgbox.exe. В этом разделе мы перечислили имена API системных функций, импортированные в IAT msgbox.exe как реальную программу. Однако только IAT не позволяет нам узнать, из какого модуля DLL были импортированы эти имена функций. В следующем подразделе мы рассмотрим, как анализировать полную IAT и использовать методы. Вызов программ непосредственно в памяти В этом подразделе мы объединяем все, чему научились ранее, чтобы показать вам, как запускать EXE-программу в чистой памяти без необходимости создавать еще один объект процесса, что является довольно незаметным способом. Этот метод широко используется в новых типах вредоносных программ. Это очень сложный метод, который обходит статическое антивирусное сканирование файловых систем, считывая содержимое вредоносного ПО в память, расшифровывая его и выполняя его в памяти по сети. Это может обойти некоторые методы статического сканирования, поскольку вообще не создаются процессы, за которыми антивирусное программное обеспечение может активно следить. Эта техника использовалась шпионским проектом Athena, инсценированными полезными нагрузками Metasploit и даже группировками кибер-армии MustangPanda и APT41 в своих атаках. Следующий пример — это исходный код invoke_memExe.cpp в папке Chapter#5 проекта GitHub. На рис. 5.6 показана функция fixIat, используемая для исправления таблицы импорта для PE-модулей с файловым отображением в памяти:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 29-30 кода мы сначала получаем адрес текущего ИАТ из поля IMAGE_DIRECTORY_ENTRY_IMPORT во втором элементе DataDirectory и конвертируем его в массив IMAGE_IMPORT_DESCRIPTOR. Затем мы можем просмотреть все модули импорта и соответствующие поля функций импорта. В строках 31-34 кода мы можем получить имя модуля, импортированного в данный момент программой, из поля Name в IMAGE_IMPORT_DESCRIPTOR, загрузить его в память с помощью LoadLibraryA и получить возвращаемое значение в качестве базового адреса изображения модуля. В строках 36-43 кода мы упомянули, что FirstThunk IMAGE_IMPORT_DESCRIPTOR указывает на набор массивов IMAGE_THUNK_DATA, где каждое поле IMAGE_THUNK_DATA — это отдельный адрес функции, содержащий переменную, а его исходное содержимое указывает на текстовую структуру IMAGE_IMPORT_BY_NAME в INT. Итак, здесь мы извлечем имя функции, используем GetProcAddress, чтобы найти адрес текущего модуля, и запишем обратно структуру IMAGE_THUNK_DATA, чтобы успешно исправить IAT. На рис. 5.7 показана функция invokeMemExe в invoke_memExe.cpp, используемая для вызова статического содержимого EXE непосредственно в памяти:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 48-50 кода мы проверяем текущую ImageBase, чтобы узнать ожидаемую базу создания образов исполняемого файла, и используем функцию VirtualAlloc, чтобы запросить достаточно памяти по ожидаемому адресу для последующего выделения EXE-файла. В строках 53-61 кода это стандартный процесс сопоставления файлов в главе 1. Все структуры заголовков PE (т. е. заголовок DOS, заголовки NT и заголовки разделов) сначала перемещаются из статического содержимого в память, а затем каждый блок содержимого раздела размещается по соответствующему ожидаемому адресу для завершения отображения файла. В строках 62-69 кода мы используем функцию fixIat, которую мы только что разработали, для исправления PE-модуля с файловым отображением, а затем вызываем точку входа программы, чтобы успешно выполнить ее из памяти. Далее на рис. 5.8 показана основная функция текущей записи программы, которая пытается прочитать содержимое программы из указанного пути в память с помощью fopen и выполняет статическое содержимое из памяти с помощью функции invoke_memExe:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 5.9 показан файл invoke_memExe.cpp, скомпилированный MinGW и запущенный для запуска msgbox. exe с базовым адресом образа по умолчанию 0xFF00000:
Результаты показывают, что эта лабораторная работа успешно выполнила поведение [email protected], открыла диалоговое окно с сообщением, но не выполнила программу как подпроцесс. Это указывает на то, что он был успешно выполнен из памяти, а не как отдельный процесс, созданный API CreateProcess. Важная заметка
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Внимательные читатели должны были заметить, что [email protected] — это специально разработанная программа, для которой во время компиляции задан базовый адрес образа 0xFF00000 (вместо обычного 0x400000). Поскольку текущий динамический модуль invoke_memExe.exe уже занимает память по адресу 0x400000, мы не можем запросить новое пространство по адресу 0x400000 для сопоставления файла EXE. Но что, если мы хотим запустить EXE-программу с тем же 0x400000 ImageBase (адресом модуля) в памяти, даже если 0x400000 уже занято? Мы объясним это в Главе 6, Перемещение модуля PE. В этом разделе мы объяснили, как загружается и выполняется статическая программа, и использовали фактический invoke_memExe.cpp, чтобы проиллюстрировать и объяснить, как программа EXE может выполняться в памяти без создания отдельного процесса для выполнения. Примеры подмены IAT Поскольку каждый IMAGE_THUNK_DATA в IAT содержит адрес системной функции, нельзя ли было бы отслеживать и перехватывать активное поведение программы, если бы мы могли перезаписать содержимое IMAGE_THUNK_DATA функцией для целей мониторинга? Ответ положительный. Давайте попробуем это на примере программы. Следующий пример — это исходный код iatHook.cpp в папке Chapter#5 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы прочитать полный проект:
На рис. 5.10 показан исходный код функции iatHook, которая считывает четыре параметра: • module: указывает на загруженный модуль для мониторинга. • szHook_ApiName: имя функции, которую необходимо перехватить. • callback: функция для целей мониторинга
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• apiAddr: исходный правильный адрес захваченной функции. В строках 15-17 кода таблица импорта модуля считывается из адреса памяти загруженного PE-модуля, а затем преобразуется в массив IMAGE_IMPORT_DESCRIPTOR для перечисления каждого из упомянутых модулей. Мы упоминали, что IAT и HNT будут идентичны в памяти. Разница в том, что первый будет исправлен для заполнения текущего адреса системной функции во время выполнения; предпоследний не будет, а последний останется указывать на структуру IMAGE_IMPORT_BY_NAME. Это означает, что если мы сможем подтвердить, что fieldi является той функцией, которую мы хотим захватить, просканировав HNT, то мы сможем вернуться к IAT и заменить адрес системной функции, хранящийся в поле i, на тот, который мы отслеживаем. Таким образом, мы завершили технику захвата IAT. В строках 20-30 кода мы используем цикл for для извлечения структуры IMAGE_THUNK_DATA из каждого HNT, извлекаем соответствующую структуру IMAGE_IMPORT_BY_NAME, чтобы узнать имя i-й функции, и используем strcmp для подтверждения этого. Затем мы можем заполнить i-е поле IAT адресом нашей функции мониторинга. На рис. 5.11 показана основная функция входа:
В строках 37-40 кода мы написали лямбда-функцию ptr для мониторинга MessageBoxA. Когда функция мониторинга вызывается, она распечатывает параметры, полученные MessageBoxA, и подделывает строку msgbox got hooked в качестве нового параметра, который будет передан исходной системной функции MessageBoxA. В строках 42-43 кода мы используем GetModuleHandle(NULL) для получения текущего адреса EXE-модуля (то есть базы образа PEB). Затем мы можем вызвать функцию iatHook, которую мы только что разработали, чтобы перехватить API MessageBoxA в IAT текущего EXE-модуля, а затем запустить MessageBoxA, чтобы вывести на экран тестовую строку Iat Hook, чтобы проверить, был ли захват успешным. На рис. 5.12 показан результат iatHook.cpp после компиляции и выполнения:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мы видим, что строка Iat Hook Test, которая должна отображаться во всплывающем окне, была перехвачена и распечатана функцией монитора, а исходное выполнение MessageBoxA было преобразовано в msgbox got перехваченное строковое содержимое. В этом разделе мы узнали о технике захвата IAT. Переписывая содержимое IMAGE_THUNK_DATA нашей функцией, мы можем успешно захватить и подделать содержимое MessageBoxA. Этот метод мониторинга и захвата может показаться простым, но он широко используется в разработке игровых плагинов, в инструментах-песочницах, таких как песочница для анализа вредоносных программ Cuckoo, и даже во многих облегченных антивирусных активных средствах защиты. Пример боковой загрузки DLL Пример боковой загрузки DLL или перехват DLL — это классический метод взлома, который задокументирован в MITRE ATT&CK® как метод атаки Hijack Execution Flow: DLL Side-Loading, Subtechnique T1574.002 (атака. /). Основной принцип заключается в замене загруженной системной DLL на одну, разработанную хакером для управления выполнением процесса. Это означает, что точно разместив нужный вредоносный DLL-модуль, хакер может запустить его как любой EXE-процесс, например, притворившись системным служебным процессом с цифровой подписью. Многие правила антивирусного программного обеспечения рассматривают программы с цифровыми подписями в своих механизмах обнаружения как безопасные программы. Вот почему APT-группы широко используют эту технику, чтобы избежать статического антивирусного сканирования, активного защитного мониторинга или запроса UAC на повышение привилегий. Для получения более подробной информации об этом вы можете обратиться к публичному отчету FireEye «Боковая загрузка DLL: еще одно слепое пятно для антивируса» (https://www.mandiant.com/resources/blog/dll-side- loading-anotherblind-spot-for-anti-virus), что указывает на то, что этот метод широко использовался APT-группами еще в 2014 году. На рис. 5.13 показаны результаты браузера Chrome 88.0.4324.146 после анализа его IAT с помощью инструмента PE-bear:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Зная IAT, мы можем понять, что при запуске Chrome процесс должен загрузить chrome_elf.dll, KERNEL32.dll и VERSION.dll в динамическую память. Программа Chrome импортирует в VERSION.dll три функции экспорта: GetFileVersionInfoSizeW, GetFileVersionInfoW и VerQueryValueW. Прежде чем приложение вызовет функцию ввода, загрузчик приложения должен будет найти и загрузить модуль и заполнить IAT. Если модуль ищется только по строке VERSION.dll, как загрузчик приложения определяет, где в файловой системе находится VERSION.dll? На рис. 5.14 показано поведение программы Chrome при ее запуске, записанное с помощью известного инструмента Process Monitor:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Мы можем наблюдать запрос API CreateFile на рис. 5.14 с выделенным цветом. Все мы знаем, что VERSION.dll — это системный модуль, поэтому он должен находиться либо в C:\Windows\System32\VERSION.dll, либо в C:\WIndows\SysWOW64\VERSION.dll, в зависимости от того, является ли он 32-разрядным или 64-битным. Однако Chrome попытается отдать приоритет загрузке VERSION.dll в том же каталоге. Мы можем дважды щелкнуть по событию и посмотреть, какая программа пытается загрузить модуль DLL по этому пути. На рис. 5.15 показан стек вызовов, когда Process Monitor отслеживает загруженный модуль системной DLL:
Нижняя запись (кадр 20) показывает, что загрузка DLL из пути инициируется NtDLL!LdrInitializeThunk; то есть путь DLL проверяется, когда загрузчик приложения пытается исправить IAT. Из-за неуверенности в правильности пути к модулю DLL загрузчик приложения сначала проверит, есть ли в текущем рабочем каталоге DLL с таким же именем. Если есть, DLL будет загружаться непосредственно из того же каталога; если нет, то системные папки C:\Windows\System32\, C:\Windows\SysWOW64 и C:\Windows будут проверены на наличие такой папки. Если такового по-прежнему нет, то перечисленные пути в переменной окружения PATH будут проверяться итеративно. Это поведение официально задокументировано корпорацией Майкрософт как порядок поиска библиотеки динамической связи (docs. microsoft.com/en-us/windows/win32/dlls/dynamic-link-librarysearch-order). Он используется для слепого поиска абсолютного пути к DLL, когда абсолютный путь к модулю DLL не определен.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Как, несомненно, поймут умные читатели, вы можете успешно взломать Chrome, поместив собственную вредоносную версию VERSION.dll в тот же каталог, что и Chrome. Следующий пример — это исходный проект DLLHijack в папке Chapter#5 проекта GitHub. На рис. 5.16 показана функция входа вредоносного DLL-кода:
В строках 17-21 кода, когда модуль DLL впервые монтируется в процессе, будет отображаться всплывающее сообщение MessageBoxA, чтобы подтвердить наш успешный захват. Однако загрузчик приложения монтирует VERSION.dll в память, чтобы получить адреса функций GetFileVersionInfoSizeW, GetFileVersionInfoW и VerQueryValueW, поэтому наш модуль DLL также должен экспортировать эти три функции для запроса загрузчиком. В строках 8-14 кода мы используем функцию переадресации функций, предоставляемую компоновщиком Microsoft Visual C++ (MSVC), чтобы наша DLL могла экспортировать эти три функции экспорта, но на практике она вызывает три указанные функции в C:\ Windows\System32\VERSION.dll. Этот метод перенаправления функций для атак известен как проксирование DLL. Заинтересованные читатели также могут обратиться к прокси-серверу DLL для персистенса — Red Teaming Experiments (www.ired.team/offensive-security/persistence/dll-proxying-for-persistence). Затем DLL компилируется и переименовывается в VERSION.dll и помещается в тот же каталог, что и Chrome. Каждый раз, когда пользователь пытается использовать Chrome для доступа в Интернет, запускается вредоносный код, помещенный в DLL, как показано на рис. 5.17:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В этом разделе мы узнали о принципах боковой загрузки DLL и о том, как она на самом деле используется в атаках. Боковая загрузка DLL — это метод, который часто используется APT-группами для использования, обхода антивирусного программного обеспечения или сохранения бэкдора. Пока файл DLL может быть записан в файловую систему, процесс выполнения можно контролировать. Вы должны иметь в виду, что этот метод часто может использоваться в различных вариациях для целей эксплуатации или защиты. Краткое содержание В этой главе мы объяснили, как загрузчик приложения выполняется через IAT в структуре PE, и подробно объяснили различные поля в IAT. Мы также узнали о таких атаках, как непосредственный вызов программ в памяти, перехват IAT и неопубликованная загрузка DLL. Эти методы часто используются злоумышленниками для разработки , бесфайловых атак и поэтапных полезных нагрузок для повышения привилегий, обхода антивирусного программного обеспечения или сокрытия бэкдоров. Поняв, как работают эти методы, вы сможете в будущем разработать методы тестирования красной команды или защиты синей команды. В следующей главе мы рассмотрим более глубокий вопрос: что делать, если двоичный файл PE не может быть помещен в память (базу образа), требуемую компилятором? Дизайн перенаправления модуля PE может помочь! Просто примените коррекцию перенаправления, что позволит нам разместить PE-модуль на любой базе образа, не предполагаемой компилятором. Поэтому в следующей главе мы сможем разработать наиболее компактный и полный системный загрузчик для выполнения любой программы в памяти.
Вложения
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
1684341841900.png
Перемещение модуля PE В предыдущих главах мы создали прочную основу для программирования, начиная с компиляции исходного кода C/C++, создания статических программных файлов, проверки динамического распределения памяти и выполнения программ непосредственно в памяти. В этой главе мы узнаем о дизайне перемещения PE-модулей. Мы узнаем, как вручную анализировать двоичный файл PE и реализовать динамическое перемещение модуля PE, позволяющее загружать любую программу в память. В этой главе мы рассмотрим следующие основные темы: • Таблица перемещений PE • Пример tinyLoader Таблица перемещений PE В предыдущих главах мы предполагали, что исполняемые файлы должны быть смонтированы на основе образов, ожидаемых компилятором. Однако в следующих случаях нам может понадобиться смонтировать PE-модуль на основе образа, который не ожидается во время компиляции: • В одном процессе должно быть несколько смонтированных PE-модулей (независимо от EXE или DLL), и очевидно, что общий адрес образа 0x400000 не может быть выбран для каждого модуля DLL во время компиляции. Поэтому Microsoft разработала релокацию для PE, которая используется для решения проблемы сопоставления PE-модуля с неожиданной базой образа. • В разделе Вызов программ непосредственно из памяти главы 5 мы столкнулись с похожей проблемой с загрузчиком приложений, которую пытались воспроизвести. Поскольку загрузчик приложения уже сопоставлен с адресом 0x400000, больше невозможно смонтировать EXE-файл на занятую память 0x400000. • С исправлением Service Pack 2 (SP2) Windows XP обеспечивает защиту от рандомизации адресного пространства (ASLR) на системном уровне, позволяя вам монтировать любой EXE или DLL в любом пространстве памяти, если компилятор предоставляет таблицу перемещения. В результате PE-модули не могут быть смонтированы на непредусмотренной основе образа. Как решалась эта проблема в первые дни, например, до Windows XP (патч SP1)? Умные читатели сразу же подумают: если базовый адрес образа по умолчанию является случайным адресом, выпавшим во
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
время компиляции, очень маловероятно, что адрес, который хочет использовать модуль, будет занят. Давайте посмотрим на ситуацию на рисунке 6.1:
На рис. 6.1 показано содержимое раздела OptionalHeader файла DLL, сгенерированного MinGW после двойной компиляции одного и того же исходного кода DLL на C/C++ и открытия его с помощью инструмента PE-bear. Мы видим, что первая сгенерированная DLL по умолчанию имеет значение 0x69740000 во время компиляции, а вторая сгенерированная DLL по умолчанию имеет значение 0x66280000. Это идеальное решение? Нет. В настоящее время механизм декодирования аудио/видеоплеера, механизм JavaScript браузера или модуль управления ресурсами онлайн-игры (содержащей большое количество изображений и аудио/видео файлов) могут занимать более 2 МБ в одном PE-модуле, что приводит к конфликту при выборе адреса памяти. Следовательно, необходимо иметь решение, которое прекрасно справляется с сопоставлением PE-модулей с непредусмотренными адресами. Это называется релокацией. Давайте используем быструю диаграмму, чтобы объяснить концепцию перемещения:
На рис. 6.2 показано, что текущий раздел .text сопоставлен с адресом 0x401000, а по адресу 0x40100C находится инструкция call dword ptr: [0x403018]. Это работает с текущим базовым адресом образа 0x400000 и вызывает адрес функции, хранящийся в разделе .idata, на 0x403000.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Но что, если msgbox.exe сопоставлен с 0xA00000? Эту строку следует исправить, чтобы вызвать dword ptr : [0xA03018] для правильного выполнения. Таким образом, задача перемещения состоит в том, чтобы исправить все такие записи с 0x403018 (18 30 40 00) на 0xA03018 (18 30 A0 00), что является адресом 4 байтов, сохраненных по адресу 0x40100E, на правильный новый адрес. Шестой элемент в таблице DataDirectory для OptionalHeader (IMAGE_DIRECTORY_ENTRY_BASERELOC) указывает на структуру, называемую таблицей перемещений, которая содержит массив записей о перемещениях переменной длины. Содержимое образа всего PE-модуля во время динамического выполнения разрезается на структуру блока размером 4 КБ (0x1000, что является минимальным выравниванием блока) и структуру relocHdr (IMAGE_BASE_RELOCATION) для записи того, какую структуру VirtualAddress необходимо исправить. Однако структура VirtualAddress с содержимым размером 4 КБ не будет иметь только одно место для исправления (т. е. может быть несколько мест для перемещения). Поэтому набор массивов ENTRY (BASE_RELOCATION_ENTRY) дополняется в конце relocHdr. Каждая структура ENTRY имеет фиксированный размер и используется для записи того, какое смещение в текущей структуре VirtualAddress необходимо переместить. Возьмите рисунок 6.2 в качестве примера. Таблица перемещений, записанная в текущем DataDirectory[IMAGE_BASE_RELOCATION], указывает на адрес раздела .reloc. Вначале мы можем проанализировать структуру IMAGE_BASE_RELOCATION, чтобы узнать, что relocHdr#1 в настоящее время имеет виртуальный адрес 0x1000, который необходимо исправить, и что вся структура relocHdr#1, включая массив ENTRY, занимает всего 0x0E байт. Таким образом, мы можем вычислить адрес relocHdr#1 + 0x0E = 0x40400E, чтобы найти запись relocHdr#2: нужно исправить содержимое VA = 0x2000, а вся структура занимает 0x10 байт. Итак, мы вычисляем адрес relocHdr#2 + 0x10 = 0x40401E, чтобы найти запись relocHdr#3. Каждая структура ENTRY имеет поле типа в старших 4 битах: значение может быть RELOC_32BIT_FIELD (0x03) для 32-битного числового адреса или RELOC_64BIT_FIELD (0x0A) для 64-битного числового адреса, который необходимо исправить. Поле нижнего 12-битного смещения содержит тип 32- или 64-битных адресов в известном смещении VirtualAddress, которые необходимо исправить. В качестве иллюстрации воспользуемся записью relocHdr#1. Первые 8 байтов этой записи содержат структуру IMAGE_BASE_RELOCATION (VirtualAddress 0x1000), за которой следует массив ENTRY в форме структуры BASE_RELOCATION_ENTRY, которая содержит 0x3003, 0x3007 и 0x300E по порядку, представляя, что всего 3 значения смещения +3, +7 и +0x0E в форме RELOC_32BIT_FIELD по адресу 0x1000 необходимо исправить. Итак, мы знаем, что нам нужно исправить три записи адреса данных (ожидаемый компилятором расчет базы изображения), 0x401003, 0x401007 и 0x40100E, на новый адрес в секции .text. Например, на рис. 6.2 есть инструкция push 0x402005 с машинным кодом 68 05 20 40 00 по адресу 0x401006 в секции .text. Видно, что 4 байта машинного кода по адресу 0x401007 в секции .text содержат значение 0x402005. Если текущий базовый адрес образа перемещается с 0x400000 на 0xA00000, то текущий push 0x402005 (68 05 20 40 00) необходимо обновить, чтобы отправить 0xA02005 (68 05 20 A0 00). В этом разделе мы узнали о концепции перемещения, а также объяснили и рассчитали, как перемещаться в структуре PE с помощью динамического распределения памяти. Пример крошечного загрузчика В следующем примере представлен исходный код peLoader.cpp в папке Chapter#6 проекта GitHub. В
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы увидеть все детали проекта. Сначала мы компилируем наш исходный код msgbox.c, как мы это делали в главе 1 с MinGW, и используем аргументы -Wl,-- dynamicbase,--export-all-symbols для создания EXE-файла с таблицей перемещения, msgbox_reloc.exe, как показано на рисунке 6.3:
На рис. 6.4 показана функция fixReloc, отвечающая за исправление задачи перемещения для всего PE-модуля:
В строках 56-57 кода мы можем получить начальную точку таблицы перемещений, хранящуюся в текущем разделе .reloc, из DataDirectory[IMAGE_BASE_RELOCATION], а затем использовать ее для анализа полей перемещений. В строках 60-62 кода, во-первых, мы можем получить структуру firstIMAGE_BASE_RELOCATION по адресу таблицы релокации, которая позволяет нам идентифицировать RVA, который необходимо
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
исправить, из VirtualAddress. Конец структуры IMAGE_BASE_RELOCATION является начальной точкой массива ENTRY. Из этого массива мы можем получить количество смещений, которые необходимо переместить относительно RVA. Поскольку мы сказали, что SizeOfBlock в IMAGE_BASE_RELOCATION содержит суммарный размер массивов IMAGE_BASE_RELOCATION и ENTRY, мы можем узнать, сколько в нем смещений, вычитая размер IMAGE_BASE_RELOCATION из SizeOfBlock и разделив его на размер структуры ENTRY. В строках 64-74 кода, поскольку поле Type каждой структуры ENTRY записывает значение как 32-битное или 64-битное, мы затем корректируем значение RVA+Offset соответствующим образом (UINT32/UINT64). RVA получается путем вычитания исходного ожидаемого значения (т. е. VirtualAddress, рассчитанного на основе ожидаемого базового адреса образа) из базового адреса образа, ожидаемого компилятором. Этот RVA затем добавляется к новому базовому адресу образа, чтобы исправить адрес данных на VirtualAddress по новому базовому адресу образа. Функция peLoader показана на рис. 6.5:
Его функция точно такая же, как у invoke_memExe в разделе «Вызов программ непосредственно в памяти» главы 5: отображение статического содержимого в файле, исправление таблицы адресов импорта, а затем попытка вызова функции ее ввода. Разница в том, что функция peLoader на рис. 6.5 отдает приоритет тому, имеет ли текущее статическое содержимое таблицу перемещения. Если это так, это означает, что программа позволяет отображать файлы на любой адрес, который не ожидался во время компиляции, поэтому мы можем использовать VirtualAlloc(NULL, imgSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECRESERVE, PAGE_EXECUTE_READWRITE) для запроса памяти по любому адресу без каких-либо ограничений. Если нет, это означает, что программе разрешено сопоставляться только с ожидаемым базовым адресом образа. На рис. 6.6 показана функция входа (т. е. главная функция):
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Он используется для чтения двоичного файла по указанному пользователем пути в память, а затем пытается запустить его статическое содержимое в памяти с помощью функции peLoader. На рис. 6.7 показан результат компиляции peLoader.cpp для создания peLoader и его использования для запуска msgbox_reloc.exe в памяти:
Можно отметить, что PE-bear показывает, что в msgbox_reloc.exe компилятор ожидает сопоставление файла с адресом 0x400000, но из-за его таблицы перемещения он должен быть сопоставлен с адресом 0x20000 после выполнения задачи перемещения и может работать нормально. В этом разделе мы объяснили, как создать самую компактную версию загрузчика приложений с реальной программой. С таблицей перемещения мы можем переместить программу для нормальной работы. Однако загрузчик приложения отвечает не только за эти действия. Заинтересованные читатели
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
могут обратиться к проекту автора с открытым исходным кодом RunPE-In-Memory (github.com/aaaddress1/RunPE-In-Memory), чтобы узнать больше о том, как разработать более полный загрузчик приложений. Краткое содержание В этой главе мы узнали о дизайне перемещения PE-модулей. В качестве примера мы научились анализировать и использовать таблицу релокаций PE-структуры и реализовывать дизайн релокаций для динамических PE-модулей. Этот метод перемещения позволяет нам монтировать EXE или DLL в любое пространство памяти, которое мы хотим, с помощью нашего собственного загрузчика приложений. В следующей главе мы воспользуемся всеми полученными знаниями, чтобы представить, как написать облегченный загрузчик приложений в x86, который можно использовать для преобразования любого модуля DLL в шеллкод. Эта классическая техника широко использовалась как в дикой природе, так и в коммерческих пакетах атак, таких как Metasploit и Cobalt Strike.
PE в шеллкод — преобразование PE файлов в шеллкод Теперь у вас есть прочная основа знаний о том, как спроектировать минималистичный загрузчик приложений. Мы можем перейти к тому, как преобразовать любой исполняемый файл непосредственно в шелл-код без необходимости писать шелл-код. В этой главе мы расскажем, как написать легкий загрузчик на ассемблере x86, который можно использовать для преобразования любого EXE-файла в шеллкод. В этой главе мы рассмотрим следующие основные темы: • Разбор экспортной таблицы Kernel32 в сборке x86. • Получение адресов API в сборке x86 • Сопоставление файлов и восстановление таблицы импорта в x86 • Обработка перемещения в x86 • Пример преобразования PE в шеллкод Анализ проекта с открытым исходным кодом pe_to_shellcode Польская исследовательница Александра Донец (@hasherezade в Твиттере) из Malwarebytes выпустила проект pe_to_shellcode с открытым исходным кодом (github.com/hasherezade/pe_to_shellcode), который представляет собой набор заглушек, написанных на языке ассемблера x86. Стаб на самом деле является шеллкодом, за исключением того, что полезная нагрузка, обычно используемая для загрузки, называется заглушкой. Этот проект с открытым исходным кодом представляет собой полную реализацию облегченного загрузчика приложений. В этой главе мы будем использовать 32-битную версию этого проекта. В предыдущей главе мы подробно рассказали, что для облегченного загрузчика приложений потребуются как минимум три задачи: 1. Выделите новую память для монтирования целевого EXE-файла путем сопоставления файлов. 2. Зафиксируйте IAT.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
3. Переместите адреса согласно таблице перемещений. Первая задача использует VirtualAlloc для запроса блока памяти; вторая задача использует LoadLibraryA для монтирования DLL в динамическую память и GetProcAddress для поиска правильного адреса функции экспорта. Итак, если мы разделим заглушку на две части, то увидим следующее: • Первая часть отвечает за поиск базы образа Kernel32 путем перечисления структуры Ldr (загрузчик) в структуре блока среды процесса (PEB) (используется для записи необходимой внешней информации для правильной работы текущего процесса) и нахождения трех предыдущих необходимых функции, анализируя PE в памяти и записывая их в стек. • Вторая часть реализует три задачи полного загрузчика приложений и вызывает исходную точку входа (OEP). На рис. 7.1 показан код в начале заглушки:
После получения адреса Ldr из структуры PEB, первый адрес структуры LDR_DATA_TABLE_ENTRY получается из регистра esi с помощью lodsd и сохраняется в регистре eax (по порядку первым будет база образа ntdll.dll). Затем используйте xchg eax, esi, чтобы lodsd мог загрузить следующий указатель LDR_DATA_TABLE_ENTRY из первого LDR_DATA_TABLE_ENTRY->InLoadOrderLinks. Адрес следующей структуры LDR_DATA_TABLE_ENTRY будет точно таким же, как у модуля Kernel32.dll. Текущий базовый
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
адрес образа Kernel32.dll затем извлекается из структуры и сохраняется в регистре ebp. Наконец, инструкция call переходит к parse_exports для продолжения выполнения, помещая адрес возврата (который является базовым адресом таблицы API CRC) в верхнюю часть стека в соответствии с характером инструкции call. Разбор таблицы экспорта Kernel32 в сборке x86 На рис. 7.2 показан код parse_exports. Вначале в регистр esi записывается только что сохраненный в стеке базовый адрес таблицы API CRC:
В строках 37-39 кода мы начали пытаться сканировать PE поверх базы образа Kernel32.dll, чтобы сохранить базовый адрес таблицы адресов экспорта в регистре ebx. Затем мы начали перечислять имена функций над таблицей по порядку. Здесь регистр edx используется для записи текущей индексной переменной (т. е. для подсчета количества имен функций,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
перечисляемых в данный момент). В строке 40 кода регистр edx очищается с помощью инструкции cdq. В строках 42-46 кода мы извлекаем текущий сохраненный массив имен Kernel32.dll из поля AddressOfNames в таблице адресов экспорта, извлекаем адрес имени функции экспорта edx и сохраняем его в регистре edi. В строках 50-67 кода мы экспортируем первые 8 байт имени функции для стандартного хеширования CRC. Здесь вы можете увидеть магическое число 0xEDB88320. Результат CRC-хэша имени текущей функции сохраняется в регистре eax и сравнивается с CRC-хэшем искомого имени системной функции. Если результат правильный, то текущее имя функции edx — это то, что мы ищем; если нет, то вернитесь к walk_names в строке 42, чтобы продолжить перечисление оставшихся имен функций. Получение адресов API в сборке x86 Следующая задача — получить адрес функции из имени функции edx, как показано на рис. 7.3:
В строках 74-77 кода мы начинаем с получения значения размером WORD из столбца edx AddressOfNameOrdinals, который является порядковым номером функции. В строке 78 кода относительный виртуальный адрес (RVA) текущей функции получается из массива AddressOfFunctions с использованием порядкового номера функции в качестве индекса и добавления ядра Kernel32. dll базовый адрес образа в регистр eax для получения текущего адреса системной функции. В строках 81-84, во-первых, мы используем инструкцию push eax, чтобы поместить текущий адрес системной функции, полученный с помощью eax, в стек для резервного копирования. Затем переместите текущий базовый адрес esi (таблица CRC API) +4 в следующий хеш CRC имени функции с помощью lodsd и сравните его с sub cl, byte ptr [esi], чтобы увидеть, равен ли он 0 (текущее значение регистра ecx равно 0). Если это не 0, вернитесь к walk_names и продолжите обход таблицы, чтобы получить адрес функции и поместить его в стек; если он равен 0, это означает, что адрес функции,
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
соответствующий каждому хэшу CRC в таблице API CRC, был сохранен в стеке. Затем идет стандартный процесс загрузки:
Во-первых, мы используем VirtualAlloc, чтобы запросить блок памяти, достаточно большой для последующей обработки сопоставления файлов, как показано в строках 90–100 на рис. 7.4. Затем мы копируем все заголовки DOS, NT и заголовки разделов в эту память с помощью rep movsb в строках 106-109. Сопоставление файлов и восстановление таблицы импорта в x86 Отображение файла реализовано в строках 116-121. Мы перемещаем содержимое каждого раздела в блоки с помощью rep movsb по адресу ожидаемого RVA для каждого раздела:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
После выполнения этих шагов мы успешно смонтировали PE-файл в память (как модуль процесса). Затем нам нужно исправить IAT этой программы, чтобы она могла получить желаемый адрес API при запуске:
В строках 140-150 мы получаем текущий IAT-адрес исполняемого файла (регистр ebx в настоящее время указывает на только что запрошенный адрес памяти) для перечисления IMAGE_IMPORT_DESCRIPTOR, в котором записаны все имена модулей, используемых программой. Затем используйте LoadLibraryA для монтирования этих модулей DLL в память с диска. Далее на рис. 7.7 показан массив IMAGE_IMPORT_BY_NAME, извлеченный из IMAGE_IMPORT_DESCRIPTOR этой программы, которая использует GetProcAddress для получения
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
адреса API, соответствующего имени API:
Наконец, мы записываем обратно в поле IMAGE_THUNK_DATA с помощью stosd, чтобы исправить IAT, что позволяет программе получить любой необходимый ей API. Обработка перемещения в x86 Как мы видели в Главе 6, Перемещение PE-модуля, любая программа имеет предпочтительный адрес загрузки по умолчанию (ImageBase) в процессах. Однако шеллкод не всегда имеет возможность выделить память по нужному программе адресу. Поэтому нам нужно решить проблему перемещения, чтобы помочь программам, смонтированным на неожиданном ImageBase, работать правильно:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 181-191 сначала находим RVA таблицы релокаций из DataDirectory, затем добавляем базовый адрес образа, чтобы получить правильный адрес таблицы релокаций в текущей памяти и сохраняем в регистре edi. Позже нам нужно перечислить каждый адрес IMAGE_BASE_RELOCATION в таблице перемещений один за другим, поэтому нам нужна переменная для записи смещения последнего решенного нами IMAGE_BASE_RELOCATION. Здесь выбран регистр edx, поэтому в строке 191 мы очищаем регистр edx с помощью cdq:
Отметим, что каждая структура IMAGE_BASE_RELOCATION заканчивается массивом BASE_RELOCATION_ENTRY, чтобы описать, почему смещение необходимо исправить. В строке 194 мы читаем содержимое BASE_RELOCATION_ENTRY (ровно один размер WORD) в регистр eax. В строках 196-199 мы сравниваем поле Type в BASE_RELOCATION_ENTRY 0x03 (RELOC_32BIT_FIELD): если да, значит нужно переместить текущее значение. В строках 201-202 мы берем VirtualAddress, записанный в IMAGE_BASE_RELOCATION, добавляем смещение в BASE_RELOCATION_ENTRY и добавляем новый адрес отображения файла, чтобы
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
определить текущий адрес в регистре eax, который необходимо переместить. В строках 203–206 текущее значение dword ptr [eax] вычитается из ожидаемого компилятором базового адреса изображения для получения RVA, а новый базовый адрес изображения записывается обратно в dword ptr [eax] для завершения задачи перемещения. На рис. 7.10 показано смещение последнего проанализированного IMAGE_BASE_RELOCATION, сохраненного нашим регистром edx, с проверкой, не превысило ли смещение размер всей таблицы перемещений:
Если нет, это означает, что есть поля, которые необходимо переместить, а затем обновить следующий адрес IMAGE_BASE_RELOCATION (добавив адрес регистра edi в SizeOfBlock текущего IMAGE_BASE_RELOCATION) и вернуть reloc_block для продолжения задачи перемещения. Как показано на рис. 7.11, затем мы пытаемся вызвать функцию входа, на которую указывает AddressOfEntryPoint:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
После этого программа EXE будет успешно выполнена в памяти. Пример PE для шеллкода После объяснения принципов пришло время увидеть мощь проекта с открытым исходным кодом pe_to_shellcode (github.com/hasherezade/pe_to_shellcode) в действии. На рис. 7.12 показан файл шелл-кода msgbox.shc.exe, сгенерированный с помощью инструмента pe2shc для чтения msgbox.exe, который имеет суффикс .shc.exe, указывающий, что начало PE-файла (т. е. заголовок DOS) было изменено на заглушку, которая переходит к загрузчику приложения (версия на ассемблере), описанному ранее. Таким образом, весь файл msgbox.shc.exe может быть запущен непосредственно как шеллкод или как обычный исполняемый файл:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 7.13 показана простая программа на C/C++, которая считывает в память все содержимое файла msgbox.shc.exe, изменяет атрибут памяти на исполняемый и использует его как указатель на функцию для запуска. Файл msgbox.shc.exe успешно запускается в памяти (без перехода в новый процесс), а шелл-код, сгенерированный инструментом pe2shc, поставляется с собственным загрузчиком приложений, поэтому нам не нужно заморачиваться реализацией нового загрузчика приложений:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В настоящее время этот метод является одним из самых популярных методов, используемых хакерами в мире, в дополнение к инструментам атаки (таким как Cobalt Strike и Stager Payload от Metasploit). Возьмем пример meterpreter_reverse_tcp из известного инструментария Metasploit. Всего за 350 байт заглушка может выполнять сложные функции бэкдора, такие как снимки экрана, загрузка и скачивание файлов с компьютера жертвы, повышение привилегий и даже сохранение бэкдора. Принцип этого заключается в том, что функция сетевого подключения в winnet.dll вызывается для получения большого шелл-кода с сервера Metasploit в память и вызова шелл-кода. Заинтересованные читатели могут попытаться проанализировать этот большой шеллкод с помощью динамической отладки. Это будет файл DLL, начинающийся с MZ, который содержит сложный дизайн бэкдора и преобразуется из DLL в шеллкод с помощью облегченного загрузчика приложений. Краткое содержание Написание шелл-кода вручную слишком дорого для сложных действий атаки. Современные злоумышленники предпочитают разрабатывать свое вредоносное ПО на C/C++ и конвертировать EXE-файлы в шелл-код для использования. Для этого есть две основные причины: первая заключается в том, что написанный от руки шеллкод является дорогостоящим и трудоемким, а также трудно разрабатывать сложные конструкции бэкдоров, повышенные привилегии или функции бокового перемещения; во-вторых, шелл-код часто используется в качестве кода для перехвата выполнения только в эксплойте первого этапа. На практике из-за как переполнения буфера, так и эксплойтов кучи злоумышленнику часто не хватает
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
места под контролем злоумышленника для хранения всего шелл-кода, поэтому он обычно делится на две части шелл-кода: небольшой шелл-код (называемый заглушкой) отвечает за первый этап эксплойта; в случае успеха шелл-код большего размера загружается в память для выполнения, будь то сетевое соединение, чтение файла или методы поиска яиц. В этой главе мы представили принцип и реализацию прямого преобразования EXE в шелл-код, так что мы можем преобразовать любой исполняемый файл непосредственно в шелл-код без необходимости вручную писать язык ассемблера x86. Далее, если мы хотим избежать обнаружения антивирусом таких программ, нам необходимо обходить их с помощью шеллинга и цифровых подписей. Мы обсудим это в следующих двух главах.
Дизайн программного упаковщика Упаковщик программного обеспечения часто используется киберпреступниками для уменьшения размера исполняемых файлов, чтобы избежать проверки статической сигнатуры антивируса или даже для противодействия анализу обратной инженерии исследователей. Поскольку этот метод особенно важен и часто используется в операциях атаки, в этой главе мы объединим то, что узнали, и разработаем минималистичный программный упаковщик. В этой главе мы рассмотрим следующие основные темы: • Концепция упаковщика • Конструктор пакеров • Стаб – основная программа распаковщика • Примеры упаковщиков программного обеспечения Что такое упаковщик программ? Вы можете представить, что программа, упакованная упаковщиком программного обеспечения, будет защищена или сжата и заключена в оболочку, так что ее внутреннее содержимое не будет видно аналитикам напрямую. Как обычно, мы будем использовать данные о распределении памяти, чтобы дать вам краткий обзор того, как реализована технология упаковки. На рис. 8.1 показано распределение msgbox.exe в динамической фазе до (слева) и после (справа) упаковки программного обеспечения:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В левой части рисунка показано распределение памяти исполняемого файла msgbox.exe после сопоставления файлов, о котором мы упоминали в главе 7. Мы видим, что текущая база образа исполняемого файла смонтирована по адресу 0x400000, а всему PE-модулю выделено всего 0x307A байт в памяти. Раздел .text, содержащий код, в настоящее время расположен по адресам от 0x401000 до 0x401FFF; раздел .data, в котором хранятся данные, располагается по адресам от 0x402000 до 0x402FFF. Раздел .idata, содержащий таблицу импорта, расположен по адресам от 0x403000 до 0x40307A. Точка входа этой программы находится по адресу 0x401234. Простой математический расчет дает следующие результаты: после вычитания 0x307A из RVA 0x1000 первой секции (.text) получается 0x207A, что является общим количеством байтов, занимаемых секциями .text, .data и .idata. во время динамической фазы. Для сравнения справа на рисунке показано распределение памяти после использования программного упаковщика, которое обычно состоит из трех частей: • Раздел text_rwx: большой блок памяти, который доступен для чтения, записи и выполнения (PAGE_EXECUTE_READWRITE) и используется для резервирования достаточного объема памяти для последующего заполнения содержимого сопоставления файлов. • Раздел полезной нагрузки: блок необработанного содержимого программы, который хранится особым образом и может быть сжат, закодирован и зашифрован (в зависимости от основного назначения упаковщика программного обеспечения). • Секция-заглушка: компактный программный распаковщик, дополнительно вставленный в исполняемый файл, отвечающий за восстановление состояния памяти до состояния, ожидаемого при исходной компиляции. Вся упакованная программа сопровождается дополнительной программой-заглушкой, которая декодирует, расшифровывает или распаковывает содержимое раздела полезной нагрузки в
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
соответствии с правильным алгоритмом и записывает его обратно в раздел text_rwx, чтобы вывести исходное распределение памяти с отображением файлов. Поскольку системный загрузчик приложений не знает, что в упакованную программу встроена сжатая PE-программа, он только исправит упакованную программу до стадии исполняемого файла и не исправит исходную программу, которую мы восстановили. Таким образом, работа заглушки состоит в том, чтобы играть роль загрузчика приложения: исправление таблицы импорта, перемещение программы и т. д., чтобы закончить фиксацию образа программы до исполняемого состояния, а затем вернуться к исходной точке входа (OEP) для завершите распаковку и заставьте исходную программу работать правильно. Возьмем рис. 8.1 в качестве примера: раздел text_rwx занимает пространство от 0x401000 до 0x40307A (всего 0x207A), чтобы зарезервировать это пространство для исходных разделов .text, .data и .idata. При нажатии на упакованную программу заглушка отвечает за распаковку сжатого содержимого в разделе полезной нагрузки обратно в раздел text_rwx в соответствии с ожидаемым потоком вычислений, выполнение задачи загрузчика приложения и возврат к исходной записи программы, 0x401234. Однако до этого момента мы говорили о том, как работает упакованная программа. К этому моменту вы, должно быть, поняли: компиляторы не компилируют программы напрямую с оболочками! Верно, так что метод упаковки программного обеспечения обычно состоит из двух частей: 1. Упаковщик несет ответственность за преобразование любого исполняемого файла в упакованное приложение. 2. Упакованное приложение заполняется компактно упакованной программой, а исходное содержимое программы сжимается. Разные пакеры предназначены для разных задач. На практике их обычно делят на две категории: • Упаковщики сжатия: часто со специальной конструкцией или выбранными алгоритмами для сжатия исполняемого файла до меньшего размера. Известными примерами являются UPX и MPRESS. • Защитные пакеры: в дополнение к сжатию они также могут обеспечивать защиту от обратного проектирования или специальную защиту для коммерческих нужд. Примерами являются VMProtect, Themida и Enigma Protector. На практике, независимо от того, используется ли упаковщик сжатия или защитный, исходное содержимое программы неизбежно будет закодировано, зашифровано или сжато. Поэтому антивирусное программное обеспечение, основанное на сканировании статических сигнатур, не может понять или распознать завернутый внутрь код. В результате на китайских форумах распространена поговорка: если не можешь победить антивирус, избавься от него с помощью непопулярного упаковщика. Защитный пакер обеспечивает защиту от обратного инженерного анализа (или взлома), как правило, с помощью различных тестов, чтобы избежать анализа исследователями, или для обеспечения функциональности для коммерческих нужд, обычно следующих типов: • Обфускация: замена машинного кода, сгенерированного компилятором, эквивалентными
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
комбинациями машинных инструкций усложняет исследовательскую задачу анализа. Например, 1 + 1 (код) = 2 (результат выполнения) можно заменить эквивалентом (exp(3) + 44)/33 = 2, что увеличивает время, стоимость и сложность для исследователя, чтобы понять то же самое поведение во время анализа. • Анти-виртуальная машина: из-за неопределенности поведения программы исследователи часто реконструируют упакованные программы на виртуальной машине, готовой к моментальным снимкам. Эта защита обычно осуществляется путем сканирования списка процессов, записей реестра, цикла инструкций и т. д., чтобы проверить, выполняется ли программа на виртуальной машине. Если это так, то упакованная программа не будет распакована и не будет инициализирована. • Anti-debugger/attach: использование отладчика для динамической отладки для определения поведения при выполнении является важной частью обратного проектирования. Следовательно, распаковщик может не запуститься, если обнаружит, что отладчик присоединил его. Например, обычно проверяют, установлен ли логический объект PEB BeingDebugged, чтобы указать, что он монтируется отладчиком, или используют экспортированную ntdll функцию NtSetInformationThread, чтобы установить для текущего атрибута потока значение ThreadHideFromDebugger, чтобы отладчик не мог подключиться к процессу упаковщика. • Защита от несанкционированного доступа: Защита от несанкционированного доступа используется для защиты целостности кода. Например, поле CheckSum в OptionalHeader в структуре PE содержит хеш-значение содержимого программы во время компиляции или дизайн Authenticode в цифровой подписи Windows (упомянутый в главе 9). Аналогичную функцию выполняют упаковщики: хэш циклического контроля избыточности (CRC) динамического или статического содержимого периодически вычисляется, чтобы гарантировать, что сама программа ведет себя так, как ожидает компилятор. Этот метод предпочитают многие корейские производители онлайн-игр, чтобы предотвратить модификацию самой игры, а также его используют такие известные игроки, как nProtect и HackShield. • Виртуальная машина: это поведение замены машинного кода конкретным набором инструкций, разработанным производителем упаковщика, и использование собственного механизма моделирования упаковщика для анализа конкретных инструкций на этапе выполнения. Например, в случае коммерческого упаковщика VMProtect он упрощает интеграцию RISC-инструкций в набор инструкций, специфичный для VMProtect, преобразует исполняемый машинный код в эквивалентные инструкции VMProtect, а заглушка VMProtect имеет соответствующий механизм для трансляции и выполнения эти инструкции. • Коммерческие специальные функции. Коммерческие приложения снабжены дополнительными функциями, такими как проверка серийного номера, ограничение использования или дня, проверка сети и регистрация, активация отображения значков на экране и т. д. Таким образом, компании, использующие защитный пакер, могут сосредоточиться на разработке коммерчески ценных характеристик своих продуктов, а не тратить время на попытки обхода цензуры. Теперь вы можете подумать, что у всех вредоносных программ должен быть коммерческий упаковщик. Это неправда. Основная причина заключается в том, что эти упаковщики обычно покупаются под настоящими именами, а упакованное вредоносное ПО обычно имеет водяной знак коммерческого упаковщика, по которому можно отследить человека, купившего упаковщик. Кроме того, большинство методов, используемых для защиты коммерческих продуктов от анализа, частично совпадают с методами, используемыми в вредоносных программах, что упрощает их ошибочную идентификацию
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
антивирусным программным обеспечением, что не нравится большинству хакеров. В результате киберармии национального уровня или хакеры со значительными техническими навыками часто модифицируют специальный непопулярный упаковщик, основанный на сжимающем упаковщике, чтобы избежать как антивирусного сканирования, так и обратного инженерного анализа. В этом разделе мы узнали об основных концепциях упаковщика, включая три компонента (зарезервированная память, полезная нагрузка и программа-заглушка), а также о различных практических применениях упаковщика. С этими понятиями легче понять, как самому написать простой упаковщик. Упаковщик билдер В этом разделе мы проведем вас через практический процесс разработки с нуля специального непопулярного упаковщика. Следующие примеры представляют собой исходный код packer.cpp из папки Chapter#8 проекта GitHub. Для экономии места эта книга содержит только основные моменты кода; пожалуйста, обратитесь к полному проекту для получения полного исходного кода. На рис. 8.2 показана функция dumpMappedImgBin, которая используется для резервного копирования файлового содержимого оригинальной программы:
Процедура довольно проста: 1. Во-первых, SizeImage дополнительного заголовка может сказать нам, сколько байтов будет занимать вся программа после сопоставления файлов. После вычитания VirtualAddress первого раздела (т. е. заголовков DOS, NT заголовков и заголовков разделов) получается объем памяти, который следует зарезервировать для распаковки и заполнения данных исходной программы. 2. Затем запросите достаточно места в памяти для сохранения содержимого сопоставления файлов. 3. Следуйте процессу сопоставления файлов, чтобы имитировать динамическое распределение памяти статической программы, сохраняя ее в области памяти, только что запрошенной переменной mappedImg.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 8.3 показана конструкция функции compressData:
Поскольку цель этой книги не в том, чтобы научить вас проектировать алгоритмы для высоких коэффициентов сжатия, а объяснять, как обращаться к библиотекам сжатия с открытым исходным кодом, слишком сложно, мы решили использовать RtlCompressBuffer API, изначально поставляемый с Windows.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Вернемся к точке входа упаковщика, то есть к основной функции. Сначала прочитайте статическое содержимое PE пути, на которое указывает входной параметр, в переменную buf и запишите фактический размер программы в переменную png:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 174-177 кода только что считанные данные программы имитируются с помощью dumpMappedImgBin для создания динамического распределения памяти чистых разделов после вычитания заголовков PE-структуры из сопоставления файлов и сохраняются в памяти, на которую указывает mappedImg. Строки 181-188: Содержимое динамической памяти смоделированного сопоставления файлов сжимается, и меньшее содержимое сжатого сопоставления файлов получается в качестве полезной нагрузки. Строки 194-198: Внешний файл stub.bin считывается в переменную x86_Stub как двоичный файл. Затем мы можем написать и сгенерировать машинный код для заглушки в форме шелл-кода, используя компилятор Yasm вместе с GCC, а упаковщик отвечает за заполнение этого содержимого в качестве компоновщика в упакованной программе. На рис. 8.6 показан вызов специально разработанной функции linkBin, отвечающей за преобразование сжатой полезной нагрузки в новый исполняемый файл (упакованную программу):
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 200–203 кода сначала выделите достаточно памяти для временного хранения упакованного статического содержимого, а затем создайте резервную копию заголовков PE (т. новая пустая память, на которую указывает newOutBuf. Затем вызовите функцию linkBin в качестве компоновщика, чтобы построить упакованную программу в этой памяти. Затем в строках 208-213 возьмите PointerToRawData последнего раздела собранного упакованного PE-файла и добавьте размер этого раздела, чтобы вывести размер вывода всей программы. Затем можно легко экспортировать упакованный файл с помощью функции fwrite. Рисунок 8.7 и Рисунок 8.8 показывают детали функции linkBin:
В строках 100-107 кода мы обозначили раздел text_rwx, который динамически отображается, чтобы сохранить все пространство, необходимое для полного содержимого исходной программы, и присвоить разделу читаемые, записываемые и исполняемые атрибуты для облегчения наполнение контента. Этот раздел динамически рассчитывается и распаковывается заглушкой только во время динамической фазы, поэтому в статическом файле нет соответствующего содержимого. Таким образом, PointerToRawData и SizeOfRawData могут быть обнулены напрямую. Строки 110-116: Сохраните машинный код stub.bin как новый раздел с именем stub. Давайте проверим некоторые детали функции linkBin:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 119-125 сохраните сжатую полезную нагрузку как данные в новом разделе в качестве ссылки для заглушки, чтобы распаковать ее позже. Поскольку упакованная программа изменяет EntryPoint таким образом, что заглушка выполняется первой (как восстановление исходной программы), а заглушка действует как загрузчик приложения, для целей исправления требуется информация DataDirectory. По этим причинам необходимо сделать полную резервную копию исходной информации заголовков NT, которая затем используется заглушкой для исправления и возобновления выполнения. В строках 128-137 создайте отдельный раздел ntHdr, в котором хранится содержимое исходных заголовков NT, и заглушка не будет исправлена собственным загрузчиком системы. Итак, мы можем очистить всю таблицу DataDirectory и изменить запись программы на функцию-заглушку. В этом разделе мы шаг за шагом проиллюстрировали минимальные принципы проектирования пакеров с помощью реальной программы. Сначала создается резервная копия исходного сопоставления, считывается файл stub.bin и создается заглушка, а затем вызывается функция linkBin для сборки программы в оболочке в качестве компоновщика. Таким образом, мы завершили упрощенный упаковщик. Stub – основная программа распаковщика На данный момент мы научились разрабатывать программы-упаковщики. В предыдущем разделе мы использовали внешний файл stub.bin для создания главной программы заглушки упаковщика. В этом разделе мы опишем, как разработать заглушку в x86. Следующие примеры представляют собой исходный код stub.asm из папки Chapter#8 проекта GitHub. Для экономии места эта книга содержит только основные моменты кода. Пожалуйста, обратитесь к полному проекту для полного исходного кода. На рис. 8.9 показана точка входа рукописной основной точки x86 заглушки:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Основная задача разбита на три части: • вызов decompress_image: используется для распаковки сжатого содержимого сопоставления файлов полезной нагрузки, для заполнения раздела text_rwx для завершения задачи восстановления исходного содержимого сопоставления файлов и для работы в качестве загрузчика приложения, помогающего исправить таблицу импорта. • вызов recovery_ntHdr: используется упаковщиком для извлечения резервных копий заголовков NT для перезаписи текущих заголовков NT. Поскольку содержимое заголовков NT было изменено после упаковки, если OEP запускается немедленно без восстановления заголовков NT (в исходное ожидаемое состояние), это может привести к серьезным последствиям, таким как исходная программа не сможет найти свою собственную файлы ресурсов (такие как игровая графика, звуки, значки и т. д.). • вызов lookup_oep: после выполнения первых двух шагов мы можем извлечь AddressOfEntryPoint из резервных копий заголовков NT, чтобы узнать исходное смещение OEP и поместить его в стек для записи. Затем мы можем перейти к OEP и успешно возобновить исходную программу. Важная заметка Используйте pushad в записи заглушки, чтобы сделать резервную копию исходного контекста потока (содержимого каждого регистра) в стек, и popad, чтобы восстановить резервную копию из стека после того, как заглушка закончит свою работу. Поскольку некоторые старые программы могут использоваться для извлечения информации из исходного контекста потока, заданного системой (например, адрес PEB), важно, чтобы контекст потока мастер-оболочки работал согласованно с контекстом исходной распакованной программы. Информация, требуемая современными компиляторами, послушно извлекается из вызовов API Win32 в IAT, поэтому меньше нужно беспокоиться об этом или даже выполнять восстановление контекста потока. Поскольку popad восстановит все регистры в исходном контексте потока, мы не можем сохранить OEP в регистрах, чтобы перейти к ним позже. В качестве альтернативы можно поместить запрошенное значение указателя OEP в стек, а затем использовать popad для
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
восстановления содержимого восьми различных регистров в 32-разрядной системе. Таким образом, исходная запись OEP в стеке перемещается в [esp - (8+1) * 4] (т. е. 0x24). На рис. 8.10 показано начало функции decompress_image:
В строках 61-62 кода найдите текущий адрес PEB из регистра сегмента fs и поместите текущий базовый адрес образа основного EXE-модуля в стек для резервного копирования. В строках 65-69 первым узлом, перечисленным в поле PEB→Ldr InLoadOrderModuleList, будет запись ntdll.dll (как структура LDR_DATA_TABLE_ENTRY), поэтому поместите ее образ в стек для резервного копирования. Строки 71-74 используют lodsd для чтения Flink записи ntdll.dll, чтобы получить второй узел, который будет фиксированной записью kernel32.dll, а затем снова поместить базовый адрес его образа в стек для резервного копирования. Строка 75: Затем установите регистр ebp в качестве адреса наверху текущего стека и получите базовые адреса образов kernel32.dll, ntdll.dll и текущего основного модуля EXE на ds:[ebp], ds:[ ebp + 4] и ds:[ebp + 8] в указанном порядке. На рис. 8.11 показан фрагмент кода для поиска указателя Win32 API:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Три API — API ntdll!RtlDecompressBuffer, необходимый для распаковки заглушек, API kernel32!LoadLibraryA и kernel32!GetProcAddress, необходимые для исправления таблицы импорта, — все они ищутся с помощью подпрограммы find_addr и помещаются в стек для резервного копирования. Затем снова установите ebp на адрес в верхней части стека и получите адрес RtlDecompressBuffer, адрес LoadLibraryA, адрес GetProcAddress, kernel32.dll, ntdll.dll и базу образа текущего главного EXE-модуля на ds:[ebp] , ds:[ebp + 8], ds:[ebp + 12], ds:[ebp + 16] и ds:[ebp + 20] в следующем порядке:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
С помощью функции распаковки ntdll!RtlDecompressBuffer мы можем распаковать полезную нагрузку в разделе данных и записать ее обратно в раздел text_rwx. Функция RtlDecompressBuffer имеет шесть параметров, вызываемых в следующем порядке: 1. Тип алгоритма сжатия, например, LZNT1 2. Адрес назначения распакованного содержимого. 3. Объем памяти текущего распакованного содержимого 4. Исходный адрес несжатого контента 5. Размер несжатого контента 6. Указатель переменной ULONG: используется для хранения того, сколько байтов фактически распаковано в место назначения. В строках 115-116 кода сначала мы запрашиваем 4 байта пространства в стеке для переменной ULONG, изначально сохраняя значение 0xdeadbeef. Тогда высшая точка текущего стека, esp, будет адресом переменной, которую мы затем запишем в стек в качестве шестого параметра. Далее нам нужно распаковать полезную нагрузку из раздела данных и заполнить ее в разделе text_rwx. В строках 118-130 вызывается подпрограмма lookupSectionInfo, чтобы увидеть, соответствуют ли
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
первые 4 байта имени каждого раздела в смонтированном основном EXE-модуле строковым данным, хранящимся в регистре edx. Если он находит соответствующий раздел, то сохраняет текущий абсолютный адрес раздела в eax и размер раздела в регистре ebx. С помощью подпрограммы lookupSectionInfo мы можем получить источник полезной нагрузки, размер полезной нагрузки, размер памяти, используемой для сохранения сопоставления распакованного файла, и размер сопоставления исходного файла (что соответствует параметрам с 2 по 5). и укажите алгоритм декомпрессии как LZNT1. Затем мы вызываем функцию RtlDecompressBuffer для ds:[ebp + 0] для распаковки и восстановления исходного содержимого сопоставления файлов. На рис. 8.13 показана полная подпрограмма для исправления IAT:
В строках 141-143 кода сначала мы извлекаем абсолютный адрес таблицы импорта (хранящейся в регистре ecx) из раздела ntHdr резервной копии упаковщика. Далее в таблице импорта находится набор массивов структур IMAGE_IMPORT_DESCRIPTOR, в которых записывается информация о каждом импортируемом модуле и функции. В строках 146-149 кода берем имя текущего модуля DLL из поля Name в текущей структуре IMAGE_IMPORT_DESCRIPTOR (регистр ecx), монтируем его в память с помощью LoadLibraryA и
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
сохраняем базовый адрес этого образа DLL в ebx. Поскольку FirstThunk в структуре IMAGE_IMPORT_DESCRIPTOR указывает на набор массивов IMAGE_THUNK_DATA, каждое поле в этом массиве используется для того, чтобы позволить коду в разделе .text извлечь адрес переменной Win32 API. Эти поля указывают на структуру IMAGE_IMPORT_BY_NAME до того, как они будут исправлены загрузчиком, при этом поле Name указывает на поле IMAGE_THUNK_DATA, которое должно быть заполнено именем системной функции. В строках 149–152 кода задайте для адресата edi и источника esi массив FirstThunk, соответствующий структуре IMAGE_IMPORT_DESCRIPTOR, на которую в данный момент указывает регистр ecx. В строках 154–166 используйте lodsd из исходного кода для извлечения структуры IMAGE_THUNK_DATA, соответствующей имени системной функции в IMAGE_IMPORT_BY_NAME, используйте GetProcAddress для запроса соответствующего адреса функции, затем используйте stosd для обратной записи в регистр esi (т. е. то же поле IMAGE_THUNK_DATA) и продолжайте исправлять до тех пор, пока значение IMAGE_THUNK_DATA не станет пустым, что означает, что оно исправлено до конца. В строках 168-169 регистр ecx указывает на следующую структуру IMAGE_IMPORT_DESCRIPTOR и продолжает повторяться до тех пор, пока все импортированные модули не будут исправлены. После восстановления содержимого динамического сопоставления файлов и исправления IAT пришло время восстановить заголовки NT текущего EXE-модуля, как показано на рис. 8.14:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 20-37 кода мы находим адрес функции VirtualProtect и находим запись NT Headers в резервной копии упаковщика (находится в разделе ntHdr) и размер всей резервной копии. Однако существующее содержимое программы, которое было смонтировано в памяти, не может быть изменено по желанию в соответствии с политикой Windows. Следовательно, далее мы должны использовать Windows API для переключения состояния памяти содержимого программы, чтобы оно могло быть в записанном состоянии. Таким образом, мы можем изменить содержимое смонтированной программы на желаемое, как показано на рис. 8.15:
В строках 43-58 кода мы можем найти NT-заголовки текущего EXE-модуля, установить его в доступное для записи состояние с помощью VirtualProtect, а затем использовать rep movsb для перезаписи NT-заголовков текущего EXE-модуля резервной копией в ntHdr для завершения исправления. Затем мы готовы вернуться к OEP, чтобы закончить исходную программу. На рис. 8.16 показаны две подпрограммы:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
fetch_ntHdr в основном вызывает подпрограмму lookupSectInfo для получения абсолютного адреса раздела ntHdr, а подпрограмма lookup_oep извлекает абсолютный адрес предварительно упакованной точки входа из резервной копии заголовков NT в разделе ntHdr. На рис. 8.17 показана подпрограмма lookupSectInfo:
Она находит базу изображений текущего главного EXE-модуля из PEB→ImageBase и перечисляет, соответствуют ли первые 4 байта имени каждого раздела ASCII-значению запрашиваемого имени
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
раздела. Если найдено, абсолютный адрес и размер раздела помещаются в два регистра, eax и ebx, и функция возвращается. В этом разделе мы объяснили назначение каждого вызова функции в заглушке и шаг за шагом проследили работу машинного кода в реальной программе-заглушке. Примеры программных упаковщиков Мы используем известный компилятор Yasm с открытым исходным кодом для компиляции исходного кода stub.asm в формате COFF, sub.bin, который содержит код заглушки, как показано на рис. 8.18:
Затем мы можем скомпилировать наш упаковщик C/C++ в служебную программу, используя MinGW, как показано на рис. 8.19:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Используя наш скомпилированный упаковщик для упаковки старой игры NS-Shaft, наш скомпилированный упаковщик сожмет содержимое программы и внедрит stub.bin в качестве механизма инициализации для вывода упакованной программы down_protected.exe. Затем мы дважды щелкаем, чтобы открыть файл down_protected.exe. Как показано на рис. 8.20, игровая программа по-прежнему работает нормально, но статический размер успешно сжат с 565 КБ до 280 КБ, что подтверждает осуществимость нашей конструкции сжатого упаковщика:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 8.21 показаны результаты статического реверс-инжинирингового анализа упакованной программы с помощью инструмента IDA Pro:
Видно, что исходный игровой контент больше не отображается в этом инструменте статического анализа. Мы также видим, что упакованная программа не может быть непосредственно проанализирована IDA Pro, поскольку основная программа не использует таблицу импорта, а исходная программа была сжата и защищена. Исследователям необходимо понять работу упаковщика и запустить процедуру распаковки, прежде чем они смогут увидеть код защищаемой игровой программы. Примечание Пример, представленный в этой главе, на самом деле адаптирован из сжатой оболочки автора на чистом C/C++ theArk: Windows x86 PE Packer In C++ (github.com/aaaddress1/theArk). Заинтересованные читатели могут проверить это сами. Принцип реализации точно такой же, как и в этой книге; единственное отличие состоит в том, что он разработан на чистом C/C++. В этом разделе мы фактически скомпилировали наш упаковщик и упаковали его для старой игры. Результат показывает, что программа все еще работает, но ее файл сжат, и IDA Pro не может проанализировать его содержимое. Это доказывает полезность упаковщика. На практике из-за особенностей упаковщиков их также часто используют, чтобы избежать анализа исследователями и обнаружения антивирусным ПО. Краткое содержание В этой главе мы подробно рассказали, как разрабатывать простейшие упаковщики сжатия. Мы узнали о принципах проектирования современных программных упаковщиков и самостоятельно написали сборщик упаковщиков и его программу входа (заглушку). На практике эта технология упаковки программного обеспечения обычно используется киберсилами. Многие непопулярные упаковщики также расширяются на этой основе, добавляя новые функции, такие как анти-отладка и анти-песочница против исследователей, или оснащаются уязвимостями против антивирусного программного обеспечения для повышения огневой мощи вредоносных атак в дикой природе. Технологию, изложенную в этой главе, важно освоить в будущем, независимо от того, пишете ли вы упаковщики или проводите исследования по расшифровке вредоносных программ. В следующей главе мы представим дизайн цифровой подписи Windows. Тот факт, что наличие цифровых подписей в программных файлах часто используется поставщиками антивирусных программ для определения того, заслуживает ли программа доверия, делает злоумышленников крайне
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
заинтересованными в любой возможности злоупотребления проверкой подписи. Мы рассмотрим стандартную спецификацию подписи Windows, Authenticode, и на нескольких примерах объясним, как злоумышленники могут по своему желанию получить любые доверенные подписи для вредоносных программ.
Цифровая подпись — проверка подлинности кода Для пользователей Windows обычной практикой является установка антивирусного программного обеспечения, регулярное обновление систем, тщательный выбор источника загрузки и двойная проверка того, что приложения имеют цифровую подпись надежных технологических компаний. Однако действительно ли этих методов безопасности достаточно, чтобы держать хакеров в страхе? Эта глава может дать читателям совсем другую точку зрения. В этой главе мы узнаем о спецификации Windows Authenticode, обратном проектировании функции проверки подписи, WinVerifyTrust и о том, как перехватить известные цифровые подписи. Эта глава основана на общедоступной презентации «Подрыв доверия в Windows», сделанной Мэттом Грэбером, исследователем безопасности из Spectre Ops, на конференции TROOPERS18 в 2018 годуСоответствующий API аутентификации и вредоносные эксплойты в модели доверия Windows. Автор этой книги выступил с публичной сессией на CYBERSEC 2020 на тему Цифровая подпись? Нет, тебе на это на самом деле наплевать. Заинтересованные читатели могут найти его и посмотреть в Интернете. Этот метод атаки также указан MITRE ATT&CK® как Subvert Trust Controls: SIP and Trust Provider Hijacking (attack.mitre.org/techniques/T1553). В этой главе мы рассмотрим следующие основные темы: • Цифровые подписи Authenticode • Проверка подписи • Примеры фиктивных подписей • Примеры обхода проверки хэша • Примеры стеганографии подписи • Получение подписи путем злоупотребления нормализацией пути Цифровые подписи Authenticode Authenticode — это технология подписи кода, разработанная Microsoft, которая помогает пользователям проверять издателя, подписавшего программу. Это также гарантирует, что подписанная программа не будет изменена злоумышленниками во время транспортировки. Кроме того, подпись, используемая для подписи, должна быть проверена доверенными центрами сертификации (ЦС), чтобы гарантировать, что подписываемый файл действительно исходит от издателя. Дополнительные сведения см. в общедоступном документе Microsoft «Цифровые подписи Authenticode» https://learn.microsoft.com/en-us/windows-hardware/drivers/install/authenticode. В этом введении говорится, что Microsoft разработала спецификацию Authenticode для предоставления
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
механизма цифровой подписи, который позволяет пользователям проверять целостность кода. Эта целостность доказывает, что программа не была подделана хакерами или бэкдорами, а, скорее, исходное программное содержимое было получено от надежной компании без подделки. В документе также указывается спецификация подписи Authenticode для исполняемых файлов (например, программ *.exe или функциональных модулей *.dll) и драйверов *.sys. Это два основных типа подписи. Первый метод (и тот, который используется в основных коммерческих продуктах) — это встроенная цифровая подпись, которая привязывает информацию о подписи для проверки непосредственно к концу структуры PE, чтобы информацию о подписи (как и в записях об отпечатках пальцев) можно было передать другим устройствам, компьютерам для проверки во время переноса, копирования или публикации файла программы. Второй способ — отсоединить цифровую подпись, сохранив запись отпечатка программы (хеш-информацию) в операционной системе C:\Windows\System32\CatRoot, как показано на рис. 9.1, где представлены все записи обособленной подписи в авторской системе. :
Каждый файл с расширением .cat представляет собой инкапсулированную запись ASN.1 — он содержит имя файла и соответствующий хэш содержимого. Эта папка находится в папке C:\Windows\System32\, поэтому только привилегированная системная служба или привилегированный процесс с контролем доступа пользователей (UAC) могут записать в нее файл отпечатка .cat, а не хакер, который может разместить свое вредоносное ПО. отпечатки пальцев там по желанию, чтобы обмануть пользователей и продукты безопасности. В этом разделе мы представили спецификацию цифровой подписи Authenticode и два типа цифровых подписей — встроенную цифровую подпись и отдельную подпись — с использованием общедоступной документации Windows. В следующем разделе описывается использование каждого из двух типов цифровых подписей и подробности соответствующих атак. Проверка подписи Вы можете узнать, как вызывать Windows API для проверки подписи программы, в общедоступном документе Microsoft Пример программы C: проверка подписи PE-файла (docs.microsoft.com/en-
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
us/windows/win32/seccrypto/example-c-program--verifying-the-signature-pe-file). В этом документе представлен полный исходный код C/C++, в котором показано, как вызывать Windows API для проверки действительности цифровой подписи. Следующий пример — это проект winTrust в папке Chapter#9 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; на полный исходный код следует ссылаться в полном проекте для подробного чтения. На рис. 9.2 показана основная секция входа. На рис. 9.2 основная запись довольно компактна, с функцией VerifyEmbeddedSignature, предназначенной для чтения в указанной программе проверки подлинности цифровой подписи и вывода результата на экран:
На рис. 9.3 показана функция VerifyEmbeddedSignature:
В начале функции VerifyEmbeddedSignature объявляется структура WINTRUST_FILE_INFO для указания пути к программе, подлежащей проверке .
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Строки 27-32 кода указывают поле pcwszFilePath структуры на путь проверенного файла. На рис. 9.4 показан процесс инициализации структуры WINTRUST_DATA:
Он содержит много деталей, таких как сохранение параметров для последующих вызовов проверки WinVerifyTrust, указание того, должен ли всплывающий пользовательский интерфейс запрашивать пользователя во время проверки, подтверждение действительности сертификата подписавшего для онлайн-проверки и проверка того, соответствует ли тип документа проверяется отдельная подпись, встроенная подпись или сертификат с полной цифровой подписью. Заинтересованные читатели могут обратиться к официальному файлу Microsoft Win32 API, функции WinVerifyTrust (wintrust.h) (https://learn.microsoft.com/en-us/windows/win32/api/wintrust/nf-wintrustwinverifytrust) для получения дополнительной информации. В строке 49 кода на рис. 9.4 переменная WVTPolicyGUID (которая является переменной типа GUID) объявляется в качестве аргумента функции WinVerifyTrust и устанавливается в значение WINTRUST_ACTION_GENERIC_VERIFY_V2, что означает, что документ, который мы сейчас проверяем, является цифровой подписью подписаны спецификацией Authenticode. Это значение представляет собой набор кодов интерфейса Windows COM, который позволяет WinVerifyTrust проверять функцию экспорта модулей DLL в разных интерфейсах COM, выбирая разные номера GUID. Есть еще два более распространенных варианта:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• HTTPSPROV_ACTION: используется в браузерах Internet Explorer (IE) для проверки цифровой подписи текущего сетевого подключения SSL/TLS HTTPS к другой стороне. • DRIVER_ACTION_VERIFY: это драйвер Windows Hardware Quality Labs (WHQL), используемый для проверки правильности файла. В строке 66 мы можем указать поле pFile структуры WINTRUST_DATA на наш только что подготовленный WINTRUST_FILE_INFO (который записывает информацию о пути к тестовому файлу), чтобы мы могли правильно зафиксировать путь к тестовому файлу при вызове WinVerifyTrust. На рис. 9.5 показана функция WinVerifyTrust:
При вызове функции WinVerifyTrust COM-интерфейс (переменная WVTPolicyGUID) и структура WINTRUST_DATA передаются в качестве параметров и вызываются, а возвращаемое значение сохраняется в переменной lStatus. Возвращаемое значение является результатом проверки подписи, и есть несколько возможных результатов: • ERROR_SUCCESS: подлинность входящего файла подтверждена подписью, и нет никаких сомнений в том, что он был поврежден или подделан. • TRUST_E_NOSIGNATURE: Подпись входящего файла не существует (отсутствует информация о подписи) или цифровая подпись недействительна. • TRUST_E_EXPLICIT_DISTRUST: входящий файл правильно подписан и аутентифицирован, но подпись была отключена подписывающей стороной или текущим пользователем и поэтому недействительна.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• TRUST_E_SUBJECT_NOT_TRUSTED: подпись не является доверенной, поскольку она была заблокирована пользователем вручную, когда сертификат с подписью был установлен в локальной системе. • CRYPT_E_SECURITY_SETTINGS: сертификат подписи отключен групповой политикой, установленной сетевым администратором, результат вычисления отпечатка пальца не соответствует текущему входящему файлу или отметка времени неверна. См. Рисунок 9.6; этот инструмент был скомпилирован и протестирован в системе Windows на калькуляторе и блокноте:
Поскольку оба были подписаны отдельной подписью (знак каталога) и не имеют встроенной информации о подписи, инкапсулированной в спецификации Authenticode, результатом является недопустимая подпись. Файл GoogleUpdate.exe для Google Chrome был протестирован, чтобы подтвердить, что он имеет цифровую подпись Google и что подпись все еще действительна и срок ее действия не истек, когда свойства файла появляются в проводнике, щелкнув правой кнопкой мыши и выбрав «Содержимое». Это подтверждает, что мы использовали WinVerifyTrust, чтобы правильно определить, имеет ли какая-либо программа цифровую подпись Authenticode, и убедиться, что она все еще действительна. WinVerifyTrust под капотом На рис. 9.7 показан рисунок из публичной презентации исследователя Мэтта Грэбера «Подрыв доверия в Windows», где объясняется весь процесс проверки после вызова функции WinVerifyTrust:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Заинтересованные читатели могут обратиться к его техническому документу для получения полной информации о том, как система доверия Windows проверяет цифровые подписи и вредоносные атаки: pecterops.io/assets/resources/ SpecterOps_Subverting_Trust_in_Windows.pdf. Поскольку разные типы файлов имеют разные способы сохранения своих цифровых подписей, для каждого типа проверки файлов в системе Microsoft был разработан отдельный COM-интерфейс (глобальный общий модуль DLL) в качестве интерфейса пакета интерфейса субъекта (SIP) для текущего файла. тип с набором идентификаторов GUID, которые можно отследить до модуля SIP. Следующий вопрос: как внутренняя реализация WinVerifyTrust предназначена для ссылки на использование интерфейса SIP? Блок-схема на рис. 9.7 показывает, что при вызове функции WinVerifyTrust она сначала выполняет необходимую инициализацию, а затем вызывает три функции экспорта для трех Crypt32.dll по порядку:
1. CryptSIPDllIsMyFileType: Функция CryptSIPDllIsMyFileType определяет по порядку, какие типы PE, Catalog, CTL и Cabinet соответствуют текущему входящему файлу, и возвращает номер GUID соответствующего интерфейса SIP. Если это не один из этих четырех типов, тогда он затем подтвердит из реестра, является ли это сценарием PowerShell, установочным пакетом Windows MSI или программой .Appx в Windows Marketplace и т. д., и вернет соответствующий номер GUID в SIP-интерфейс. 2. CryptSIPGetSignedDataMsg: после того, как CryptSIPDllIsMyFileType успешно извлек GUID соответствующего SIP-интерфейса, мы можем использовать CryptSIPGetSignedDataMsg для извлечения информации о подписи (подписанных данных) из файла, соответствующего SIP-интерфейсу.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
3. CryptSIPVerifyIndirectData: результат хеширования текущего файла затем вычисляется как отпечаток пальца и сравнивается с информацией о подписи, извлеченной из CryptSIPGetSignedDataMsg. Если результат хеширования тот же, это означает, что текущий файл идентичен подписываемому файлу; если нет, это означает, что файл был поврежден во время передачи или копирования или что хакер внедрил лазейку и подделал файл. На рис. 9.8 показан обратный инженерный анализ функции PsIsMyFileType, вызываемой внутри CryptSIPDllIsMyFileType для сравнения расширения файла с расширением скриптов PowerShell:
Если это так, возвращается GUID интерфейса SIP для проверки подписи PowerShell. Данные подписи в PE-файлах Далее мы более подробно рассмотрим подписанные PE-файлы. Мы объясним, как информация о подписи встраивается в структуру PE, когда PE-файл подписан Authenticode, в отличие от обычной
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
неподписанной программы. На рис. 9.9 показана таблица Data Directory, полученная в результате анализа программой PE-bear PE-файлов с цифровыми подписями:
На рис. 9.9 мы видим, что адрес поля Security Directory не нулевой, а адрес смещения (0x18C00), указывающий на встроенное сообщение подписи Authenticode размером 0x1948 байт. Затем мы можем перейти на страницу безопасности, чтобы узнать больше. На рис. 9.10 показана встроенная информация о подписи после того, как полезный инструмент PE-bear проанализировал ее:
Мы только что упомянули, что поле Security Directory указывает на структуру сообщения подписи, WIN_CERTIFICATE, по адресу смещения 0x18C00, которое содержит сообщение подписи для текущей
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
проверки программы. На рис. 9.11 показаны поля структуры WIN_CERTIFICATE:
Структура WIN_CERTIFICATE содержит следующие поля: • В поле dwLength записываются байты данных подписи, следующие за начальной точкой информации о подписи (т. е. 0x18C00). • Поле bCertificate используется в качестве отправной точки для всех данных в записи сертификата для проверки. • Поле wCertificateType записывает тип сертификата bCertificate: WIN_CERT_TYPE_X509 (0x0001): сертификат X.509 WIN_CERT_TYPE_PKCS_SIGNED_DATA (0x0002): структура структуры SignedData, дополненная методом PKCS#7. WIN_CERT_TYPE_RESERVED_1 (0x0003): зарезервировано Пример на рис. 9.10 — это документ, подписанный PKCS#7 (0x0002). Поле wRevision может быть WIN_CERT_REVISION_1_0 (0x100) для более старой версии Win_Certificate или WIN_CERT_REVISION_2_0 (0x200) для текущей версии. Примечание 1. Каждое поле в Data Directory представляет собой структуру IMAGE_DATA_DIRECTORY, и адрес, записанный в этой структуре (VirtualAddress), должен представлять собой смещение RVA относительно базы изображения. 2. Здесь мы подчеркнули, что адрес Security Directory является адресом смещения, поскольку специальный адрес, хранящийся в IMAGE_DATA_DIRECTORY, является смещением статического файла (а не VirtualAddress образа процесса). 3. Проверка цифровой подписи предназначена для проверки того, что статическому файлу, который еще не был выполнен, можно доверять, а не для проверки того, что процессу можно доверять на этапе динамического выполнения. Фактически цифровые подписи не могут быть проверены на динамической фазе; если программный файл уже исполнялся и есть большая
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
вероятность, что он уже занесет себя в список доверенных через уязвимость или путем установки самоподписанного сертификата, то нет смысла проверять подпись запущенного процесса. Информация PKCS#7 В официальном документе Microsoft для разработчиков в 2008 г. «Формат переносимой исполняемой подписи Windows Authenticode» четко разъясняются подробности информации о сертификате, хранящейся в bCertificate, и подробности вычисления отпечатка пальца. На рис. 9.12 показана структура информации о подписи, встроенной в PE, на которую ссылается этот документ:
Из рисунка мы можем ясно увидеть следующую информацию: • В структуре типичного формата файла Windows PE слева сообщение о подписи добавляется в конец всего статического файла PE (т. е. в конец последнего сегмента), и добавление начинается со смещения адреса, записанного Security Directory.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• Структура формата подписи Authenticode справа представляет собой информацию о сертификате, заполненную после PKCS#7. Он состоит из трех частей: contentInfo: записывает хэш-значение документа в качестве отпечатка пальца во время подписи. сертификаты: записывает информацию о публичном сертификате X.509 подписывающей стороны. signerInfos: используется для хранения хеш-значения incontentInfo и информации, отображаемой пользователю для просмотра подписывающей стороны, такой как имя подписывающей стороны, ссылочный URL-адрес, время подписи и т. д. Как упоминалось в начале этой главы, спецификация Authenticode предназначена для сопоставления отпечатков пальцев документов путем проверки результатов хэширования документа, чтобы подтвердить, что содержимое документа во время подписания такое же, как и на компьютере пользователя, и не были изменены, подделаны или повреждены во время передачи. Подробности процесса расчета также приведены в последней главе технического документа «Вычисление хеша PE-образа». Ниже приводится пошаговое объяснение: 1. Считайте PE-файл в память и выполните необходимую инициализацию алгоритма хеширования. 2. Хешируйте данные от начала PE-файла до поля контрольной суммы (в структуре дополнительного заголовка заголовков NT) и обновите результат хеширования. 3. Пропустите поле Контрольная сумма и не выполняйте хэш-вычисления. 4. Хэшируйте данные с конца поля Checksum до Security Directory и обновите результат хеширования. 5. Пропустите поле Security Directory (т. е. структуру IMAGE_DATA_DIRECTORY с общим размером 8 байт) и не выполняйте хэширование. 6. Хешируйте данные от конца поля Security Directory до конца массива заголовков блоков и обновите результат хеширования. 7. Первые шесть шагов завершили снятие отпечатков всех заголовков структуры PE, т. е. всего содержимого размера SizeOfHeaders в OptionalHeader (т. е. содержащего заголовки DOS, NT и всю информацию заголовков разделов). 8. Объявите числовую переменную SUM_OF_BYTES_HASHED для хранения количества байтов, для которых было выполнено хеширование, а затем установите значение по умолчанию для этой переменной как значение SizeOfHeaders. 9. Создайте список заголовков разделов, чтобы содержать все заголовки разделов в структуре PE, и отсортируйте заголовки разделов в списке в порядке возрастания согласно PointerToRawData, т. е. заголовки разделов в списке будут отсортированы по смещению разделов. 10. Перечислите каждый заголовок раздела в отсортированном списке по порядку, выполните вычисление блочного хеширования содержимого заголовка раздела и обновите результат хеширования. Переменная SUM_OF_BYTES_HASHED добавляется к размеру блока для каждого блока
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
хэшированного контента. 11. Теоретически информация о подписи Authenticode должна храниться в конце PE-структуры, т. е. хеширование отпечатка PE-файла завершается на шаге 10. Однако на практике в конце могут быть дополнительные данные. подписи. Если это так, вычисление хеш-функции должно быть выполнено снова для всех избыточных данных в конце файла (EOF) подписи, а результат хэширования должен быть обновлен. 12. Хеширование отпечатков пальцев PE-файлов завершено. Примечание При более внимательном прочтении читатели могут увидеть, что поля контрольной суммы и каталога безопасности в дополнительном заголовке, а также сам блок цифровой подписи были намеренно исключены из предыдущего процесса вычисления хэша (как видно на шаге 11 процесса вычисления). Это сделано преднамеренно, так как цифровые подписи вставляются в качестве дополнительных данных после компиляции программы, чтобы избежать последующей вставки данных подписи в PE-файл, что уничтожило бы исходный хэш отпечатка пальца. В этом разделе мы объяснили шаги проверки подписи и узнали о WinVerifyTrust, данных подписи в PE-файлах и информации PKCS#7, а также о пошаговом процессе вычисления отпечатков документов. Затем читатели могут задаться вопросом, может ли хакер подделать подпись Authenticode в каталоге безопасности для вредоносного ПО и обмануть его функцию проверки хэша, чтобы вредоносное ПО выглядело так, как будто оно имеет цифровую подпись. В следующем разделе мы попробуем практический пример. Примеры фиктивных подписей Следующий пример — это проект signalThief в папке Chapter#9 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код. На этом этапе первый эксплойт, о котором могут подумать читатели, — поскольку подписанные программы должны иметь сообщение о подписи Authenticode в конце своих файлов, — это кража чужого сообщения о подписи Authenticode непосредственно внутри нашего вредоносного ПО, которое должно обходить процесс аутентификации. Давайте проверим это. На рис. 9.13 показана функциональная схема кражи статической информации о подписи Authenticode в проекте signalThief:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В строках 26-37 кода находится дизайн функции rippedCert. Он читает входящий PE-файл с помощью fopen и fread, анализирует блок подписи Authenticode, на который указывает Security Directory, и копирует его в переменную certData. На рис. 9.14 показана функция ввода для виджета кражи подписи:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Для этого требуются три параметра пути, указывающие на следующее: • PE-файл с цифровой подписью для кражи • Подписание программы PE • Выходная программа PE В строках 48-56 кода мы сначала копируем подпись Authenticode из PE-файла с цифровой подписью, затем читаем PE-файл, который нужно подписать в качестве полезной нагрузки, и подготавливаем место, достаточно большое для finalPeData, чтобы хранить полезную нагрузку и подпись. Далее в строках 58-64 кода все, что нам нужно сделать, это вставить украденную копию подписи в конец исходной программы и указать Security Directory на наш злонамеренно подделанный блок подписи, и, наконец, использовать fwrite для перетащите поддельный PE-файл в драйвер диска. На рис. 9.15 показана обработка игры Pikachu Volleyball с помощью программы signThief.exe в проекте signThief:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Видно, что он генерирует sign_pika.exe с цифровой подписью, украв и вставив подпись из GoogleUpdate.exe в игру Pikachu Volleyball. Как мы видим, sign_pika.exe был идентифицирован как имеющий подпись Google на экране меню в разделе «Свойства». Однако, поскольку эта подпись не совпадает с хэшем отпечатка пальца, рассчитанным игрой отображается сообщение - Эта цифровая подпись недействительна. На рис. 9.16 представлена запись атаки программы-вымогателя Petya в дикой природе, которую наблюдал исследователь «Лаборатории Касперского» Костин Райу, @craiu:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Он характеризуется использованием крупных национальных утечек (таких как EternalBlue, уязвимости SMB и уязвимости, связанные с MS Office для фишинга) в качестве стандартного пути заражения и нанес глобальный ущерб крупным государственным и частным организациям, таким как аэропорты, метро, банки. В 2017 году @craiu обнаружил, что программа-вымогатель Petya использовала технику кражи подписи, описанную в этом разделе, чтобы сделать бэкдор менее заметным для пользователей, замаскировав его под версию Microsoft, чтобы сбить их с толку. Даже недействительная подпись может стать эффективным способом завоевать доверие пользователей. В этом разделе мы заменили цифровую подпись в игре в качестве практического примера. Это показывает нам, что после загрузки любой программы из неизвестного источника важно не только проверить, есть ли у нее ЭЦП, но и более тщательно проверить, что подпись все еще действительна, чтобы избежать выполнения специально созданной ЭЦП. хакерами. Примеры обхода проверки хэша Для хакеров нельзя ли подделать программу, если она имеет цифровую подпись и подтверждена? В этом разделе мы обсудим, как обойти проверку цифровой подписи. Следующий пример — это проект signVerifyBypass в папке Chapter#9 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код — читатели могут обратиться к полному проекту для подробного изучения. На рис. 9.17 показано описание функции Windows API CryptSIPVerifyIndirectData из публичной
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
презентации исследователя Мэтта Грэбера «Подрыв доверия к Windows».
На этом рисунке Мэтт Грэбер описывает, как после того, как исполняемый файл с цифровой подписью извлек информацию о подписи (т. е. полную структуру WIN_CERTIFICATE, на которую указывает каталог безопасности) с помощью CryptSIPGetSignedDataMsg, информацию о подписи можно проверить с помощью Windows API CryptSIPVerifyIndirectData. Если подпись действительна, она вернет True; в противном случае он вернет False. Если мы сможем подделать эту функцию, то есть любой, кто вызовет функцию CryptSIPVerifyIndirectData для проверки подписи, сможет ответить True, мы сможем достичь цели обхода проверки подписи. На рис. 9.18 показан полный код проекта signVerifyBypass. Мы предполагаем, что пользователи обычно используют контекстное меню Explorer.exe, чтобы убедиться, что программа имеет цифровую подпись и действительна, поэтому мы можем найти способ подделать функцию CryptSIPVerifyIndirectData во всей памяти процесса Explorer:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Процесс, показывающий, имеет ли он цифровую подпись или нет, должен иметь отображаемый оконный интерфейс для взаимодействия с пользователем. Функция EnumWindows используется для перечисления всех отображаемых окон, а функция GetModuleFileNameExA используется для проверки того, является ли полный путь владельца окна C:\Windows\explorer.exe. Если это так, это означает, что владельцем окна является File Explorer. Затем мы записываем машинный код в функцию CryptSIPVerifyIndirectData в ее память с помощью WriteProcessMemory, чтобы функция при вызове должна была возвращать True. После его компиляции и выполнения мы можем увидеть результат на рис. 9.19, где цифровая подпись, которую не удалось проверить в предыдущем разделе, теперь стала легитимной подписью:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это показывает, что наш проект signVerifyBypass завершил подделку проверочного письма File Explorer и успешно превратил фальшивую цифровую подпись в законную цифровую подпись! Этот метод атаки был впервые представлен в официальном документе «Подрыв доверия к Windows», публичной презентации исследователя безопасности Мэтта Грэбера. Первоначальный технический документ содержит более полную системную реализацию системы доверия к цифровой подписи Windows и различные методы атак, такие как перехват CryptSIPGetSignedDataMsg для перенаправления системы для извлечения законных подписей, локальное создание в системе средства проверки информации о законных подписях и т. д. В конечном итоге это позволит нам достичь нашей цели — обойти процесс проверки путем подделки фальшивых подписей. Примечание Если компьютер читателя представляет собой 64-разрядную среду Windows, то проводник, расположенный в C:\Windows\explorer.exe, должен быть 64-разрядным процессом; напротив, проводник в 32-разрядной среде Windows должен быть 32-разрядным процессом с тем же путем. Поэтому, в зависимости от компьютерной среды читателя, этот проект должен быть скомпилирован как 64- или 32-разрядный, чтобы WriteProcessMemory работал правильно. В этом разделе мы реализовали технику атаки из выступления Мэтта Грэбера. Подменяя функцию CryptSIPVerifyIndirectData в памяти, чтобы она всегда возвращала True, можно обойти процесс проверки и превратить поддельную цифровую подпись в законную цифровую подпись. Читателям предлагается прочитать технический документ, представленный Мэттом Грэбером, чтобы узнать больше о системе доверия Windows и атаках. Примеры подписной стеганографии
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
В предыдущем разделе мы добились подмены проверки подписи, подделав системные функции в памяти. Однако до этого момента спуфинг производился только путем исправления функции из памяти. Теперь, когда мы разобрались в деталях вычисления хэша в официальном документе Microsoft, мы попытаемся найти недостатки в процессе вычисления и идеально обойти проверку подписи. Как мы упоминали ранее, в заключительном разделе официального документа Microsoft «Вычисление хэша PE-образа» в процессе хеширования намеренно избегаются три элемента: контрольная сумма, которую можно изменить, вставив сообщение подписи, поле Security Directory, которое используемый для пост-заполнения, и структура самого блока сообщения подписи. Поскольку само сообщение подписи не может использоваться как часть процесса хеширования отпечатка пальца, а подписанная и действующая программа считается безопасной системой доверия Windows (например, антивирусными поставщиками или системой защиты из белого списка), можно скрыть любые вредоносные файлы или данные в блоке сообщений подписи без нарушения действительности подписи. Это делает его отличным местом для укрытия от сканирования антивирусными продуктами. Следующий пример — это проект signStego в папке Chapter#9 проекта GitHub. В целях экономии места в этой книге извлекается только выделенный код; полный исходный код должен быть указан для подробного чтения. На рис. 9.20 показана функция входа для проекта signStego, которая требует, чтобы три параметра пути указывали на программу с цифровой подписью, данные, которые нужно скрыть, и программу вывода:
В строках 34-42 кода он считывает все содержимое цифровой подписи в переменную signedPeDataLen, затем считывает все содержимое данных, которые нужно скрыть, в переменную payloadData и, наконец, запрашивает достаточно большое пространство для временного хранения outputPeData. сохранить содержимое выходной программы.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Согласно спецификации Authenticode, мы можем ожидать, что поле Security Directory будет указывать на структуру цифровой подписи, которая добавляется в самом конце всей программы. Таким образом, в строках 45-47 кода, не разрушая содержимое программы и подписи, мы должны поместить данные, которые мы хотим скрыть, в конец полного блока сообщения подписи и увеличить размер сообщения подписи на единицу. payloadSize, чтобы содержимое полезной нагрузки, которую мы скрываем, распознавалось как часть сообщения подписи. Примечание 1. Если мы просто вставим скрытые данные в конец структуры подписи без увеличения размера, то скрытые данные, которые мы вставляем в программу, будут считаться дополнительными данными в конце программы на шаге 11 вычисления хэша, что приведет к сбою вычисления хеша. 2. Акцент на том, что структура сообщения подписи должна быть добавлена в конец всего файла программы, основан на официальном документе Microsoft по подписи, в котором говорится, что большинство программных файлов, находящихся в настоящее время в раздаче, имеют единую подпись и не учитывают двойные или множественные подписи на сертификатах. На рис. 9.21 показан проект signStego, скомпилированный в программу signStego.exe, и его использование:
Сначала мы сохранили текстовое сообщение Windows APT Warfare от [email protected] в полезной нагрузке, затем использовали этот виджет, чтобы скрыть его в блоке сообщений подписи GoogleUpdate.exe, и создали зараженный файл.exe. Видно, что зараженный файл скрывает в программе полезную нагрузку, которая отлично проверяется
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
даже без подделки функции проверки системы, и мы можем наблюдать шестнадцатеричный вид с CFF Explorer и видеть, что конец программы действительно добавлен с вышеупомянутым текстовым сообщением. На рис. 9.22 показано использование signStego.exe для сокрытия печально известного хакерского инструмента mimikatz в сообщении GoogleUpdate.exe и создания файла mimikatzUpdate.exe:
Мы знаем, что Защитник Windows и другие антивирусные продукты всегда удаляют и предотвращают выполнение программ, содержащих любой шаблон Mimikatz, Metasploit или Cobalt Strike, используя сканирование шаблонов. Однако при сканировании недавно созданного файла mimikatzUpdate.exe с помощью Защитника Windows мы можем обнаружить, что антивирусное программное обеспечение рассматривает mimikatz как часть сообщения подписи и поэтому не удаляет его. Этот метод был впервые представлен миру в презентации Тома Ниправски, исследователя Deep Instinct Research Team, на конференции BlackHat Europe 2016 под названием «Обход сертификата: сокрытие и выполнение вредоносного ПО из исполняемого файла с цифровой подписью». В своей презентации он использовал метод сокрытия печально известного шифровальщика HydraCrypt в сообщении-сигнатуре, а также метод Reflective EXE Loader, чтобы успешно обойти активную защиту антивирусного программного обеспечения ESET и запустить шифровальщика. Этот метод по-прежнему является очень хорошим методом сокрытия вредоносного содержимого при статическом анализе. В этом разделе мы показали еще один способ подделать цифровую подпись как законную. В дополнение к предыдущему разделу, где мы обошли проверку путем фальсификации результатов функции проверки, в этом разделе мы следовали описанию спецификации Authenticode, чтобы скрыть любые вредоносные документы или данные в блоке сообщения подписи, не нарушая достоверность подпись. Таким образом, можно избежать сканирования и обнаружения антивирусного программного обеспечения, чтобы можно было распространять вредоносное ПО. Получение подписи путем злоупотребления нормализацией пути
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Эта методика основана на презентации автора - Digital Signature? Нет, на самом деле вам на это на конференции по информационной безопасности iThome CYBERSEC 2020 на Тайване. наплевать Он в основном основан на исследованиях Мэтта и расширении недостатков безопасности нормализации пути Windows для подделки цифровой подписи. Как мы упоминали ранее, системные функции для проверки цифровой подписи, WinVerifyTrust, будут внутренне вызывать три функции экспорта в Crypt32.dll — CryptSIPDllIsMyFileType, CryptSIPGetSignedDataMsg и CryptSIPVerifyIndirectData — и проверять, что файл на пути имеет действительную цифровую подпись. В предыдущем разделе мы атаковали CryptSIPGetSignedDataMsg, подделав цифровую подпись в любой программе, и мы атаковали CryptSIPVerifyIndirectData, скрыв бэкдор в подписанном программном файле из процесса вычисления хэша отпечатка пальца. В этом разделе мы представим более элегантный подход к подделке подписи, основанный на неправомерных методах нормализации пути Windows для атаки CryptSIPDllIsMyFileType. На рис. 9.23 показано использование пропуска нормализации:
Пропуск нормализации — это функция протокола нормализации пути Windows NT, используемая для поддержки длинных путей, которая позволяет нам обойти нормализацию пути, создав содержимое игровой программы Pikachu Volleyball в виде файла GoogleUpdate.exe\x20 с пустым именем файла. Примечание В обычных условиях в конце папки или файла не может быть пробела; он должен быть удален системой. Это связано с шагом «Обрезка символов» в логике нормализации пути в реализации Windows, который стирает с пути такие символы, как пустые или многослойные папки. Этот метод и вредоносные атаки будут подробно описаны в спецификации преобразования путей Win32 в NT в Приложении. На рис. 9.24 показана системная команда wmic, вызывающая Windows CreateProcess API для создания нового процесса с файлом GoogleUpdate.exe\x20 (с пустым именем файла), который соответствует
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
формату короткого имени файла 8.3 и отображает экран игры Pikachu Volleyball:
Тем временем мы помещаем непустой GoogleUpdate.exe в тот же каталог, что и программа с легальной и действительной подписью. Здесь мы используем известный криминалистический инструмент Process Explorer, чтобы проверить результаты цифровой подписи текущей игры Pikachu Volleyball. Как видно на рис. 9.24, инструмент пытался проверить подпись с помощью WinVerifyTrust в папке C:\WinAPT\chapter#9\GoogleUpdate.exe\x20, но из-за нормализации пути фактически проверяемым файлом был вместо этого C:\WinAPT\chapter#9\GoogleUpdate. exe вместо этого. Это успешно обмануло Process Explorer, идентифицировав его как действительную подпись с результатом Google Inc. На рис. 9.25 представлены результаты более детальной проверки действительности ЭЦП:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Слева — Explorer.exe, показывающий, что программа игры Pikachu Volleyball имеет действительную подпись и подписана Google Inc. Справа — результат успешной подделки после проверки цифровой подписи в Process Monitor. На рис. 9.26 показано, что мы записали в GoogleUpdate пресловутый хакерский инструмент Mimikatz. exe\x20 с неправомерным методом нормализации пути, и он успешно избежал обнаружения Защитником Windows:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это показывает, что даже собственный Защитник Windows от Microsoft уязвим для нормализации пути. В этом разделе мы использовали Пропуск нормализации, чтобы более элегантно обойти процесс проверки подписи, вставив пустое имя файла и используя законную программу подписи в том же каталоге. Краткое содержание Приложениям с цифровыми подписями часто доверяют антивирусные продукты. В этой главе мы узнали о спецификации Microsoft Authenticode и о том, как перехватить известные цифровые подписи. Мы нашли способы обойти процесс проверки цифровой подписи в системах Windows, в том числе атаковать CryptSIPGetSignedDataMsg путем подделки цифровой подписи в любой программе, атаковать CryptSIPVerifyIndirectData, скрывая бэкдор в структуре подписи от процесса вычисления отпечатка пальца, и атаковать CryptSIPVerifyIndirectData путем пропуска нормализации. Мы надеемся, что после прочтения этой главы читатели будут иметь совершенно другое представление о цифровых подписях.
Реверсинг UAC и разные обходные штучки Контроль учетных записей пользователей (UAC) — это средство защиты, предназначенное для предотвращения получения вредоносными программами прав администратора. В этой главе мы реконструируем дизайн UAC, чтобы понять внутренний рабочий процесс защиты UAC и изучить методы, используемые злоумышленниками для обхода дизайна UAC для повышения привилегий. Эта глава основана на книге автора «Атака дублирующих путей: получение повышенных привилегий с помощью поддельных удостоверений», представленной на конференции «Хакеры на Тайване» на конференции студентов по информационным (HITCON) 2019 и «Играя в Win32 как король» технологиям (SITCON) 2020. Эти презентации описывают полную обратную разработку защиты UAC для Windows 10 Enterprise 17763 и представляют методы повышения привилегий UAC для всех версий
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Windows от 7 до 10 на основе эксплойта нормализации пути. Заинтересованные читатели могут искать презентации и полные видеоролики двух сессий. В этой главе мы рассмотрим следующие основные темы: • Обзор UAC • Обратный вызов RAiLaunchAdminProcess • Двухуровневый механизм аутентификации • Условия повышенных привилегий • Примеры обхода UAC Обзор UAC Операционная система Windows XP не контролировалась должным образом на предмет привилегий, что привело к росту числа вредоносных программ. После Vista и более поздних версий Microsoft внедрила в систему набор средств защиты с разделением привилегий, называемых UAC. Он был разработан, чтобы дать незнакомым или ненадежным программам более низкие привилегии во время выполнения; только определенные службы, встроенные в систему, могут иметь процесс повышения привилегий, чтобы игнорировать защиту UAC. Примечание Исследование автора по обратному проектированию UAC основано на Windows 10 Enterprise LTSC (10.0.17763 N/A Build 17763) только для того, чтобы вы могли понять структуру защиты UAC с точки зрения обратного проектирования. В будущем Microsoft еще может внести существенные структурные изменения или исправления в защиту UAC, и результаты ваших экспериментов на собственных компьютерах могут отличаться от обсуждаемых автором. В Windows вы можете щелкнуть программу правой кнопкой мыши и выбрать «Запуск от имени системного администратора» или использовать Start-Process [path/to/exe] -Verb RunAs в PowerShell, чтобы создать новый процесс в режиме с повышенными правами. Обе эти операции знакомы многим пользователям. Независимо от того, какой из этих методов используется, появится всплывающее предупреждение UAC, как показано на рис. 10.1, с вопросом, авторизован ли пользователь для делегирования привилегий, и отображая сведения о программе, для которой необходимо повысить права, например, издатель, путь к программе, имеет цифровую подпись и т. д., чтобы помочь пользователям решить, следует ли предоставлять эту привилегию процесса или нет с достаточной информацией:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Итак, где в Windows находится служба UAC? На рис. 10.2 показана локальная системная служба привилегий под названием Application Information в панели управления Windows в диспетчере служб, которая сама является службой защиты UAC:
Дважды щелкните по нему, чтобы просмотреть дополнительные сведения, как показано на рис. 10.3. Видно, что имена Appinfo/Application Information на его интерфейсе отвечают за пробуждение высокопривилегированного сервис-менеджера services.exe с помощью команды C:\Windows\system32\svchost.exe -k netsvcs -p -s Appinfo и размещение основного модуля службы UAC C:\Windows\system32\appinfo.dll в качестве отдельного процесса. Подробности см. в поле «Описание» на рис. 10.3:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
«Облегчает запуск интерактивных приложений с дополнительными правами администратора. Если эта служба будет остановлена, пользователи не смогут запускать приложения с дополнительными административными привилегиями, которые им могут потребоваться для выполнения желаемых пользовательских задач». Это означает, что эта служба является основной службой, отвечающей за делегирование привилегий другим программам с низким уровнем привилегий, запрашивающим привилегии, и если эта служба будет закрыта, пользователи не смогут получить привилегии для любых программ с привилегиями UAC. На рис. 10.4 показано дерево процессов в Process Explorer под авторизацией UAC при запуске calc.exe от имени системного администратора:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Видно, что его служба привилегий UAC, svchost.exe (PID 5968), получила наш запрос на повышение привилегий от PowerShell и открыла графический интерфейс авторизации UAC, consent. exe с экраном «Да/Нет», ожидая, пока пользователь примет дальнейшие решения. Примечание В целях записи и запоминания служба привилегий UAC, о которой мы упомянем позже, означает, что любой svchost.exe имеет загруженную в свой процесс AppInfo.dll, интерфейсную программу UAC в качестве consent.exe и дочерний процесс в качестве подпрограммы, которая должна быть привилегированной. В этот момент вас может интересовать следующее: • Служба привилегий UAC открывает окно авторизации, пробуждая интерфейсную программу UAC. Как служба привилегий UAC взаимодействует с программой интерфейса UAC? • Как упоминалось ранее, некоторые службы, встроенные в систему, могут получать привилегированные состояния, не открывая авторизованный пользователем интерфейс UAC. Как осуществляется эта проверка? • Если мы можем понять процесс проверки, есть ли в нем какая-либо логическая ошибка, допускающая злонамеренное использование? Помня об этих трех проблемах, мы теперь обратимся к обратному инжинирингу, чтобы проанализировать, как работает привилегия UAC в Windows 10 Enterprise, и попытаться понять методы обхода UAC, которые использовались в диких атаках. В этом разделе мы кратко рассмотрели, для чего используется служба UAC и как она активируется. В следующем разделе мы рассмотрим внутренний рабочий процесс сервиса. Обратный вызов RAiLaunchAdminProcess В предыдущем разделе мы упомянули очень важный момент: когда кто-либо пытается создать процесс повышения привилегий из программы с низким уровнем привилегий, служба привилегий UAC будет
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
уведомлена и подтвердит, следует ли делегировать привилегии или нет. Если запрос на повышение прав будет удовлетворен, служба привилегий UAC продолжит выводить программу с низкими привилегиями с высокими привилегиями. На этом этапе служба привилегий UAC должна иметь функцию обратного вызова, которая отвечает за получение запросов, их проверку и делегирование привилегий при создании процесса. Эта функция обратного вызова является функцией RAiLaunchAdminProcess, расположенной в appinfo.dll. На рис. 10.5 показан снимок экрана динамического анализа службы привилегий UAC с помощью известного бинарного декомпилятора IDA и динамической отладки его контрольных точек функции обратного вызова RAiLaunchAdminProcess. Теперь мы полностью объясним это с точки зрения сгенерированного IDA псевдокода и динамических экранов отладки:
На рис. 10.6 показано определение функции RAiLaunchAdminProcess в сообщении в блоге ведущей группы Google по исследованию уязвимостей, Project Zero, Calling Local Windows RPC Servers from .NET (googleprojectzero.blogspot.com/2019/12/calling-local-windows-rpc- серверы-из.html):
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Видно, что в функции обратного вызова есть 13 параметров, а ключевые параметры представлены и объяснены ниже: • RPC_ASYNC_STATE: когда запрос RPC на привилегию отправляется в службу привилегий UAC, создается асинхронный канал, и структура RPC_ASYNC_STATE отвечает за запоминание текущего состояния канала, когда он ожидает, запрашивает, отвечает или отменяет. • hBinding: здесь хранится дескриптор текущего канала RPC для предыдущей операции. • ExecutablePath: это путь к программе с низким уровнем привилегий из процесса создания, отправленного пользователем. • CommandLine: параметры команды, полученные пользователем после отправки процесса выполнения. • Create Flags: здесь записывается параметр dw Create Flags из запроса CreateProcessAsUser, который записывает запрос, сгенерированный указанным пользователем дочерним процессом. Например, CREATE_NEW_CONSOLE создает процесс с консольным интерфейсом, CREATE_SUSPENDED создает процесс с приостановленным потоком, DEBUG_PROCESS создает дочерний процесс для динамической отладки и так далее. • CurrentDirectory: это рабочий каталог по умолчанию для определяемых пользователем процессов
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
выполнения. • WindowsStation: указывает, какая рабочая станция должна быть настроена, если программа имеет интерфейс Windows. По умолчанию используется рабочая станция WinSta0, которая может взаимодействовать с пользователем. • StartupInfo: указывает на некоторые требования пользователя к отображению окна процесса выполнения, такие как начальные координаты, размер, максимум и минимум или скрытые экраны и т.д. • ProcessInformation: эта структура используется для обратной отправки информации о родительском процессе и его дочернем процессе после успешного создания процесса с низкими привилегиями. Структура содержит идентификатор процесса/потока и управляющий код процесса/потока (дескриптор). На рис. 10.7 показан ряд соединений RPC после функции RAiLaunchAdminProcess:
После завершения связи RPC вызовите I_RpcBindingInqLocalClientPID(), чтобы получить идентификатор процесса родительского процесса, который инициировал запрос RPC для входящей переменной hBinding. Затем попытайтесь получить доступ к родительскому процессу с помощью NtOpenProcess, чтобы убедиться, что родительский процесс все еще жив, прежде чем переходить к последующим действиям. Если родительский процесс мертв, нет необходимости продолжать процесс аутентификации и создание дочернего процесса. Читатели, которые использовали серию функций Windows API CreateProcess, знают, что у них есть два параметра: первый указывает абсолютный путь к программе, а второй — строковую команду. Оба они могут быть переданы как один или другой. Поэтому на рис. 10.8 показано, как UAC определяет правильный путь к программе по первому и второму параметрам:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Если родительский процесс не передает путь дочернему процессу (т. е. первый параметр пуст), то в качестве цели используется командная строка. Затем используйте API CreateFileW, чтобы запросить дескриптор файла у ядра и сохранить его в переменной exeHandle:
Затем вызывается Windows API для получения времени уведомления, настроенного пользователем в системных настройках Windows, в виде числового значения, как показано на рис. 10.10:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это начинается снизу с 1 (нет уведомлений), уведомляет, когда приложение пытается изменить, но не затемняет рабочий стол (это значение равно 2), уведомляет, когда приложение пытается изменить (это значение равно 3, по умолчанию) и Всегда уведомлять (это значение равно 4, максимальная строгость). В этом разделе мы узнали о функции обратного вызова RAiLaunchAdminProcess в службе привилегий UAC, которая отвечает за получение запросов, их проверку и делегирование разрешений при создании процесса. Используя динамический анализ IDA службы привилегий UAC, мы поняли, как работает весь процесс RAiLaunchAdminProcess. Двухуровневый механизм аутентификации Когда защита UAC была впервые введена в Windows Vista, все запросы на повышение привилегий, инициированные и обработанные RAiLaunchAdminProcess, должны были отображать consent.exe, чтобы указать, повышать или нет, а затем создать дочерний процесс повышения привилегий. Однако этот механизм был слишком раздражающим. В результате защита UAC в Windows 7 и более поздних версиях была разработана с двумя уровнями аутентификации повышения привилегий доверия. Это означает, что существует два уровня аутентификации: если запрос на привилегию передается с обоими уровнями аутентификации, то интерфейс UAC не будет появляться при вызове
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
consent.exe, чтобы спросить, авторизован ли пользователь, и автоматически согласится на запрос на создание процесса повышения. Это означает, что когда вызывается доверенный процесс, consent.exe по-прежнему просыпается, но окно запроса на одобрение пользователя не появляется. В этом разделе мы представим механизм аутентификации на двух отдельных уровнях: Аутентификация А и Аутентификация Б. Примечание Поскольку официального документа от Microsoft, объясняющего, как реализован базовый уровень UAC, не существует, все последующие описания основаны на собственном опыте автора в обратном инжиниринге и основаны на структуре и коде. Если есть какие-либо пробелы или ошибки, пожалуйста, напишите нам. Аутентификация А На рис. 10.11 показан код в начале аутентификации А. Основная задача аутентификации А — убедиться, что путь дочернего процесса исходит из доверенного пути:
В строках 851-859 кода путь дочернего процесса сначала сохраняется в переменной v47 с длиной строки пути, вычисляемой GetLongPathNameW, затем пространство строки wchar_t, соответствующее этой длине, запрашивается LocalAlloc и сохраняется в переменной v49, а путь дочернего процесса сохраняется в пространстве строк, только что запрошенном в переменной v49 при втором вызове GetLongPathNameW. Примечание Этот процесс преобразует уникальный путь спецификации короткого имени файла Microsoft 8.3 обратно в абсолютный путь длинного имени файла. Заинтересованные читатели могут увидеть имя файла d Википедии (en.wikipedia.org/wiki/8.3_filename). Примером короткого имени файла 8.3 является C:\WinAPT\chapt#9\GOOGLE~2.EXE, упомянутое в разделе «Получение подписи путем злоупотребления нормализацией пути» в главе 9; результатом его преобразования в путь с длинным именем файла будет C:\WinAPT\chapt#9\GoogleUpdate.exe\x20.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Затем используйте функцию RtlDosPathNameToRelativeNtPathName_U_WithStatus, чтобы преобразовать абсолютный путь, только что полученный GetLongPathNameW, в типичный путь NT. Например, путь дочернего процесса, введенный как L "C:\a.exe", будет преобразован в L "\?\C:\a.exe". Затем RtlPrefixUnicodeString будет использоваться для сравнения пути NT, который только что был преобразован, с начальным путем с системным путем в белом списке, например, \? \С:\windows\, \? \C:\Program Files\ или \? \C:\Program Files(x86)\, который не находится в каталоге черного списка (который обычно является каталогом для дополнительных системных гаджетов, таких как калькулятор, Windows Edge и т. д.), как показано на рис. 10.12:
Если дочерний процесс имеет абсолютный путь, начинающийся с C:\Windows\, то для параметра trustFlag будет установлено значение 0x2000, что является первым уровнем доверия: значение, которому можно доверять для справки, но еще не полностью доверять. Если путь к программе начинается в каталоге Program Files, вызовите AipCheckSecurePFDirectory, чтобы проверить, находится ли каталог в Защитнике Windows, журнале, медиаплеере или многоточечном сервере. Если это так, установите для параметра trustFlag значение 0x2000 | 0x4000, который относится к внешней службе приложений, являющейся частью Windows (выборочная установка в папке C:\Program Files), как показано на рис. 10.13:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 10.14 показан код, следующий за рис. 10.13:
Затем мы должны убедиться, что путь дочернего процесса начинается с C:\Windows и что его каталог является одним из следующих: • C:\Windows\System32 • C:\Windows\SysWOW64 • C:\Windows\ehome • C:\Windows\Адам • C:\Windows\ImmersiveControlPanel Если путь дочернего процесса начинается с одного из них, это означает, что текущая программа
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
дочернего процесса возникла из наиболее конфиденциального и собственного системно-привилегированного пути службы, поэтому установите для параметра trustFlag значение 0x6000. Затем убедитесь, что дочерний процесс начинается с C:\Windows\System32\, тогда, если это будет \? \C:\Windows\System32\Sysprep\sysprep.exe или \? \C:\Windows\System32\inetsrv\InetMgr.exe, он должен иметь более высокие привилегии, как показано на рис. 10.15:
Функция AipMatchesOriginalFileName используется для сопоставления программы с памятью и проверки того, что скомпилированное имя файла, записанное в файле version.txt (см. рис. 10.16 для получения дополнительной информации) в файле ресурсов PE, соответствует имени файла текущего дочернего процесса. Это позволяет избежать перехвата замены файла, проверяя, что имя файла при компиляции совпадает с именем файла при выполнении:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Если предыдущая проверка пройдена, дополнительная настройка trustFlag 0x400000 или 0x800000 будет предоставлена операцией побитового ИЛИ, что является важным маркером для прохождения второго уровня проверки. Однако C:\Windows\System32 и C:\Windows\SysWow64 являются системно-зависимыми и критически важными каталогами, а sysprep.exe и InetMgr.exe, упомянутые ранее, — не единственные две системные программы, которым нужны привилегии. Есть еще много системных программ, которые должны иметь привилегии в этих двух родных системных каталогах. Как показано на рис. 10.17, затем проверяется, находится ли путь дочернего процесса в одном из этих двух каталогов; если это так, он вычисляется с помощью ИЛИ до 0x200000, что является последним важным флагом, который может быть проверен вторым уровнем автоматического повышения:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это полный процесс аутентификации для Аутентификации А. Основная часть сопоставляется с путем для проверки достоверности, и результат записывается в trustFlag для записи. Аутентификация Б Как показано на рис. 10.18, следующим шагом является вход в функцию AiIsEXESafeToAutoApprove, которая является ключевой проверкой общей привилегии автоматического утверждения UAC (без всплывающего окна авторизации):
Как показано на рис. 10.19, первой задачей при входе в функцию AiIsEXESafeToAutoApprove является проверка того, что дочерний процесс для текущего запроса на повышение привилегий прошел вышеупомянутую аутентификацию A для проверки пути. Если trustFlag не больше 0x200000 (т. е. условие bt eax, 15h не выполняется), последующие проверки отбрасываются и функция пропускается:
Затем, как показано на рис. 10.20, возьмите путь дочернего процесса и сохраните имя текущей программы в переменной clrExeName с помощью wcsrchr. Продолжайте отображать статическое содержимое программы дочернего процесса из файла на диске в переменную exeRawData, используя
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
код управления файлом, полученный из CreateFile, как описано ранее (т. е. переменная exeHandle на рис. 10.8), дополненный MapViewOfFile:
На рис. 10.21 показано, установлено ли для ключа autoElevate значение true в файле manifest.xml статического содержимого дочернего процесса, чтобы подтвердить, что сама программа хочет получить привилегию автоматического повышения прав. Если это так, он продолжит проверку; в противном случае он оставит последующую аутентификацию:
Если дочерний процесс не имеет запроса на автоматическое повышение прав в информационном списке содержимого, но извлеченное ранее имя файла clrExeName является одним из 10 элементов в белом списке, то он также считается программой, требующей автоматического повышения привилегий, как показано на рисунке 10.22:
AipIsValidAutoApprovalEXE, показанный на рис. 10.23, затем будет использоваться для проверки того, что программа имеет цифровую подпись Microsoft и что подпись по-прежнему действительна, прежде чем она будет полностью аутентифицирована:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Функция WTGetSignatureInfo используется для проверки правильности цифровой подписи дочернего процесса, и аналогичным образом функция AipMatchesOriginalFileName используется для проверки того, что имя текущего дочернего файла не было изменено, как на этапе компиляции. Если оба этих теста пройдены, файл программы проверяется как доверенный файл. Программа интерфейса UAC, ConsentUI Следующее, что нужно сделать, это вызвать AiLaunchConsentUI, чтобы попытаться вызвать всплывающее окно consent.exe, чтобы спросить пользователя, согласны ли они с этим запросом на повышение привилегий дочернего процесса. Примечание Прохождение или сбой предыдущей аутентификации A и B не влияет на то, будет ли вызываться функция AiLaunchConsentUI или нет. Аутентификация A и B обновит проверенный результат в trustFlag и передаст trustFlag в consent.exe, когда вызывается AiLaunchConsentUI, чтобы разбудить интерфейсную программу UAC consent.exe и сообщить ей статус проверки подлинности A и B. На рис. 10.24 и рис. 10.25 показаны части кода AiLaunchConsentUI:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Он просыпается в приостановленном состоянии, а затем использует функцию AipVerifyConsent для подтверждения что consent.exe не был взломан (см. рис. 10.25), а затем использует ResumeThread для пробуждения процесса интерфейсной программы UAC consent.exe и ожидает завершения процесса и возврата. Причина выхода хранится в переменной ExitCode:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 10.26 показан код ключа функции проверки AipVerifyConsent для программы интерфейса UAC. Видно, что он использует NtReadVirtualMemory для извлечения содержимого временно приостановленной программы consent.exe. Также проверяется, есть ли у процесса поле consent и что это поле помечено Microsoft Windows (c) 2009 Microsoft Corporation. Если да, то он аутентифицирован и текущая программа интерфейса UAC не взломана и ей можно доверять:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 10.27 показано окно авторизации после того, как ResumeThread возобновил работу интерфейсной программы UAC consent.exe, спросив пользователя, хотят ли они авторизовать это повышение привилегий. Если пользователь нажмет Да, ExitCode процесса вернет значение 0 для этой авторизации. И наоборот, если пользователь нажмет «Нет» или закроет окно, ExitCode процесса вернет 0x4C7:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Код, следующий за рис. 10.25 (т. е. конец функции AiLaunchConsentUI), показан на рис. 10.28. Если ExitCode consent.exe просто 0x102 или 0x42B, то функция AiLaunchConsentUI вернет значение 0x4C7. Если ExitCode не является двумя предыдущими значениями, AiLaunchConsentUI вернет ExitCode в качестве возвращаемого значения. При повторении отладочных тестов функция AiLaunchConsentUI на практике должна возвращать только два возможных значения; если он возвращает значение 0, пользователь соглашается на авторизацию, а если возвращает значение 0x4C7, в повышении привилегий отказано:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Примечание Вы можете быть обеспокоены: результат двухуровневой аутентификации, похоже, не работает. На самом деле, если оба уровня аутентификации A и B пройдены, параметры будут переданы в файл consent.exe. Она проснется без всплывающего окна авторизации, чтобы побеспокоить пользователя, и просто установит для ExitCode значение 0 и закроет программу. На рис. 10.29 показано, что после передачи ExitCode значения 0 служба привилегий UAC может подтвердить, что запрос на повышение привилегий был удовлетворен, и передаст путь дочернего процесса в функцию AiLaunchProcess:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
На рис. 10.30 показано, что функция AiLaunchProcess внутренне вызывает функцию CreateProcessAsUserW для создания пути дочернего процесса в качестве привилегированной службы, после чего дочерний процесс будет выполняться как привилегированный процесс с повышенными правами:
В этом разделе мы узнали о разработке двухуровневой аутентификации привилегий доверия в защите UAC, то есть аутентификации A, аутентификации B и программе интерфейса UAC. Если для запроса привилегий одобрены оба уровня аутентификации, то программа интерфейса UAC не будет всплывать, чтобы спросить, авторизован ли пользователь, и автоматически согласится на запрос повышения привилегий при вызове consent. exe.
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Условия повышенных привилегий Мы обобщаем предыдущие результаты обратной разработки для Windows 10 LTSC (10.0.17763 Н/Д, сборка 17763) и можем вывести следующие условия для автоматического повышения привилегий для дизайна UAC: • Программа должна настроить себя как Auto Elevation • Программа должна иметь действующую цифровую подпись • Программа запускается из доверенного системного каталога На самом деле, вы скоро поймете, что в системе есть много служб и инструментов, которым при пробуждении предоставляются прямые привилегии, чтобы пользователи могли беспрепятственно использовать их без необходимости соглашаться на частую авторизацию. Итак, если мы сможем захватить эти привилегированные процессы, не сможем ли мы также повысить уровень нашего вредоносного ПО? Вот несколько распространенных примеров: • Пути к модулям DLL или команды, используемые системной программой с высоким уровнем привилегий, неправильно хранятся в реестре, файлах *.xml или *.ini на диске. • Привилегированная служба экспортирует общедоступный COM-интерфейс, чтобы любой мог вызывать ее (без тщательной проверки благонадежности вызывающего абонента), и этот интерфейс потенциально может быть использован злоумышленниками. • Процесс проверки службы привилегий UAC недостаточно надежен, чтобы допускать прямые атаки на сам процесс проверки подлинности доверия UAC. Поэтому в этом разделе мы представим наши выводы, основанные на обратном проектировании, чтобы понять различные потоки эксплуатации UAC, которые использовались киберсилами и хакерами в массовых атаках в диких условиях. Неправильная конфигурация реестра, вызванная перехватом привилегий В качестве примера возьмем запись в блоге (@enigma0x3) исследователя информационной безопасности Spectre Ops Мэтта Нельсона «Обход UAC с помощью путей к приложениям» (enigma0x3.net/2017/03/14/bypassing-uac-using-app-paths). На рис. 10.31 показаны журналы, записанные Process Monitor при запуске инструмента восстановления системы sdclt.exe в Windows 10. Видно, что привилегированный системный инструмент sdclt.exe просыпается и пытается вслепую искать в реестре, в конце концов читая HKCU: \Software\Microsoft\ Windows\CurrentVersion\App Paths\control.exe значение ключа реестра с низкими привилегиями с "C:\Windows\System32\control.exe" Windows\System32\control. exe" /name Microsoft.BackupAndRestoreCenter:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Затем панель управления системой (control.exe) активируется с повышением привилегий и переключается на экран конфигурации восстановления системы для просмотра пользователем, как показано на рис. 10.32:
Поскольку его раздел реестра HKCU (HKEY_CURRENT_USER) является записью реестра, в которую может записывать любая программа с низким уровнем привилегий, мы меняем его команду на C:\Windows\System32\cmd.exe, как показано на рис. 10.33:
После подделки низкопривилегированных ключей реестра просто перезапустите sdclt.exe и убедитесь, что он пробуждает C:\Windows\System32\cmd.exe с повышенными привилегиями, давая нам привилегированную команду cmd.exe, как показано на рис. 10.34:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Исследователь Мэтт Нельсон первым применил этот тип неправильной настройки, ведущий к злонамеренному использованию для извлечения UAC и обхода обнаружения белого списка. Другие связанные примеры можно найти в его блоге Userland Persistence with Scheduled Tasks and COM Handler Hijacking (enigma0x3.net/2016/05/25/userland-persistence-with-cheduled-tasks-and-com-handlerhijacking) или Bypassing UAC в Windows 10 с помощью очистки диска (enigma0x3.net/2016/07/22/bypassing-uac-on-windows-10-using-disk-cleanup/). Эти статьи вызвали большой интерес к поиску подобных проблем с повышением привилегий UAC. В этом разделе мы узнали о требованиях к доверительной аутентификации UAC и продемонстрировали, как добиться извлечения UAC и обойти обнаружение белого списка с помощью навыков, описанных в статье Мэтта Нельсона «Обход UAC с помощью путей к приложениям». Примеры обхода UAC В разделе примеров боковой загрузки DLL в главе 5 мы кратко представили технику боковой загрузки DLL, которая позволяет нам перехватить процесс выполнения, просто поместив модуль DLL в тот же каталог, что и программа. Вы, должно быть, догадались, если бы мы могли найти уязвимую системную программу с высокими привилегиями, которая могла бы поместить вредоносный модуль DLL в тот же каталог, разве это не позволило бы программе с повышенными привилегиями автоматически монтировать наш DLL-файл, позволяя нам действовать злонамеренно как процесс с повышенными привилегиями? На практике это не так просто. Как упоминалось ранее, в основном системные программы, которые могут быть автоматически наделены привилегиями в полном процессе аутентификации UAC, должны располагаться в C:\Windows\System32 или C:\Windows\SysWOW64. Эти два системных каталога — это каталоги, в которые нельзя записывать файлы без привилегий. Однако, если у нас нет права записи с высоким уровнем привилегий, возможно ли заимствование из сервиса с высоким уровнем привилегий? Ответ: да, шанс есть. На рис. 10.35 показаны два разных окна авторизации UAC в системах Windows 7. Окно авторизации слева — это экран, отображаемый, когда стандартная служба привилегий UAC пробуждает consent.exe после щелчка правой кнопкой мыши по программе с правами администратора; справа — экран авторизации UAC, который появляется, когда файл вручную перетаскивается в привилегированный каталог System32 в проводнике:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Вы должны быстро заметить, что экран авторизации UAC справа на самом деле является всплывающим предупреждением от программы explorer.exe с низким уровнем привилегий, которая определяет, требуется ли авторизация. По сути, File Explorer с низким уровнем привилегий имеет возможность записи в любой привилегированный каталог без прохождения процесса проверки подлинности доверия привилегий UAC. См. статью WikiLeaks под названием Vault 7: CIA Hacking Tools Revealed (wikileaks.org/ciav7p1/), которая включает описание уязвимости в обходе UAC с повышенными правами COM-объекта (wikileaks.org/ciav7p1/cms/page_3375231.html). Эта уязвимость злонамеренно используется для привилегий UAC и описывается следующим образом: Windows 7 включает функцию, которая позволяет утвержденным приложениям, работающим с правами администратора, выполнять системные операции без запроса UAC. Один из методов, который приложение может использовать для этого, — создать COM-объект с повышенными правами и использовать его для выполнения операции. Например, DLL, загруженная в explorer.exe, может создать объект IFileOperation с повышенными правами и использовать его для удаления файла из каталога Windows. Этот метод можно комбинировать с технологической инъекцией для создания более прямого обхода UAC. В описании указано, что любой привилегированный процесс или файловый менеджер может использовать COM-интерфейс IFileOperation для чтения, записи, перемещения и удаления привилегированных файлов в качестве администратора. Это именно то, что мы пытаемся сделать, перехватив привилегированный сервис повышения прав с боковой загрузкой DLL! На рис. 10.36 показан приведенный в статье пример кода, имитирующий операцию удаления файла, выполняемую программой explorer.exe. Все, что требуется, — разработать вредоносную DLL для внедрения в проводник с низким уровнем привилегий и вызвать функцию ElevatedDelete в качестве проводника для вызова COM-интерфейса IFileOperation для удаления файла C:\Windows\test.dll от имени администратора:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
COM-объект с повышенными правами (IFileOperation) Давайте поэкспериментируем с повышением привилегий UAC в Windows 7, используя слабости, упомянутые ранее. Следующий пример — это проект iFileOperWrite, который общедоступен в проекте GitHub в папке Chapter#10. В целях экономии места в этой книге извлекается только выделенный код; пожалуйста, обратитесь к полному исходному коду, чтобы прочитать полный проект. Во-первых, путь к текущей программе в блоке Process Environment Block (PEB) создается как проводник. exe в функции ввода в проекте iFileOperWrite, чтобы обмануть COM-интерфейс IFileOperation, чтобы мы могли работать от имени администратора для выполнения операций с файлами, как показано на Рис. 10.37:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Примечание Этот проект представляет собой 32-битную среду, поэтому строка 110 кода извлекает блок 32-битной среды из fs:[0x30]. Для 64-битной среды рекомендуется исправить это, чтобы извлечь блок 64-битной среды из gs:[0x60]. Затем COM-интерфейс IFileOperation можно использовать для перемещения файла в целевой каталог с помощью функции CopyItem в компоненте IFileOperation, как показано на рис. 10.38:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Проект iFileOperWrite компилируется и генерирует iFileOperWrite.exe, который считывает два параметра: вредоносный DLL-файл, который нужно доставить, и каталог, в который записывается цель. Как показано на рис. 10.39, файл ntwdblib.dll отсутствует в исходном каталоге C:\Windows\System32, подтвержденном командой where; тем не менее, iFileOperWrite.exe может злонамеренно поместить наш злонамеренно созданный модуль hijacking DLL ntwdblib.dll в каталог с высоким уровнем привилегий C:\Windows\System32:
Как показано на рис. 10.40, мы затем вызываем C:\Windows\System32\cliconfig.exe. Поскольку наш вредоносный файл ntwdblib.dll теперь находится в том же каталоге, он автоматически смонтирует наш
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
вредоносный модуль ntwdblib.dll при пробуждении системной программы повышения привилегий cliconfig.exe. Затем наш вредоносный модуль может вызвать высокопривилегированную команду cmd.exe с cliconfig.exe, чтобы позволить хакерам выполнять вредоносные действия:
После обнаружения этого эксплойта UAC в Windows 7 хакеры и киберсилы провели множество полевых операций в Windows 7 и 8, и многие из этих методов боковой загрузки DLL, основанные на IFileOperation, использовали вредоносные DLL в качестве бэкдор-загрузчиков для перехвата системных служб с высоким уровнем привилегий, достигая тем самым тройного эффекта сохраняемости бэкдора, скрытности и эксплойта. Напротив, после раскрытия этой атаки подход Microsoft к защите UAC в Windows 7 и 8 заключался в том, чтобы исправить системные программы с высоким уровнем привилегий, которые были уязвимы для боковой загрузки DLL, что позволило уменьшить количество уязвимых системных программ, но задержка исправления уязвимости COM-интерфейса IFileOperation. В результате во время Windows 7 и 8 на форумах было много заявлений о том, что новый эксплойт UAC основан на слабости IFileOperation и что эксплойт основан на поиске других программ, которые можно взломать. Только в Windows 10 1607 проводник официально удалил произвольные привилегии на запись файлов, что сделало этот метод действительно удаленным из дикой природы. Тем не менее, это был довольно стабильный и популярный метод в более ранних версиях Windows 10. Делает ли это изменение в Windows 10 UAC еще более неразрушимым? Нет, IFileOperation — не единственный COM-интерфейс со злоупотреблением повышением привилегий, и существует еще много COM-интерфейсов с повышением привилегий, которые стоит изучить на предмет злонамеренного использования. Выполнение произвольного повышения привилегий CMSTP Оддвар Мое (@Oddvarmoe), норвежский исследователь, опубликовал сообщение в блоге под названием Исследование CMSTP.exe (msitpros.com/?p=3960), указав, что установщик профиля диспетчера подключений, cmstp.exe, который существует с Windows XP вызывает COM-интерфейс для выполнения текстовой командной строки во время установки профиля подключения, и, пока он может вызывать этот интерфейс, он имеет привилегию запускать функцию ShellExecute. В следующем примере показана папка Chapter#10 выделенного кода; пожалуйста, обратитесь к
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
исходному коду masqueradePEB_CMSTP_UACBypass.cpp в проекте GitHub. В целях экономии места в этой книге извлекается только неполный исходный код. На рис. 10.41 показан процесс, аналогичный предыдущему проекту, где сама привилегированная программа маскируется под доверенную программу, explorer.exe, а затем вызывает COM-интерфейс CMSTP:
На рис. 10.42 показан код клавиши для вызова COM-интерфейса CMSTP:
В строках 141-146 своего кода: COM-интерфейс имеет функцию ShellExec в компоненте ICMLuaUtil, которая может вводить cmd.exe /k "echo exploit done. > C:\Windows\System32\misc && type misc. Когда эта функция запускается с доверенной системной программой (например, File Explorer), функция
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
ShellExecute может быть запущена с привилегированной системной службой и выполнять наши команды:
На рис. 10.43 показан результат компиляции и запуска masqueradePEB_CMSTP_UACBypass. cpp на Windows 10 Enterprise LTSC 17763. Видно, что он успешно сделал следующее: замаскировался под оболочку File Explorer и вызвал COM-интерфейс CMSTP, разбудил cmd.exe в привилегированном повышенном состоянии, записал строку в misc в каталоге с высоким уровнем привилегий C:\Windows\System32 и распечатал его. Это показывает, что мы успешно подняли cmd.exe для последующего злонамеренного использования. Достижение повышенных привилегий за счет коллизий доверенных путей Мы упомянули так много способов атаковать привилегированную системную программу с доверием UAC. Далее мы рассмотрим прямую атаку на процесс аутентификации UAC. Дэвид Уэллс (@CE2Wells), исследователь уязвимостей нулевого дня в компании Tenable Security, опубликовал техническую статью «Обход UAC путем имитации доверенных каталогов» (medium.com/tenable-techblog/uac-bypass-by-mocking-trusted-directories-24a96675f6e), который определяет проблему в Windows 10 Build 17134, когда служба UAC не учитывает нормализацию пути Windows NT в процессе проверки подлинности на основе доверия, что приводит к произвольным привилегиям. Основываясь на этом исследовании, мы полностью реконструировали защиту UAC для Windows 10 Enterprise 17763 и представили ее на конференции HITCON 2019 в презентации под названием «Атака дублирующихся путей: получение повышенных привилегий из поддельных удостоверений», в которой рассказывается о полном обратном инжиниринге процесс аутентификации и повторно вводит этот метод атаки. После обратного проектирования всего процесса аутентификации доверия UAC мы знаем, что для автоматического повышения привилегий без появления экрана авторизации пользователя должны быть выполнены следующие условия:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
• Программа должна быть настроена как Auto Elevation • Программа должна иметь действующую цифровую подпись • Программа должна запускаться из доверенного системного каталога. Первые два легко удовлетворить, найдя действительную цифровую подпись в текущей системе Windows, пометив себя как системную программу для автоматического повышения привилегий и перехватив процесс ее выполнения с боковой загрузкой DLL. Мы объясним, как достичь первых двух с помощью следующих действий. На рис. 10.44 показан встроенный в Windows инструмент шифрования диска BitLockerWizardElev.exe, чей список манифеста помечает себя как requireAdministrator, а также помечает autoElevate как true для автоматического повышения привилегий:
На рис. 10.45 показан анализ инструмента шифрования диска с помощью PE-bear. Видно, что таблица импорта показывает, что необходимо импортировать две функции, FveuiWizard и FveuipClearFveWizOnStartup, в системный модуль FVEWIZ.dll. Поэтому вредоносный модуль DLL написан для экспорта этих двух функций и пробуждения cmd.exe с всплывающим окном MessageBoxA, когда процесс выполнения успешно перехвачен:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Как удовлетворить третье условие доверия? Запустите программу повышения привилегий из доверенного системного каталога (например, System32 или SysWOW64). См. общедоступный документ Microsoft, DACL и ACE (docs.microsoft.com/enus/windows/win32/secauthz/dacls-and-aces), в котором приводится следующее описание списка управления доступом на уровне пользователей (DACL). Если объект Windows не имеет списка DACL, система разрешает всем полный доступ к нему. Если у объекта есть DACL, система разрешает только тот доступ, который явно разрешен записями управления доступом (ACE) в DACL. Если в DACL нет ACE, система никому не разрешает доступ. Аналогичным образом, если в DACL есть элементы управления доступом, которые разрешают доступ ограниченному набору пользователей или групп, система неявно отказывает в доступе всем доверенным лицам, не включенным в элементы управления доступом. Это означает, что причина, по которой C:\Windows\System32 и C:\Windows\SysWOW64 не могут быть записаны или созданы папки, заключается в том, что системный каталог настроен с помощью DACL, и только процессам с повышенными привилегиями разрешено записывать или создавать папки в системном каталоге. А как же C:\? Это подводит нас к интересной особенности Windows. На рис. 10.46 мы видим, что команда cmd.exe с низким уровнем привилегий не может создавать или записывать какие-либо файлы в каталоге C:\, но может создавать новые папки. Итак, что это значит? Вернемся назад и посмотрим, как проверяется Аутентификация А в защите UAC:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Когда какой-либо родительский процесс инициирует повышение привилегий программы на пути дочернего процесса с правами администратора, служба привилегий UAC сначала извлекает длинный путь NT из пути дочернего процесса с помощью GetLongPathNameW (преобразуя путь спецификации короткого имени файла 8.3 в длинный путь). Затем этот длинный путь сравнивается с RtlPrefixUnicodeString, чтобы определить, начинается ли путь с C:\Windows\System32 или C:\Windows\SysWOW64, а затем передается для проверки подлинности A. Внутренняя реализация вызова GetLongPathNameW для извлечения длинного пути NT приведет к нормализации пути Windows, в результате чего служба привилегий UAC сопоставит путь в Authentication A с длинным путем, который был нормализован. Это позволяет атаковать процесс аутентификации, как показано на рис. 10.47:
После прохождения обоих процессов аутентификации A и B, пробуждения consent. exe без какого-либо всплывающего окна и согласия с запросом на повышение привилегий затем заштриховывается дочерний процесс с повышением привилегий. На рис. 10.48 видно, что CreateProcessAsUserW используется для создания дочернего процесса, но не из длинного пути сразу после аутентификации; вместо этого процесс создается из исходного пути дочернего процесса, заданного родительским процессом:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
Это позволяет нам использовать злонамеренные ситуации, когда путь при аутентификации не соответствует пути, созданному как новый процесс из-за нормализации пути Windows. Затем мы можем запустить mkdir \??\C:\Windows \ и mkdir \??\C:\Windows \System32\ с процессом с низким уровнем привилегий. Префикс \\?\ обходит нормализацию пути Windows и создает папку C:\Windows\ с низким уровнем привилегий с пробелом в конце и папкой System32 внутри. Затем программа Pikachu Volleyball копируется в эту папку и запускается, показывая, что игровая программа в настоящее время работает правильно, как показано на рис. 10.49:
На рис. 10.50 мы используем Process Explorer для наблюдения за процессом с именем changepk.exe (который на самом деле выполняет внутреннюю программу Pikachu Volleyball) и проверяем его цифровую подпись. Мы можем обнаружить, что при проверке цифровой подписи запущенная в данный момент программа Pikachu Volleyball находится в папке C:\Windows\system32\changepk.exe (с
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
пробелом), которая ошибочно идентифицируется как C:\Windows\system32\changepk.exe ( без пробела), и, таким образом, проверяется наличие цифровой подписи:
Что, если вместо этого мы поместим BitLockerWizardElev.exe, который может быть взломан упомянутой ранее DLL? Скидываем BitLockerWizardElev.exe с вредоносной DLL в низкопривилегированную папку C:\Windows\System32\ и запускаем BitLockerWizardElev.exe, как показано на рис. 10.51:
Поскольку GetLongPathNameW удаляет пробелы из пути во время процесса нормализации пути Windows, BitLockerWizardElev.exe (который мы злонамеренно поместили в папку с низким уровнем привилегий C:\Windows\System32\) может пройти проверку подлинности службы привилегий UAC A и имеет действительный Microsoft цифровая подпись. Поэтому при запуске не появляется сообщение об авторизации UAC, а повышение привилегий предоставляется напрямую. Кроме того, процесс выполнения перехватывается, потому что вредоносная DLL была помещена в тот же каталог, то есть cmd.exe создается как повышение привилегий, и в MessageBoxA появляется сообщение об успешном перехвате:
Мануал/Книга - APT война с Виндой | XSS.is (ex DaMaGeLaB)
См. Рисунок 10.52; этот метод эксплойта можно использовать и в Windows 7. Эта уязвимость возникает не только в Windows 10. Поскольку проверка основного пути защиты UAC после Windows 7 выполняется с помощью GetLongPathNameW для нормализации пути Windows, одну и ту же уязвимость можно использовать на всем пути от Windows 7 до последней версии Windows Enterprise. 10, который показывает, насколько он мощный. Примечание Если вас интересуют атаки с повышением привилегий UAC, рекомендуется подписаться на проект с открытым исходным кодом hfiref0x/UACME: Defeating Windows User Account Control (github.com/hfiref0x/UACME), который представляет собой удобный для хакеров инструмент повышения привилегий UAC. Проект содержит исчерпывающий список известных методов эксплойтов и исходный код атак в дикой природе для исследования. Краткое содержание Защита UAC играет важную роль в современной Windows. Многие меры безопасности Windows основаны на защите UAC в качестве периметра безопасности для правильной работы — например, защита брандмауэра Windows. В этой главе мы представили полный анализ защиты UAC в Windows 10, процесса аутентификации и нескольких известных атак методом обратного проектирования. Однако, поскольку битва за безопасность Windows продолжается, вполне возможно, что эти проблемы будут устранены в будущем и появятся другие новые сценарии атак.
yashechka Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Quake3 закончен ещё один проект. Переведена книга - Windows APT Warfare. Уникальное чтиво.
Файл PDF создан при помощи программы Adobe Acrobat Pro DC пользователем ESergey