Как написать самомодифицирующийся код в сборке x86
Я смотрю на написание JIT-компилятора для виртуальной машины хобби, над которой я работал в последнее время. Я знаю немного сборки, (я в основном программистов. Я могу прочитать большинство сборок со ссылкой на опкоды, которые я не понимаю, и написать несколько простых программ.) но мне трудно понять несколько примеров самомодифицирующегося кода, которые я нашел в интернете.
Это один из таких примеров: http://asm.sourceforge.net/articles/smc.html
в пример предоставленной программы делает около четырех различных модификаций при запуске, ни одна из которых четко не объяснена. Прерывания ядра Linux используются несколько раз и не объясняются и не детализируются. (Перед вызовом прерываний автор переместил данные в несколько регистров. Я предполагаю, что он передавал аргументы, но эти аргументы вообще не объясняются, оставляя читателя догадываться.)
то, что я ищу-это самый простой, самый простой пример в код самомодифицирующийся программа. Что-то, что я могу посмотреть и использовать, чтобы понять, как должен быть написан самомодифицирующийся код в сборке x86 и как он работает. Есть ли какие-либо ресурсы, на которые вы можете указать мне, или какие-либо примеры, которые вы можете привести, чтобы адекватно продемонстрировать это?
Я использую NASM в качестве моего ассемблера.
EDIT: я также запускаю этот код в Linux.
7 ответов
Вау, это оказалось намного более болезненным, чем я ожидал. 100% боли был linux, защищающий программу от перезаписи и / или выполнения данных.
два решения, показанные ниже. И много googling было задействовано, поэтому несколько простых байтов инструкций и их выполнение были моими, mprotect и выравнивание по размеру страницы были отобраны из поисков google, материал, который я должен был узнать для этого примера.
код собственной личности дорабатывая прям вперед, если вы берете программу или хотя бы две простые функции, компилируете, а затем разбираете, вы получите коды операций для этих инструкций. или используйте nasm для компиляции блоков ассемблера и т. д. Из этого я определил код операции, чтобы загрузить немедленное в eax, а затем вернуться.
В идеале вы просто помещаете эти байты в некоторую ОЗУ и выполняете эту ОЗУ. Чтобы заставить 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 xd,%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);
}
поскольку вы пишете JIT-компилятор, вы, вероятно, не хотите self-modifying код, который вы хотите создать исполняемый код во время выполнения. Это две разные вещи. Самомодифицирующийся код-это код, который модифицируется после того, как он уже запущен. Самомодифицирующийся код имеет большой штраф производительности на современных процессорах и поэтому нежелателен для JIT-компилятора.
создание исполняемого кода во время выполнения должно быть простым вопрос mmap () в некоторой памяти с разрешениями PROT_EXEC и PROT_WRITE. Вы также можете вызвать mprotect () в некоторой памяти, выделенной вами, как это сделал dwelch выше.
вы также можете посмотреть на такие проекты, как GNU lightning. Вы даете ему код для упрощенной машины типа RISC, и она генерирует правильную машину динамически.
очень реальная проблема, о которой вы должны думать, - это взаимодействие с зарубежными библиотеками. Вероятно, вам понадобится поддержка хотя бы некоторых вызовов/операций системного уровня, чтобы ваша виртуальная машина была полезной. Совет Кицунэ-хорошее начало, чтобы заставить вас задуматься о вызовах системного уровня. Вы, вероятно, использовать mprotect для убедитесь,что измененная память становится юридически исполняемой. (@KitsuneYMG)
некоторые FFI, разрешающие вызовы динамических библиотек, написанных на C, должны быть достаточными, чтобы скрыть многие конкретные детали ОС. Все эти проблемы могут повлиять на ваш дизайн совсем немного, поэтому лучше начать думать о них раньше.
немного более простой пример, основанный на примере выше. Благодаря 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);
}
Я работаю над самомодифицирующейся игрой для обучения сборке 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
это написано в AT & T assembly. Как вы можете видеть из выполнения программы, выходные данные изменились из-за самомодифицирующегося кода.
компиляция: GCC-M32 изменить.ы изменить.c
опция-m32 используется, потому что пример работает на 32-битных машинах
Aessembly:
.globl f4
.data
f4:
pushl %ebp #standard function start
movl %esp,%ebp
f:
movl ,%eax # moving one to %eax
movl ,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
Я никогда не писал самомодифицирующийся код, хотя у меня есть базовое понимание того, как он работает. В основном вы пишете на память инструкции, которые хотите выполнить, а затем прыгаете туда. Процессор интерпретирует те байты, которые вы написали инструкции и (пытается) выполнить их. Например, вирусы и программы защиты от копирования могут использовать этот метод.
Что касается системных вызовов, вы были правы, аргументы передаются через регистры. Для справки системных вызовов linux и их аргумент просто проверить здесь.