Нельзя копировать код с помощью memcpy, всё намного сложнее

от автора

В своё время один из клиентов сообщил нам, что на Itanium его программа завершалась аварийно.

Постойте, не закрывайте статью!

На Itanium клиент выявил проблему, но она свойственна и всем остальным архитектурам, так что продолжайте чтение.

Код выглядел примерно так:

struct REMOTE_THREAD_INFO {     int data1;     int data2;     int data3; };  static DWORD CALLBACK RemoteThreadProc(REMOTE_THREAD_INFO* info) {     try {         ... use the info to do something ...     } catch (...) {         ... ignore all exceptions ...     }     return 0; } static void EndOfRemoteThreadProc() { }  // Error checking elided for expository purposes void DoSomethingCrazy() {     // Calculate the number of code bytes.     SIZE_T functionSize = (BYTE*)EndOfRemoteThreadProc - (BYTE*)RemoteThreadProc;      // Allocate memory in the remote process     SIZE_T allocSize = sizeof(REMOTE_THREAD_INFO) + functionSize;     REMOTE_THREAD_INFO* buffer = (REMOTE_THREAD_INFO*)       VirtualAllocEx(targetProcess, NULL, allocSize, MEM_COMMIT,         PAGE_EXECUTE_READWRITE);      // Write data to the remote process     REMOTE_THREAD_INFO localInfo = { ... };     WriteProcessMemory(targetProcess, buffer,                        &localInfo, sizeof(localInfo));      // Write code to the remote process     WriteProcessMemory(targetProcess, buffer + 1,                        (void*)RemoteThreadProc, functionSize);      // Execute it!     CreateRemoteThread(targetProcess, NULL, 0,                        (LPTHREAD_START_ROUTINE)(buffer + 1),                        buffer); }

Этот код настолько плох, что я специально добавил в него ошибки, чтобы он даже не компилировался.

Смысл заключался в том, что клиент хотел внедрить некий код в целевой процесс, поэтому использовал Virtual­Alloc для выделения памяти под этот процесс. Первая часть блока данных содержала какие-то данные, которые нужно было передать. Вторая часть блока данных содержала байты кода, которые нужно было исполнить, и клиент запускал эти байты кода при помощи Create­Remote­Thread.

Скажу прямо: сама идея, на которой построен этот код, фундаментально неверна.

Клиент сообщил, что этот код «отлично работал на 32-битных x86 и 64-битных x86», но не работает на Itanium.

На самом деле, я удивлён, что он работал даже на x86!

Структура программы подразумевает, что весь код в RemoteThreadProc не зависит от позиции. Требование независимости сгенерированного кода от позиции отсутствует. Например, один из вариантов генерации кода для операторов switch заключается в использовании таблицы переходов, и эта таблица состоит из абсолютных адресов x86.

На самом деле, очевидно, что код не является независимым от позиции, потому что в нём используется обработка исключений C++, а в реализации обработки исключений компилятора Microsoft используется таблица, сопоставляющая точки исполнения с операторами catch, чтобы было понятно, какой оператор catch использовать. И если бы использовался catch с фильтрацией, то существовали бы дополнительные таблицы для определения того, применяется ли фильтр catch к выданному исключению.

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

Но мы знаем, что ссылки на содержимое за пределами тела функции будут присутствовать, потому что блок C++ try/catch вызывает функции в библиотеке C runtime support library.

И x86-64, и Itanium используют для обработки исключений коды раскрутки (unwind codes), а в целевом процессе отсутствуют попытки регистрации этих кодов.

Предполагаю, что клиенту повезло, и исключений не выдавалось, или, по крайней мере, они выдавались достаточно редко, чтобы это осталось незамеченным при тестировании.

Кроме того, нет гарантий того, что EndOfRemoteThreadProc будет размещена в памяти непосредственно после RemoteThreadProc. На самом деле, нет даже гарантий того, что EndOfRemoteThreadProc будет иметь отдельную сущность. Компоновщик может выполнить свёртывание COMDAT, при котором несколько идентичных функций соединяются в одну. Даже если отключить свёртывание COMDAT, то Profile-Guided Optimization переместит функции по отдельности и маловероятно, что они окажутся в одном месте.

На самом деле, не существует даже требования, чтобы байты кода функции RemoteThreadProc вообще были смежными! Profile-Guided Optimization изменяет порядок базовых блоков и код одной функции может оказаться разбросанным по разным частям программы (это зависит от паттернов использования).

И даже без Profile-Guided Optimization оптимизация этапа компиляции может встроить часть функции или функцию целиком, поэтому одна функция может иметь множество копий в памяти, каждая из которых была оптимизирована под свою конкретную точку вызова.

Также существуют особые правила для Itanium, гарантировано обеспечивающие аварийное завершение на Itanium.

У процессоров Itanium все команды должны быть выровнены по 16-байтным границам, но приведённый выше код не соответствует этому требованию. Кроме того, на Itanium указатели функций указывают не на первый байт кода, а на структуру дескриптора, содержащую пару указателей: один на gp функции, второй на первый байт кода. (Тот же паттерн используется в PowerPC.)

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

Теперь я уже был напуган.

Более безопасным1 способом инъектирования кода в процесс была бы загрузка кода в качестве библиотеки при помощи Load­Library. Она бы вызвала загрузчик, который бы проделал всю работу по реализации необходимых исправлений, правильно бы распределил память с корректным выравниванием, регистрацией защиты потока управления и таблиц раскрутки исключений, загрузил бы зависимые библиотеки и в целом правильно подготовил среду выполнения для запуска нужного кода.

С тех пор от этого клиента не поступало никаких известий.

1 Я не сказал, что это безопасный способ инъектирования кода. Он всего лишь более безопасный.


ссылка на оригинал статьи https://habr.com/ru/post/598781/