
«Зачем вообще писать программу, меняющую код в процессе выполнения? Это же ужасная идея!»
Да, всё так и есть. Но это и хороший опыт. Такое делают только тогда, когда хотят что-то исследовать, или из любопытства.
Самоизменяемые/самомодифицируемые программы не обладают особой полезностью. Они усложняют отладку, программа становится зависимой от оборудования, а изучение кода превращается в очень утомительный и запутанный процесс, если только вы не опытный разработчик на ассемблере. Единственный разумный сценарий применения самоизменяемых программа в реальном мире — это механизм маскировки зловредного ПО от антивирусов. Моя цель исключительно научна, поэтому ничем подобным я заниматься не буду.
Предупреждение: в этом посте активно используется язык ассемблера x86_64, в котором я ни в коем случае не являюсь специалистом. Для написания статьи мне пришлось изучать приличный объём материалов, и, возможно (почти наверняка), в ней есть ошибки.
Первый этап написания самоизменяемой программы — обеспечение возможности изменения кода в среде выполнения. Программисты давно уже поняли, что это плохая идея, поэтому были добавлены меры защиты, предотвращающие изменение кода программы в среде выполнения. Для начала нам нужно понять, где находятся команды при выполнении программы. Когда программа готовится к выполнению, загрузчик загружает всю программу в память. Затем программа выполняется внутри пространства виртуальной памяти, которым управляет ядро. Это адресное пространство разбито на сегменты, показанные ниже.

