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



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




К сожалению, не доступен сервер mySQL