5 лет назад я сказал в одной из своих статей, что однажды вернусь с методом горячего исправления функций внутри живых процессов; Итак… я думаю, это тот самый день.

Здесь мы попытаемся добиться замены извне функции внутри работающего исполняемого файла без остановки/замораживания процесса ( или сломать его…).

На мой взгляд, применение горячих исправлений — довольно сложная задача, если ее реализовать с нуля, поскольку:

  • для этого требуется доступ к памяти другого процесса (большинство операционных систем являются поклонниками изоляции процессов)
  • имеет ограничения совместимости программного обеспечения (бинарные файлы Windows и двоичные файлы Linux)
  • имеет ограничения по совместимости архитектуры (32-разрядная и 64-разрядная)
  • это подразумевает работу с машинным кодом и выносит на стол определенные проблемы
  • он имеет только дидактическую цель — вероятно, никто не будет использовать метод «с нуля», поскольку есть инструменты, которые делают это лучше

Учитывая это, я думаю, что лучше использовать то, что действительно было написано для этой задачи, а не кодировать что-то вручную. Поэтому мы рассмотрим способ сделать это с помощью Intel Pin. Я наткнулся на этот инструмент, работая над совершенно другим проектом, но он кажется довольно универсальным. По сути, он описывается как Инструмент динамического бинарного инструментирования, однако мы будем использовать его для облегчения процедуры записи кода в память другого процесса.

Первоначальные приготовления

Начните с загрузки Intel Pin и извлеките его куда-нибудь в свое рабочее пространство.

Отказ от ответственности. Я использую это руководство для Ubuntu x86_64. Вы могли бы увидеть некоторые bash.

Я буду использовать следующую фиктивную программу на C в качестве цели для горячего исправления:

#include<stdio.h>

// TODO: hot patch this method
void read_input()
{
    printf("Tell me your name:\n");
    
    char name[11];
    scanf("%s", name); // this looks bad
    
    printf("Hello, %s!\n\n", name);
}

int main()
{
    // not gonna end too soon
    while(1 == 1)
        read_input();
    
    return 0;
}

Некоторые из вас, вероятно, заметили, что функция read_input() написана не очень хорошо, поскольку она считывает входные данные с помощью scanf("%s", name); и, таким образом, позволяет злоумышленнику перехватить выполнение программы с помощью переполнения буфера.

Структура проекта

Intel Pin работает, выполняя действия, указанные в инструментах, с целевыми двоичными файлами или процессами. Например, у вас может быть инструмент, который говорит «увеличивать счетчик каждый раз, когда вы находите инструкцию RET», который вы можете прикрепить к исполняемому файлу и получить значение счетчика в определенное время.

Он предлагает каталог с примерами инструментов, которые можно найти по адресу: pin/source/tools/. Чтобы избежать обновления зависимостей make-файла, мы будем работать здесь, поэтому продолжим, создав новый каталог (мой называется Hotpatch) — здесь происходит кодирование.

Кроме того, скопируйте makefile в новый каталог, если вам не хочется его писать:

cp ../SimpleExamples/makefile .

И используйте следующий файл в качестве файла makefile.rules:

TEST_TOOL_ROOTS := hotpatch # for hotpatch.cpp
SANITY_SUBSET := $(TEST_TOOL_ROOTS) $(TEST_ROOTS)

Наконец, создайте файл с именем hotpatch.cpp с некоторым фиктивным кодом и выполните команду make. Если все работает нормально, у вас должно получиться что-то вроде этого…

Программирование Hot Patcher

Вся идея вращается вокруг регистрации обратного вызова, который вызывается каждый раз, когда двоичный файл загружает изображение (см. IMG_AddInstrumentFunction()). Поскольку метод определен в работающей программе, нас интересует, когда процесс загружает собственное изображение. В этом обратном вызове мы ищем метод, который мы хотим исправить (заменить) — в моем примере это read_input().

Вы можете перечислить функции, которые присутствуют в двоичном файле, используя:

nm targeted_binary_name

Процесс замены функции ( RTN_ReplaceSignatureProbed()) основан на зондах — как видно из названия, которые, согласно заявлениям Intel, обеспечивают меньшие накладные расходы и менее навязчивый. Под капотом Intel Pin перезапишет исходные инструкции функции с помощью JMP, указывающего на замещающую функцию. Вы можете вызывать исходную функцию, если это необходимо.

Без лишних слов, код, который у меня получился:

#include "pin.H"
#include <iostream>
#include <stdio.h>


char target_routine_name[] = "read_input";


// replacement routine's code (i.e. patched read_input)
void read_input_patched(void *original_routine_ptr, int *return_address)
{
    printf("Tell me your name:\n");
    
    // 5 stars stdin reading method
    char name[12] = {0}, c;
    fgets(name, sizeof(name), stdin);
    name[strcspn(name, "\r\n")] = 0;

    // discard rest of the data from stdin
    while((c = getchar()) != '\n' && c != EOF);

    printf("Hello, %s!\n\n", name);
}


void loaded_image_callback( IMG current_image, VOID *v )
{
    // look for the routine in the loaded image
    RTN current_routine = RTN_FindByName(current_image, target_routine_name);
    

    // stop if the routine was not found in this image
    if (!RTN_Valid(current_routine))
        return;

    // skip routines which are unsafe for replacement
    if (!RTN_IsSafeForProbedReplacement(current_routine))
    {
        std::cerr << "Skipping unsafe routine " << target_routine_name << " in image " << IMG_Name(current_image) << std::endl;
        return;
    }

    // replacement routine's prototype: returns void, default calling standard, name, takes no arugments 
    PROTO replacement_prototype = PROTO_Allocate(PIN_PARG(void), CALLINGSTD_DEFAULT, target_routine_name, PIN_PARG_END());

    // replaces the original routine with a jump to the new one 
    RTN_ReplaceSignatureProbed(current_routine, 
                               AFUNPTR(read_input_patched), 
                               IARG_PROTOTYPE, 
                               replacement_prototype,
                               IARG_ORIG_FUNCPTR,
                               IARG_FUNCARG_ENTRYPOINT_VALUE, 0,
                               IARG_RETURN_IP,
                               IARG_END);

    PROTO_Free(replacement_prototype);

    std::cout << "Successfully replaced " << target_routine_name << " from image " << IMG_Name(current_image) << std::endl;
}


int main(int argc, char *argv[])
{
    PIN_InitSymbols();

    if (PIN_Init(argc, argv))
    {
        std::cerr << "Failed to initialize PIN." << std::endl; 
        exit(EXIT_FAILURE);
    }

    // registers a callback for the "load image" action
    IMG_AddInstrumentFunction(loaded_image_callback, 0);
    
    // runs the program in probe mode
    PIN_StartProgramProbed();
    
    return EXIT_SUCCESS;
}

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

sudo ../../../pin -pid $(pidof targeted_binary_name) -t obj-intel64/hotpatch.so

Результаты и выводы

Aaand, кажется, работает:

В заключение, я почти уверен, что Intel Pin способен на более сложные вещи, чем то, что я представляю здесь — я считаю, что это уровень примеров (на самом деле это вдохновлено примером). Мне кажется довольно странным, что это не более популярный инструмент — и нет, Intel не платит мне за его поддержку.

Тем не менее, я надеюсь, что эта статья сможет предоставить поддержку и решения/идеи тем, кто ищет методы горячего исправления и кто, как и я, никогда раньше не слышал о Intel Pin.

Первоначально опубликовано на https://codingvision.net 20 августа 2019 г.