C: Как изменить мою собственную программу в моей программе во время выполнения? - PullRequest
0 голосов
/ 08 сентября 2018

Во время выполнения ассемблер или машинный код (что это?) Должны находиться где-то в ОЗУ. Могу ли я как-нибудь получить к нему доступ, а также прочитать или даже написать?

Это только для образовательных целей.

Итак, я просто мог скомпилировать этот код. Я действительно читаю себя здесь?

#include <stdio.h>
#include <sys/mman.h>

int main() {
    void *p = (void *)main;
    mprotect(p, 4098, PROT_READ | PROT_WRITE | PROT_EXEC);
    printf("Main: %p\n Content: %i", p, *(int *)(p+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(p+i)) );
    }
}

Хотя, если я добавлю

*(int*)p =4;

тогда это ошибка сегментации.


Из ответов я мог бы построить следующий код, который изменяет себя во время выполнения:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment) {
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

// pattern is a 0-terminated string
char* find(char *string, unsigned int stringLen, char *pattern) {
    unsigned int iString = 0;
    unsigned int iPattern;
    for (unsigned int iString = 0; iString < stringLen; ++iString) {
        for (iPattern = 0;
            pattern[iPattern] != 0
            && string[iString+iPattern] == pattern[iPattern];
            ++iPattern);
        if (pattern[iPattern] == 0) { return string+iString; }
    }
    return NULL;
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }

    // Correct a part of THIS program directly in RAM
    char programSubcode[12] = {'H','e','l','l','o',
                                ' ','W','o','r','l','t',0};
    char *programCode = (char *)main;
    char *helloWorlt = find(programCode, 1024, programSubcode);
    if (helloWorlt != NULL) {
        helloWorlt[10] = 'd';
    }   
    printf("Hello Worlt\n");
    return 0;
}

Это потрясающе! Спасибо всем!

Ответы [ 4 ]

0 голосов
/ 09 сентября 2018

Самый простой и практичный способ сделать это - использовать указатели функций. Вы можете объявить указатель, такой как:

void (*contextual_proc)(void) = default_proc;

Затем вызовите его с синтаксисом contextual_proc();. Вы также можете назначить другую функцию с той же сигнатурой для contextual_proc, скажем, contextual_proc = proc_that_logs;, и любой код, который вызывает contextual_proc(), будет затем (по модулю Thread-Safety) вместо этого вызывать новый код.

Это во многом похоже на самоизменяющийся код, но его легче понять, он переносим и фактически работает на современных процессорах, где исполняемая память недоступна для записи и инструкции кэшируются.

В C ++ вы бы использовали для этого подклассы; Статическая диспетчеризация будет реализовывать его так же под капотом.

0 голосов
/ 08 сентября 2018

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

Могут быть некоторые препятствия на пути к этому на практике. Современные операционные системы пытаются ограничить разделы данных в памяти операциями чтения / записи, но без выполнения, и разделы машинного кода в памяти для чтения / выполнения, но без записи. Это делается для того, чтобы попытаться ограничить потенциальные уязвимости в безопасности, которые позволяют выполнять то, что программа чувствует, как помещать в память (например, случайные вещи, которые она может выгрузить из Интернета).

Linux предоставляет системный вызов mprotect , чтобы обеспечить некоторую настройку для защиты памяти. Windows предоставляет системный вызов SetProcessDEPPolicy .

Редактировать для обновленного вопроса

Похоже, вы пытаетесь сделать это в Linux и используете mprotect. Отправленный вами код не проверяет возвращаемое значение из mprotect, поэтому вы не знаете, является ли вызов успешным или неудачным. Вот обновленная версия, которая проверяет возвращаемое значение:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment)
{
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);

    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }
    printf("Main: %p\n Content: %i", main, *(int *)(main+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(main+i)) );
    }
}  

Обратите внимание на изменения параметра длины, переданного в mprotect, и функции выравнивания указателя по границе системной страницы. Вам нужно будет провести расследование в вашей конкретной системе. Моя система имеет выравнивание 4096 байт (определяется путем выполнения getconf PAGE_SIZE), и после выравнивания указателя и изменения параметра длины на mprotect в соответствии с размером страницы это работает, и позволяет записывать указатель на main.

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

0 голосов
/ 08 сентября 2018

В принципе это возможно, на практике ваша операционная система защитит себя от вашего опасного кода!

