В большинстве операционных систем (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"
.