Как написать самоизменяющийся код в сборке x86 - PullRequest
44 голосов
/ 27 января 2011

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

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

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

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

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

РЕДАКТИРОВАТЬ: Я также работаюэтот код в Linux.

Ответы [ 7 ]

46 голосов
/ 27 января 2011

вау, это оказалось намного более болезненным, чем я ожидал. 100% боли было в том, что linux защищал программу от перезаписи и / или выполнения данных.

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

Самомодифицирующийся код прост: если вы возьмете программу или хотя бы просто две простые функции, скомпилируете, а затем разберете, вы получите коды операций для этих инструкций. или используйте nasm для компиляции блоков ассемблера и т. д. Из этого я определил код операции для немедленной загрузки в eax и возврата.

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

во втором примере используется существующая функция, скомпилированная в программу, опять же, из-за механизма защиты, который вы не можете просто указать на нее и изменить байты, вы должны снять защиту с записи. Таким образом, вы должны выполнить резервное копирование до вызова границы предыдущей страницы 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);
}
9 голосов
/ 30 января 2011

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

Создание исполняемого кода во время выполнения должно быть простым делом mmap () с использованием некоторой памяти с помощью PROT_EXEC и PROT_WRITEразрешения.Вы также можете вызвать mprotect () для некоторой памяти, которую вы выделили самостоятельно, как dwelch делал выше.

3 голосов
/ 22 апреля 2012

Немного более простой пример, основанный на примере выше. Благодаря 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);
}
3 голосов
/ 27 января 2011

Вы также можете посмотреть на такие проекты, как GNU lightning . Вы даете ему код для упрощенной машины RISC-типа, и он генерирует правильную машину динамически.

Очень реальная проблема, о которой вы должны подумать - это взаимодействие с иностранными библиотеками. Вам, вероятно, понадобится поддерживать хотя бы некоторые вызовы / операции системного уровня, чтобы ваша ВМ была полезна. Совет Kitsune - хорошее начало, чтобы вы подумали о вызовах системного уровня. Вы, вероятно, будете использовать mprotect, чтобы гарантировать, что измененная память станет легально исполняемой. (@KitsuneYMG)

Некоторых FFI, разрешающих вызовы динамических библиотек, написанных на C, должно быть достаточно, чтобы скрыть многие детали, относящиеся к ОС. Все эти проблемы могут немного повлиять на ваш дизайн, поэтому лучше подумать о них как можно раньше.

2 голосов
/ 31 августа 2018

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

FASM ассемблер https://github.com/ZenLulz/Fasm.NET

дизассемблер UDIS86: https://github.com/vmt/udis86

Инструкции читаются с помощью Udis86, пользователь может редактировать их как строку, а затем FASM используется для сборки новых байтов. Они могут быть записаны обратно в память, и, как отмечали другие пользователи, для обратной записи требуется использование VirtualProtect в Windows или mprotect в Unix.

Примеры кода для StackOverflow немного длинны, поэтому я отсылаю вас к статье, которую я написал с примерами кода:

https://medium.com/squallygame/how-we-wrote-a-self-hacking-game-in-c-d8b9f97bfa99

Работающее репозиторий Windows здесь (очень легкий):

https://github.com/Squalr/SelfHackingApp

Эти примеры для Windows, но достаточно просто поменять VirtualProtect на mprotect, чтобы это работало для Linux

1 голос
/ 21 мая 2016

Это написано в сборке 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
0 голосов
/ 27 января 2011

Я никогда не писал самоизменяющийся код, хотя у меня есть общее представление о том, как он работает. В основном вы записываете в память инструкции, которые хотите выполнить, и переходите туда. Процессор интерпретирует те байты, которые вы написали, и (пытается) их выполнить. Например, вирусы и программы защиты от копирования могут использовать эту технику.
Что касается системных вызовов, вы были правы, аргументы передаются через регистры. Для ссылки на системные вызовы linux и их аргументы просто проверьте здесь .

...