В своё время один из клиентов сообщил нам, что на 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);
}
Этот код настолько плох, что я специально добавил в него ошибки, чтобы он даже не компилировался.
Смысл заключался в том, что клиент хотел внедрить некий код в целевой процесс, поэтому использовал VirtualAlloc
для выделения памяти под этот процесс. Первая часть блока данных содержала какие-то данные, которые нужно было передать. Вторая часть блока данных содержала байты кода, которые нужно было исполнить, и клиент запускал эти байты кода при помощи CreateRemoteThread
.
Скажу прямо: сама идея, на которой построен этот код, фундаментально неверна.
Клиент сообщил, что этот код «отлично работал на 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 способом инъектирования кода в процесс была бы загрузка кода в качестве библиотеки при помощи LoadLibrary
. Она бы вызвала загрузчик, который бы проделал всю работу по реализации необходимых исправлений, правильно бы распределил память с корректным выравниванием, регистрацией защиты потока управления и таблиц раскрутки исключений, загрузил бы зависимые библиотеки и в целом правильно подготовил среду выполнения для запуска нужного кода.
С тех пор от этого клиента не поступало никаких известий.
1 Я не сказал, что это безопасный способ инъектирования кода. Он всего лишь более безопасный.
Автор:
PatientZero