Самомодифицирующийся код мог считаться «изящной уловкой» в те дни, когда у компьютеров были очень крошечные воспоминания (в 1950-х годах). Позднее (когда в этом больше не было необходимости) это стало расцениваться как плохая практика - в результате получился код, который было сложно поддерживать и отлаживать.

В более современных системах (в конце 20-го века) это стало показателем поведения вирусов и вредоносных программ. Как следствие, все современные настольные операционные системы запрещают модификацию кодового пространства программы, а также предотвращают выполнение кода, введенного в пространство данных. Современные системы с MMU могут помечать области памяти как доступные только для чтения, а не исполняемые, например.

Более простой вопрос о том, как получить адрес кодового пространства - это просто. Например, значение указателя на функцию, как правило, является адресом функции:

int main()
{
    printf( "Address of main() = %p\n", (void*)main ) ;
}

Обратите внимание, что в современной системе этот адрес будет виртуальным, а не физическим адресом.

0 голосов
/ 08 сентября 2018

В большинстве операционных систем (Linux, Windows, Android, MacOSX и т. Д.) Программа не выполняется (непосредственно) в ОЗУ, но имеет виртуальное адресное пространство и работает в ней ( stricto sensu, код не всегда или не обязательно находится в ОЗУ, вы можете иметь код, которого нет в ОЗУ и который исполняется, после некоторого сбоя страницы выведите его прозрачно в ОЗУ). Оперативная память (непосредственно) управляется ОС, но ваш процесс видит только свое виртуальное адресное пространство (инициализируется в execve (2) время и изменяется с помощью mmap (2) , munmap, mprotect, mlock (2) ...). Используйте proc (5) и попробуйте cat /proc/$$/maps в оболочке Linux, чтобы лучше понять виртуальное адресное пространство вашего процесса оболочки. В Linux вы можете запросить виртуальное адресное пространство вашего процесса, прочитав файл /proc/self/maps (последовательно, это текстовый псевдофайл).

Чтение Операционные системы: Thee Easy Pieces , чтобы узнать больше об ОС.


На практике, если вы хотите дополнить код внутри вашей программы (работающей на некоторых распространенных ОС), вам лучше использовать plugins и динамические средства загрузки . В системах Linux и POSIX вы будете использовать dlopen (3) (который использует mmap и т. Д.), Затем с dlsym (3) вы получите (виртуальный) адрес какой-либо новой функции, и вы можете вызвать ее (сохраняя в некотором указателе функции вашего кода C).

Вы на самом деле не определяете, что такое программа . Я утверждаю, что программа - это не только исполняемый файл, но и сделанный из других ресурсов (таких как определенные библиотеки, возможно, шрифты или файлы конфигурации и т. Д.), И именно поэтому, когда вы устанавливаете некоторую программу, довольно часто гораздо больше, чем исполняемый файл перемещается или копируется (посмотрите, что make install делает для большинства свободных программ, даже таких простых, как GNU coreutils ). Следовательно, программа (в Linux), которая генерирует некоторый код C (например, во временном файле /tmp/genecode.c), компилирует этот код C в плагин /tmp/geneplug.so (запустив gcc -Wall -O -fPIC /tmp/genecode.c -o /tmp/geneplug.so), затем dlopen, который /tmp/geneplug.so плагин действительно модифицирует себя. И если вы пишете исключительно на C, это нормальный способ написания самоизменяющихся программ.

Как правило, ваш машинный код находится в сегменте кода , и этот сегмент кода доступен только для чтения (а иногда даже только для выполнения; читайте о NX бит ). Если вы действительно хотите перезаписать код (а не расширять его), вам необходимо использовать средства (например, mprotect (2) в Linux) для изменения этих разрешений и включения перезаписи внутри сегмента кода.

Как только какая-то часть вашего сегмента кода станет доступной для записи, вы можете перезаписать ее.

Также рассмотрим некоторые библиотеки JIT-компиляции , такие как libgccjit или asmjit (и другие), для генерации машинного кода в памяти.

Когда вы execve новый свежий исполняемый файл, большая часть его кода (пока) не находится в оперативной памяти. Но (с точки зрения пользовательского кода в приложении) вы можете запустить его (и ядро ​​будет прозрачно, но лениво переносить кодовые страницы в оперативную память, через требуется подкачка ). Это то, что я пытаюсь объяснить, говоря, что ваша программа работает в своем виртуальном адресном пространстве (а не непосредственно в ОЗУ). Чтобы объяснить это, нужна целая книга.

