Как написать самомодифицирующийся код в сборке x86
Я ищу писать JIT-компилятор для виртуальной машины для хобби, над которой я работал недавно. Я знаю немного сборки, (я в основном программист на C. Я могу прочитать большую сборку со ссылкой на коды операций, которые я не понимаю, и написать несколько простых программ.), Но мне трудно понять несколько примеров самомодифицирующегося кода, который я нашел в Интернете.
Это один из таких примеров: http://asm.sourceforge.net/articles/smc.html
Приведенная примерная программа выполняет около четырех различных модификаций при запуске, ни одна из которых не объясняется четко. Прерывания ядра Linux используются несколько раз и не объясняются и не детализированы. (Автор пересылал данные в несколько регистров перед вызовом прерываний. Я предполагаю, что он передавал аргументы, но эти аргументы вообще не объясняются, оставляя читателя догадываться.)
То, что я ищу, является самым простым, самым простым примером в коде самомодифицирующей программы. Что-то, на что я могу смотреть, и использовать, чтобы понять, как должен быть написан самомодифицирующий код в сборке x86 и как он работает. Есть ли какие-либо ресурсы, на которые вы можете указать мне, или какие-либо примеры, которые вы можете дать, которые будут адекватно демонстрировать это?
Я использую NASM в качестве своего ассемблера.
EDIT: Я также запускаю этот код в Linux.
Ответы
Ответ 1
Вау, это оказалось намного более болезненным, чем я ожидал. 100% боли было linux, защищающее программу от перезаписывания и/или выполнения данных.
Два решения, показанные ниже. И было задействовано много googling, поэтому несколько простых поместил несколько байтов команд и выполнил их, мой mprotect и выравнивание по размеру страницы были отбракованы из поисковых запросов Google, что я должен был изучить для этого примера.
Самомодифицирующий код является прямым, если вы берете программу или, по крайней мере, только две простые функции, компилируете и затем разбираете, вы получите коды операций для этих инструкций. или использовать nasm для компиляции блоков ассемблера и т.д. Из этого я определил код операции, чтобы сразу загрузить eax, затем верните.
В идеале вы просто помещаете эти байты в некоторый ram и выполняете этот ram. Чтобы заставить Linux сделать это, вам нужно изменить защиту, а это значит, что вам нужно отправить указатель, который выровнен на странице mmap. Поэтому выделите больше, чем вам нужно, найдите выровненный адрес в пределах этого выделения, находящегося на границе страницы, и выполните mprotect с этого адреса и используйте эту память, чтобы поместить свои коды операций и затем выполнить.
второй пример берет существующую функцию, скомпилированную в программу, снова из-за механизма защиты вы не можете просто указать на нее и изменить байты, вы должны ее защитить от записи. Таким образом, вам нужно выполнить резервное копирование на предыдущий вызов страницы mprotect с этим адресом и достаточно байтов, чтобы покрыть код, который нужно изменить. Затем вы можете изменить байты/коды операций для этой функции любым способом (до тех пор, пока вы не перейдете на любую функцию, которую хотите продолжить использовать) и выполните ее. В этом случае вы можете видеть, что fun()
работает, затем я меняю его, чтобы просто вернуть значение, снова вызвать его и теперь оно было изменено.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
unsigned char *testfun;
unsigned int fun ( unsigned int a )
{
return(a+13);
}
unsigned int fun2 ( void )
{
return(13);
}
int main ( void )
{
unsigned int ra;
unsigned int pagesize;
unsigned char *ptr;
unsigned int offset;
pagesize=getpagesize();
testfun=malloc(1023+pagesize+1);
if(testfun==NULL) return(1);
//need to align the address on a page boundary
printf("%p\n",testfun);
testfun = (unsigned char *)(((long)testfun + pagesize-1) & ~(pagesize-1));
printf("%p\n",testfun);
if(mprotect(testfun, 1024, PROT_READ|PROT_EXEC|PROT_WRITE))
{
printf("mprotect failed\n");
return(1);
}
//400687: b8 0d 00 00 00 mov $0xd,%eax
//40068d: c3 retq
testfun[ 0]=0xb8;
testfun[ 1]=0x0d;
testfun[ 2]=0x00;
testfun[ 3]=0x00;
testfun[ 4]=0x00;
testfun[ 5]=0xc3;
ra=((unsigned int (*)())testfun)();
printf("0x%02X\n",ra);
testfun[ 0]=0xb8;
testfun[ 1]=0x20;
testfun[ 2]=0x00;
testfun[ 3]=0x00;
testfun[ 4]=0x00;
testfun[ 5]=0xc3;
ra=((unsigned int (*)())testfun)();
printf("0x%02X\n",ra);
printf("%p\n",fun);
offset=(unsigned int)(((long)fun)&(pagesize-1));
ptr=(unsigned char *)((long)fun&(~(pagesize-1)));
printf("%p 0x%X\n",ptr,offset);
if(mprotect(ptr, pagesize, PROT_READ|PROT_EXEC|PROT_WRITE))
{
printf("mprotect failed\n");
return(1);
}
//for(ra=0;ra<20;ra++) printf("0x%02X,",ptr[offset+ra]); printf("\n");
ra=4;
ra=fun(ra);
printf("0x%02X\n",ra);
ptr[offset+0]=0xb8;
ptr[offset+1]=0x22;
ptr[offset+2]=0x00;
ptr[offset+3]=0x00;
ptr[offset+4]=0x00;
ptr[offset+5]=0xc3;
ra=4;
ra=fun(ra);
printf("0x%02X\n",ra);
return(0);
}
Ответ 2
Поскольку вы пишете JIT-компилятор, вам, вероятно, не нужен самомодифицирующий код, вы хотите сгенерировать исполняемый код во время выполнения. Это две разные вещи. Самомодифицирующийся код - это код, который изменяется после того, как он уже запущен. Самомодифицирующийся код имеет большое ограничение производительности для современных процессоров и поэтому будет нежелательным для компилятора JIT.
Генерирование исполняемого кода во время выполнения должно быть простым вопросом mmap() в некоторой памяти с разрешениями PROT_EXEC и PROT_WRITE. Вы также можете вызвать mprotect() на некоторой памяти, которую вы выделили себе, как это сделал dwelch выше.
Ответ 3
Вы также можете посмотреть проекты, такие как молния GNU. Вы даете ему код для упрощенной машины типа RISC, и она генерирует правильную машину динамически.
Очень реальная проблема, о которой вы должны думать, связана с зарубежными библиотеками. Возможно, вам понадобится поддержка, по крайней мере, некоторых вызовов/операций системного уровня для вашей виртуальной машины. Совет Kitsune - хорошее начало, чтобы заставить вас задуматься о вызовах системного уровня. Вероятно, вы используете mprotect, чтобы гарантировать, что измененная память станет юридически исполняемой. (@KitsuneYMG)
Некоторое FFI, позволяющее звонить динамическим библиотекам, написанным на C, должно быть достаточным, чтобы скрыть многие детали ОС. Все эти проблемы могут немного повлиять на ваш дизайн, поэтому лучше начать думать о них раньше.
Ответ 4
Немного более простой пример, основанный на примере выше. Благодаря dwelch многое помогло.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
char buffer [0x2000];
void* bufferp;
char* hola_mundo = "Hola mundo!";
void (*_printf)(const char*,...);
void hola()
{
_printf(hola_mundo);
}
int main ( void )
{
//Compute the start of the page
bufferp = (void*)( ((unsigned long)buffer+0x1000) & 0xfffff000 );
if(mprotect(bufferp, 1024, PROT_READ|PROT_EXEC|PROT_WRITE))
{
printf("mprotect failed\n");
return(1);
}
//The printf function has to be called by an exact address
_printf = printf;
//Copy the function hola into buffer
memcpy(bufferp,(void*)hola,60 //Arbitrary size);
((void (*)())bufferp)();
return(0);
}
Ответ 5
Я никогда не писал самомодифицирующий код, хотя у меня есть общее представление о том, как это работает. В основном вы записываете в память инструкции, которые хотите выполнить, а затем переходите туда. Процессор интерпретирует те байты, которые вы написали, инструкции и (пытается) выполнить их. Например, вирусы и программы анти-копирования могут использовать эту технику.
Что касается системных вызовов, вы были правы, аргументы передаются через регистры. Для ссылки на системные вызовы linux и их аргумент просто проверьте здесь.
Ответ 6
Это написано в сборке AT & T. Как видно из выполнения программы, результат изменился из-за самомодифицирующего кода.
Компиляция: gcc -m32 modify.s modify.c
используется опция -m32, потому что пример работает на 32-битных машинах
Aessembly:
.globl f4
.data
f4:
pushl %ebp #standard function start
movl %esp,%ebp
f:
movl $1,%eax # moving one to %eax
movl $0,f+1 # overwriting operand in mov instuction over
# the new immediate value is now 0. f+1 is the place
# in the program for the first operand.
popl %ebp # standard end
ret
C тестовая программа:
#include <stdio.h>
// assembly function f4
extern int f4();
int main(void) {
int i;
for(i=0;i<6;++i) {
printf("%d\n",f4());
}
return 0;
}
Вывод:
1
0
0
0
0
0