[Перевод] Нельзя копировать код с помощью 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 Я не сказал, что это безопасный способ инъектирования кода. Он всего лишь более безопасный.

Читайте так же:

  • От Lego до 3d-принтеров: как стартап хочет печатать ракеты на МарсеОт Lego до 3d-принтеров: как стартап хочет печатать ракеты на Марсе Relativity Space, стартап из YCombinator, делает ракеты полностью напечатанные на 3d-принтере. У «Шаттла» было 2,5 млн деталей, у SpaceX и Blue Origin — 100 000 на ракету. У Relativity Space тысяча деталей, меньше, чем в автомобиле. От 80 до 90% стоимости ракеты уходит на оплату труда […]
  • Видеоигры становятся важным источником данных о пользователяхВидеоигры становятся важным источником данных о пользователях Видеоигры становятся важным источником данных о пользователях. Причем информация, которую можно получить оттуда, часто является более личной и более обширной в сравнении с другими источниками в интернете. Этим все активнее пользуются компании, причем не только игровые — для некоторых […]
  • Про Ту-144 — легендарный советский самолётПро Ту-144 — легендарный советский самолёт У Андрея Николаевича Туполева была мечта: построить сверхзвуковой пассажирский лайнер. Первый полёт этот самолёт совершил 31 декабря 1968 года — и это был первый в мире пассажирский сверхзвук. Чтобы создать такой самолёт, пришлось решить очень много задач. Иллюминаторы вдавливало внутрь, […]
  • Одноклассники запустили реферальную программу в маркетплейсе товаровОдноклассники запустили реферальную программу в маркетплейсе товаров Социальная сеть Одноклассники запустила первую реферальную программу в совместном маркетплейсе товаров с AliExpress Россия. Пользователи смогут получать скидки за приглашение друзей на витрину маркетплейса и совершать выгодные покупки в сервисе «Товары» в Одноклассниках. Запуск станет […]