Например, если у вас огромный исполняемый файл (для простоты, предположим, что он статически связан) в один гигабайт. При запуске этого исполняемого файла (с execve) весь гигабайт не заносится в ОЗУ. Если ваша программа завершает работу быстро, большая часть гигабайта не была занесена в оперативную память и остается на диске. Даже если ваша программа работает долго, но никогда не вызывает огромную подпрограмму из ста мегабайт кода, эта часть кода (100 Мбайт из никогда не используемой подпрограммы) не будет в ОЗУ.


Кстати, stricto sensu, самоизменяющийся код в наши дни используется редко (а современные процессоры даже не справляются с этим эффективно, например, из-за кэшей и предикторов ветвления). Поэтому на практике вы не модифицируете точно свой машинный код (даже если это будет возможно).

и вредоносное ПО не нужно изменять исполняемый в данный момент код. Он может (и часто делает) вводить новый код в память и каким-то образом переходить к нему (точнее, вызывать его через некоторый указатель на функцию). Таким образом, в общем случае вы не перезаписываете существующий "активно используемый" код, вы создаете новый код в другом месте и вызываете его или переходите к нему.

Если вы хотите создать новый код где-либо еще в C, возможностей плагинов (например, dlopen и dlsym в Linux) или библиотек JIT более чем достаточно.

Обратите внимание, что упоминание об «изменении вашей программы» или «написании кода» очень неоднозначно в вашем вопросе.

Возможно, вы просто захотите расширить код вашей программы (и тогда уместно использовать методы плагинов или библиотеки JIT-компиляции). Обратите внимание, что некоторые программы (например, SBCL ) могут генерировать машинный код при каждом взаимодействии с пользователем.

Вы можете изменить существующий код вашей программы, но затем вам следует объяснить, что именно это означает (что означает для вас "код" точно ? выполняется машинная инструкция или это весь сегмент кода вашей программы?). Вы думаете о самоизменяющемся коде, о создании нового кода, о динамическом обновлении программного обеспечения ?

Могу ли я как-нибудь получить к нему доступ, прочитать или даже написать?

Конечно, да. Вам необходимо изменить защиту в своем виртуальном адресном пространстве для своего кода (например, с помощью mprotect), а затем записать много байтов для некоторой части «старого кода». Зачем вам это делать - это другая история (и вы не объяснили почему). Я не вижу в этом образовательных целей - вы, скорее всего, вылетите из программы довольно быстро (если вы не примете много мер предосторожности, чтобы написать достаточно хороший машинный код в память).

Я большой поклонник метапрограммирования , но обычно я генерирую некоторый новый код и прыгаю в него. На наших современных машинах я не вижу смысла перезаписывать существующий код. И (в Linux) моя manydl.c программа демонстрирует, что вы можете генерировать код C, компилировать и динамически связывать более миллиона плагинов (и dlopen всех) в одной программе. На практике на современных ноутбуках или настольных компьютерах вы можете генерировать много нового кода (до того, как вас ограничат ограничения). И C достаточно быстр (как во время компиляции, так и во время выполнения), чтобы вы могли генерировать тысячи строк C при каждом взаимодействии с пользователем (так несколько раз в секунду), компилировать и динамически загружать его (я делал это десять лет назад в своем несуществующий GCC MELT проект).

Если вы хотите перезаписать исполняемые файлы на диске (я не вижу в этом смысла, гораздо проще создавать свежие исполняемые файлы), вам необходимо глубоко понять их структуру , Для Linux погрузитесь в спецификации ELF .


В отредактированном вопросе вы забыли протестировать против сбоя mprotect. Это, вероятно, терпит неудачу (потому что 4098 не степень 2 и кратность страницы). Поэтому, пожалуйста, хотя бы код:

int c = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
if (c) { perror("mprotect"); exit(EXIT_FAILURE); };

Даже с 4096 (вместо 4098), что mprotect, скорее всего, потерпит неудачу с EINVAL, потому что main, вероятно, не выровнено по странице 4K. (Не забывайте, что ваш исполняемый файл также содержит crt0 код).

Кстати, для образовательных целей вы должны добавить следующий код в начале вашего main:

 char cmdbuf[80];
 snprintf (cmdbuf, sizeof(cmdbuf), "/bin/cat /proc/%d/maps", (int)getpid());
 fflush(NULL);
 if (system(cmdbuf)) 
   { fprintf(stderr, "failed to run %s\n", cmdbuf); exit(EXIT_FAILURE));

и вы можете добавить подобный фрагмент кода ближе к концу. Вы можете заменить строку формата snprintf для cmdbuf на "pmap %d".

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...