Подтвердить что ты не робот

Как написать самомодифицирующийся код в сборке x86

Я ищу писать JIT-компилятор для виртуальной машины для хобби, над которой я работал недавно. Я знаю немного сборки, (я в основном программист на C. Я могу прочитать большую сборку со ссылкой на коды операций, которые я не понимаю, и написать несколько простых программ.), Но мне трудно понять несколько примеров самомодифицирующегося кода, который я нашел в Интернете.

Это один из таких примеров: http://asm.sourceforge.net/articles/smc.html

Приведенная примерная программа выполняет около четырех различных модификаций при запуске, ни одна из которых не объясняется четко. Прерывания ядра Linux используются несколько раз и не объясняются и не детализированы. (Автор пересылал данные в несколько регистров перед вызовом прерываний. Я предполагаю, что он передавал аргументы, но эти аргументы вообще не объясняются, оставляя читателя догадываться.)

То, что я ищу, является самым простым, самым простым примером в коде самомодифицирующей программы. Что-то, на что я могу смотреть, и использовать, чтобы понять, как должен быть написан самомодифицирующий код в сборке x86 и как он работает. Есть ли какие-либо ресурсы, на которые вы можете указать мне, или какие-либо примеры, которые вы можете дать, которые будут адекватно демонстрировать это?

Я использую NASM в качестве своего ассемблера.

EDIT: Я также запускаю этот код в Linux.

4b9b3361

Ответ 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&lt;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