В данном случае нас интересует лишь текстовый сегмент (Text segment). В нём хранятся команды процесса. За кулисами адресного пространства находятся страницы, с которыми работает ядро. Эти страницы отображаются на физическую память компьютера. Ядро управляет разрешениями для каждой из этих страниц. По умолчанию страницы текстового сегмента имеют разрешения на чтение и выполнение. Мы не можем выполнять в них запись. Для того, чтобы получить возможность менять команды в среде выполнения, нам нужно изменить разрешения страниц текстового сегмента так, чтобы можно было выполнять запись в них.
Менять разрешения страницы можно при помощи функции
mprotect()
. Здесь стоит учитывать, что при работе с mprotect()
передаваемый ей указатель должен быть выровнен по границе страницы. Ниже показана функция, которая перемещает переданный ей указатель на границу страницы, а затем меняет разрешения страницы на чтение, запись и выполнение.int change_page_permissions_of_address(void *addr) {
int page_size = getpagesize();
addr -= (unsigned long)addr % page_size;
if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
return -1;
}
return 0;
}
Если мы передадим этой функции указатель, который указывает на адрес в текстовом сегменте, то в страницу в текстовом сегменте можно будет выполнять запись. Важно отметить, что операционная система может отказывать в праве на запись в текстовый сегмент. Я работаю в Linux, который позволяет выполнять запись в текстовый сегмент. Если у вас другая операционная система, то проверяйте возвращаемое значение, чтобы понять, не было ли выполнение
mprotect()
неудачным. В показанных ниже примерах мы предполагаем, что функция, которую будем изменять, полностью умещается в одну страницу. В случае длинных функций это может быть не так.Теперь, когда мы можем выполнять запись в текстовый сегмент, возникает следующий вопрос: что именно мы будем записывать?
Давайте начнём с чего-то простого. Допустим, у меня есть следующая функция:
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
foo()
создаёт и инициализирует локальную переменную i
значением 0, а затем выполняет её инкремент на 1 и выводит её в stdout. Давайте посмотрим, сможем ли мы изменить значение, на которое выполняется инкремент i
.Для решения этой задачи нам нужно изучить не только команды, в которые компилируется
foo()
, но и сам машинный код, в который собирается foo()
. Давайте поместим foo()
в программу, чтобы это было проще сделать.#include <stdio.h>
void foo(void);
int main(void) {
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
Теперь
foo()
находится в готовой программе на C, поэтому можно её скомпилировать. Сделать это можно так:$ gcc -o foo foo.c
И вот тут всё начинает становиться интереснее. Нам нужно дизассемблировать созданный GCC двоичный файл, чтобы увидеть команды, из которых состоит
foo()
. Это можно сделать при помощи утилиты objdump
:$ objdump -d foo > foo.dis
Если открыть
foo.dis
в текстовом редакторе, то примерно в строке 128 (в зависимости от используемой версии GCC команды foo
могут немного различаться) вы должны увидеть дизассемблированную функцию foo()
. Она выглядит так:0000000000400538 <foo>
400538: 55 push %rbp
400539: 48 89 e5 mov %rsp,%rbp
40053c: 48 83 ec 10 sub $0x10,%rsp
400540: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
400547: 83 45 fc 01 addl $0x1,-0x4(%rbp)
40054b: 8b 45 fc mov -0x4(%rbp),%eax
40054e: 89 c6 mov %eax,%esi
400550: bf 14 06 40 00 mov $0x400614,%edi
400555: b8 00 00 00 00 mov $0x0,%eax
40055a: e8 b1 fe ff ff callq 400410
<printf@plt>
40055f: c9 leaveq
400560: c3 retq
400561: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
400568: 00 00 00
40056b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
Если раньше вы никогда не работали с кодом x86_64, то это может выглядеть непонятно. По сути, здесь мы опускаем стек на 4 байта (размер integer в моей системе), чтобы использовать его как место хранения переменной
i
. Затем мы инициализируем эти 4 байта значением 0 и прибавляем к этому значению 1. Всё остальное после этого (40054b) копирует значения для подготовки к вызову функции printf().Таким образом, если мы хотим изменить значение, на которое увеличивается
i
, нам нужно изменить следующую команду:400547: 83 45 fc 01 addl $0x1,-0x4(%rbp)
Прежде, чем двигаться дальше, давайте разберём эту команду.
400547 |
83 45 fc 01 |
addl $0x1,-0x4(%rbp) |
---|---|---|
Первый столбец — это адрес памяти этой команды. | Второй столбец — это машинный код команды. Это байты, которые считает CPU и на которые он будет реагировать. | Третий столбец — это человекочитаемый (для людей, уже имеющих соответствующие знания) дизассемблированный машинный код из второго столбца. |
addl |
$0x1 |
-0x4(%rbp) |
---|---|---|
addl — это команда. В наборе команд x86_64 есть несколько команд сложения (add). Конкретно эта означает прибавление 8-битного значения к регистру или адресу памяти. |
$0x1 — это непосредственное значение. Символ доллара обозначает непосредственные значения, а префикс 0x — что за ним идёт шестнадцатеричное число. В данном случае число просто равно 1, потому что по основанию 10 0x1 = 1 . |
-0x4(%rbp) — это адрес памяти, к которому нужно прибавить значение. Здесь он означает, что нужно прибавить его к текущему адресу указателя базового стека, смещённому на 4 байта. Именно в этом месте стека находится наша переменная i . |

На этом моменте x86_64 становится по-настоящему сложным. Команды x86_64 имеют переменную длину, поэтому для незнакомых с ними декодирование команд вручную может быть запутанным и долгим процессом. Чтобы упростить его, существуют различные источники документации. На x86ref.net есть отличная документация, например, справка по команде addl. Если осмелитесь, можете изучить также Intel 64 and IA-32 Architectures Developer’s Manual: Combined Vols. 1, 2, and 3 (предупреждаю, это PDF на три тысячи страниц).
В нашем случае эти байты означают следующее:
83 |
45 |
fc |
01 |
---|---|---|---|
83 — это опкод команды addl . Все команды имеют опкод, сообщающий процессору, какую команду выполнять. |
45 — байт ModR/M. Согласно документации Intel, 0x45 = [RBP/EBP]+disp8 . Это значит, что 0x45 , обозначающий регистр %rbp — это регистр назначения, а следующий за ним байт (в данном случае 0xfc ) — байт смещения. |
fc — это байт смещения. 0xfc = 0b11111100 . Байт смещения дополняется знаком, то есть это значение просто равно 0b100 , или -4. |
01 — это непосредственное значение, которое будет прибавлено к указанному адресу памяти. Именно это значение нужно поменять, чтобы изменить значение, на которое увеличивается i . |
Итак, теперь мы можем поменять команду и знаем, что нужно менять; нам лишь нужно знать, как её менять.
Напомню, что мы хотим изменить байт 01 в команде
addl $0x1,-0x4(%rbp)
.Для этого нам нужно получить адрес этого байта. Получение адреса
foo()
в среде выполнения — это тривиальная задача, поскольку нам нужно лишь найти смещение этого байта от начала foo()
. Это можно сделать двумя способами:- Использовать дизассемблированный ранее
objdump
код, чтобы подсчитать количество байтов между началом функции и нужным нам байтом. - Написать функцию, выводящую команды
foo()
и их смещение от начала функции.
А почему бы не использовать оба способа?
Давайте для начала рассмотрим способ с
objdump
. Дизассемблированный код foo()
до интересующей нас команды addl
выглядит так:0000000000400538 <foo>:
400538: 55 push %rbp
400539: 48 89 e5 mov %rsp,%rbp
40053c: 48 83 ec 10 sub $0x10,%rsp
400540: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
400547: 83 45 fc 01 addl $0x1,-0x4(%rbp)
Функция начинается с
400538
, а интересующий нас байт находится в 40055a (400547 + 3)
(помните, что это шестнадцатеричные значения!), то есть смещение равно 40055a - 400538 = 12
. Так как это шестнадцатеричное (hex) значение, то при вычислении нужных нам смещений нужно использовать hex-значения или преобразовывать их в десятеричный вид. Последнее проще, поэтому нам нужно смещение 0x12 = 18
.В этом можно убедиться, написав короткую функцию, которая выводит команды переданной функции. Вот приведённая выше программа с внесёнными изменениями:
#include <stdio.h>
void foo(void);
void bar(void);
void print_function_instructions(void *func_ptr, size_t func_len);
int main(void) {
void *foo_addr = (void*)foo;
void *bar_addr = (void*)bar;
print_function_instructions(foo_addr, bar_addr - foo_addr);
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
void bar(void) {}
void print_function_instructions(void *func_ptr, size_t func_len) {
for(unsigned char i=0; i<func_len; i++) {
unsigned char *instruction = (unsigned char*)func_ptr+i;
printf("%p (%2u): %x\n", func_ptr+i, i, *instruction);
}
}
Обратите внимание, что для определения длины
foo()
мы добавили пустую функцию bar()
, которая идёт сразу за foo()
. Вычтя адрес bar()
из адреса foo()
, можно определить длину foo()
в байтах. Разумеется, при этом предполагается, что bar()
следует непосредственно за foo()
.Результат выполнения программы будет выглядеть так:
$ ./foo
0x40056c ( 0): 55
0x40056d ( 1): 48
0x40056e ( 2): 89
0x40056f ( 3): e5
0x400570 ( 4): 48
0x400571 ( 5): 83
0x400572 ( 6): ec
0x400573 ( 7): 10
0x400574 ( 8): c7
0x400575 ( 9): 45
0x400576 (10): fc
0x400577 (11): 0
0x400578 (12): 0
0x400579 (13): 0
0x40057a (14): 0
0x40057b (15): 83
0x40057c (16): 45
0x40057d (17): fc
0x40057e (18): 1 <-- Вот нужный нам байт!
0x40057f (19): 8b
0x400580 (20): 45
0x400581 (21): fc
0x400582 (22): 89
0x400583 (23): c6
0x400584 (24): bf
0x400585 (25): b4
0x400586 (26): 6
0x400587 (27): 40
0x400588 (28): 0
0x400589 (29): b8
0x40058a (30): 0
0x40058b (31): 0
0x40058c (32): 0
0x40058d (33): 0
0x40058e (34): e8
0x40058f (35): 7d
0x400590 (36): fe
0x400591 (37): ff
0x400592 (38): ff
0x400593 (39): c9
0x400594 (40): c3
По адресу
0x40057e
находится наш байт 0x1
. Как видите, смещение и в самом деле равно 18.Мы наконец-то готовы приступать к изменению кода! Имея указатель на
foo()
, можно создать беззнаковый указатель char на конкретный байт, который мы хотим изменить:unsigned char *instruction = (unsigned char*)foo_addr + 18;
*instruction = 0x2A;
Если мы всё сделали правильно, то этот код поменяет непосредственное значение в команде
addl
на 0x2A
, или 42. Теперь при вызове foo()
она вместо 1 выведет 42.Соединим всё вместе:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
void foo(void);
int change_page_permissions_of_address(void *addr);
int main(void) {
void *foo_addr = (void*)foo;
// Меняем разрешения страницы, содержащей foo(), на чтение, запись и выполнение
// Предполагается, что foo() полностью умещается в одну страницу
if(change_page_permissions_of_address(foo_addr) == -1) {
fprintf(stderr, "Error while changing page permissions of foo(): %s\n", strerror(errno));
return 1;
}
// Вызываем первоначальную foo()
puts("Calling foo...");
foo();
// Меняем непосредственное значение в команде addl функции foo() на 42
unsigned char *instruction = (unsigned char*)foo_addr + 18;
*instruction = 0x2A;
// Вызываем изменённую foo()
puts("Calling foo...");
foo();
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
int change_page_permissions_of_address(void *addr) {
// Перемещаем указатель к границе страницы
int page_size = getpagesize();
addr -= (unsigned long)addr % page_size;
if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
return -1;
}
return 0;
}
Скомпилируем код:
$ gcc -std=c99 -D_BSD_SOURCE -o foo foo.c
Вывод будет таким:
$ ./foo
Calling foo...
i: 1
Calling foo...
i: 42
Победа! При первом вызове
foo()
она выводит 1, потому что так ей говорит исходный код. После изменения она выводит 42.Итак, теперь у нас есть самоизменяемая программа на C. Однако она довольно скучная, потому что меняет только одно число. Будет гораздо интереснее изменить
foo()
так, чтобы она делала нечто совершенно другое. Например, выполняла exec()
оболочки!Но как нам запускать оболочку при вызове
foo()
? Логично будет использовать системный вызов execve
, но это гораздо сложнее, чем просто поменять один байт.Если мы собираемся изменить
foo()
так, чтобы она выполняла exec
оболочки, то нам понадобятся для этого команды. К счастью для нас, участники сообщества исследователей безопасности любят использовать машинный код для выполнения exec
оболочки, поэтому нам будет просто найти такие команды. Поискав «x86_64 shellcode», мы обнаружим команды для выполнения этой задачи. Они имеют следующий вид:char shellcode[] =
"\x48\x31\xd2" // xor %rdx, %rdx
"\x48\x31\xc0" // xor %rax, %rax
"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00" // mov $0x68732f6e69622f, %rbx
"\x53" // push %rbx
"\x48\x89\xe7" // mov %rsp, %rdi
"\x50" // push %rax
"\x57" // push %rdi
"\x48\x89\xe6" // mov %rsp, %rsi
"\xb0\x3b" // mov $0x3b, %al
"\x0f\x05"; // syscall
Этот код взят из http://www.exploit-db.com/exploits/13691/ с двумя моими изменениями, о которых мы поговорим ниже.
- Я добавил
xor %rax, %rax
, чтобы регистр%rax
был обнулён. В противном случае он мог быть ненулевым, что вызвало бы segfault. - Я изменил непосредственное значение
$0x68732f6e69622f2f
на$0x68732f6e69622f00
. Это позволило мне избавиться от команды сдвига, благодаря чему общая длина составила 30 байтов. Обычно подобный шеллкод инъецируется через переполнения буфера или с помощью других зловредных атак, использующих недочёты в обработке строк программой. C-строки завершаются символом NUL, имеющим значение 0, поэтому большинство функцийstring.h
выполняет возврат при считывании байта NUL. По этой причине люди, работающие в сфере безопасности, стараются избегать NUL. В данном случае с символами NUL всё в порядке, поэтому мы можем просто заменить дополнительный0x2f
на0x00
и отказаться от команды сдвига. Посмотрите по ссылке выше изначальный код, чтобы разобраться, чем отличаются от него мои изменения.
Прежде, чем двигаться дальше, давайте разберёмся, что делает приведённый выше шеллкод. Для начала нам нужно понять, как работает системный вызов. Системный вызов (syscall, system call) — это вызов ядра функцией, позволяющий попросить ядро выполнить какое-то действие. Это может быть что-то, разрешения на что есть только у ядра, поэтому мы и вынуждены просить его. В данном случае системный вызов
execve
сообщает ядру, что мы хотим запустить другой процесс и заменить адресное пространство нашего процесса адресным пространством нового процесса. Это означает, что в случае успешного выполнения execve
наш процесс, по сути, завершит выполнение.Для выполнения системного вызова в x86_64 мы должны подготовиться к нему, записав нужные значения в нужные регистры, а затем выполнив команду
syscall
. Нужные значения и регистры уникальны для каждой операционной системы. Я работаю в Linux, поэтому давайте взглянем на его документацию по системному вызову execve
:%rax |
Syscall | %rdi |
%rsi |
%rdx |
---|---|---|---|---|
59 |
sys_execve |
const char *filename |
const char *const argv[] |
const char *const envp[] |
Полный список системных вызовов можно найти в http://blog.rchapman.org/post/36801038863/linux-system-call-table-for-x86-64.
Если вы знакомы с прототипом функции
execve()
на C (для справки она представлена ниже), то можете заметить, насколько похожа подготовка системного вызова на вызов функции из программы на C.int execve(const char *filename, char *const argv[], char *const envp[]);
Если вы незнакомы с x86, то важно отметить, что процедура системного вызова сильно различается в x86 и x86_64. В наборе команд x86 команда системного вызова отсутствует. В x86 системные вызовы выполняются срабатыванием прерывания. Кроме того, в Linux номер системного вызова
execve
различается в x86 и x86_64. (11 в x86; 59 в x86_64).Теперь, когда мы знаем, как подготовить системный вызов, давайте объясним каждый этап шеллкода.
Машинный код | Команда | Объяснение |
---|---|---|
\x48\x31\xd2 |
xor %rdx, %rdx |
Обнуление регистра %rdx |
\x48\x31\xc0 |
xor %rax, %rax |
Обнуление регистра %rax . Позже мы используем его для значений NULL , так что он должен быть обнулён. |
\x48\xbb\x2f\x62\x69 |
mov $0x68732f6e69622f, %rbx |
Присваиваем регистру %rbx значение hs/nib/ . Процессоры Intel имеют формат little endian, поэтому строка должна идти в обратном порядке. Проще всего сделать это на Python при помощи '/bin/sh'[::-1].encode('hex') . Удобно, что "/bin/sh" 64-битный, поэтому умещается в один регистр. Что-то более длинное потребовало бы хитростей для конкатенации длинных строк. |
\x53 |
push %rbx |
Записываем в стек строку /bin/sh (которая пока находилась в регистре %rbx ). Команда push самостоятельно изменит указатель стека. |
\x48\x89\xe7 |
mov %rsp, %rdi |
Согласно документации по системным вызовам, регистр %rdi должен указывать на адрес памяти программы, который нужно выполнить. Указатель стека (регистр %rsp ) сейчас указывает на эту строку, поэтому копируем указатель стека в %rdi . |
\x50 |
push %rax |
Второй аргумент функции execve() — это массив argv . Этот массив должен завершаться NULL. Процессоры Intel имеют формат little endian, поэтому нам сначала нужно записать в стек значение NULL, чтобы обозначить конец массива. Помните, что ранее мы обнулили %rax , поэтому для получения значения NULL нам достаточно лишь записать этот регистр в стек. |
\x57 |
push %rdi |
По соглашению первый аргумент в массиве argv — это имя программы. Помните, что массив argv на самом деле является указателем на массив указателей на строки. В данном случае единственным значением в массиве будет имя программы. Также стоит помнить о том, что регистр %rdi теперь содержит адрес памяти строки /bin/sh в стеке. Если мы запишем этот адрес в стек, то получим массив указателей на строки, составляющий массив argv . |
\x48\x89\xe6 |
mov %rsp, %rsi |
Согласно документации по системным вызовам, регистр %rsi должен указывать на адрес массива argv в памяти. Так как мы только что записали массив argv в стек, указатель стека указывает на первый элемент argv . Нам достаточно лишь скопировать указатель стека в регистр %rsi . |
\xb0\x3b |
mov $0x3b, %al |
Последним этапом будет запись номера системного вызова (59 = 0x3b ) в регистр %rax . Здесь %al означает первый байт регистра %rax . Так мы записываем 59 в первый байт регистра %rax . Все остальные биты в %rax по-прежнему равны нулю. |
\x0f\x05 |
syscall |
Завершив всё это, мы готовы передать команду системного вызова, после чего всю работу возьмёт на себя ядро. Скрестим пальцы на удачу! |
foo()
так, чтобы она исполнила этот шеллкод. Вместо того, чтобы, как раньше, менять один байт в foo()
, мы хотим полностью заменить foo()
. Похоже, это работа для memcpy()
. Имея указатель на начало foo()
и указатель на наш шеллкод, мы можем скопировать шеллкод по адресу foo()
следующим образом: void *foo_addr = (void*)foo;
// http://www.exploit-db.com/exploits/13691/
char shellcode[] =
"\x48\x31\xd2" // xor %rdx, %rdx
"\x48\x31\xc0" // xor %rax, %rax
"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00" // mov $0x68732f6e69622f2f, %rbx
"\x53" // push %rbx
"\x48\x89\xe7" // mov %rsp, %rdi
"\x50" // push %rax
"\x57" // push %rdi
"\x48\x89\xe6" // mov %rsp, %rsi
"\xb0\x3b" // mov $0x3b, %al
"\x0f\x05"; // syscall
// Будьте внимательны здесь с длиной шеллкода, учитывайте то, что находится после foo
memcpy(foo_addr, shellcode, sizeof(shellcode)-1);
Единственное, с чем нам нужно быть внимательными — это запись после конца
foo()
. В данном случае мы в безопасности, потому что foo()
имеет длину 41 байта, а шеллкод — 29 байта. Стоит отметить, что поскольку шеллкод — это C-строка, в конце неё находится символ NUL. Мы хотим скопировать только байты самого шеллкода, поэтому вычитаем 1 из sizeof
шеллкода в аргументе длины memcpy
.Отлично! Давайте теперь соберём всё это в готовую программу.
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
void foo(void);
int change_page_permissions_of_address(void *addr);
int main(void) {
void *foo_addr = (void*)foo;
// Меняем разрешения страницы, содержащей foo(), на чтение, запись и выполнение
// Предполагается, что foo() полностью умещается в одну страницу
if(change_page_permissions_of_address(foo_addr) == -1) {
fprintf(stderr, "Error while changing page permissions of foo(): %s\n", strerror(errno));
return 1;
}
puts("Calling foo");
foo();
// http://www.exploit-db.com/exploits/13691/
char shellcode[] =
"\x48\x31\xd2" // xor %rdx, %rdx
"\x48\x31\xc0" // xor %rax, %rax
"\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00" // mov $0x68732f6e69622f2f, %rbx
"\x53" // push %rbx
"\x48\x89\xe7" // mov %rsp, %rdi
"\x50" // push %rax
"\x57" // push %rdi
"\x48\x89\xe6" // mov %rsp, %rsi
"\xb0\x3b" // mov $0x3b, %al
"\x0f\x05"; // syscall
// Будьте внимательны здесь с длиной шеллкода, учитывайте то, что находится после foo
memcpy(foo_addr, shellcode, sizeof(shellcode)-1);
puts("Calling foo");
foo();
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
int change_page_permissions_of_address(void *addr) {
// Перемещаем указатель на границу страницы
int page_size = getpagesize();
addr -= (unsigned long)addr % page_size;
if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
return -1;
}
return 0;
}
Скомпилируем программу:
$ gcc -o mutate mutate.c
Настал момент испытать удачу и выполнить этот код:
$ ./mutate
Calling foo
i: 1
Calling foo
$ echo "it works! we exec'd a shell!"
it works! we exec'd a shell!
Вот и всё, мы получили самоизменяемую программу на C!
Telegram-канал со скидками, розыгрышами призов и новостями IT 💻
