如何编写x86程序集中的自修改代码
我正在为最近正在处理的业余爱好虚拟机编写一个JIT编译器。 我知道一些程序集,(我主要是一个C程序员,我可以阅读大部分程序集,但是我不明白这些操作码的含义,然后编写一些简单的程序),但是我很难理解几个例子自我修改的代码我在网上find的。
这是一个这样的例子: http : //asm.sourceforge.net/articles/smc.html
所提供的示例程序在运行时进行了大约四种不同的修改,其中没有一个被清楚地解释。 Linux内核中断被多次使用,并没有解释或详细说明。 (作者在调用中断之前把数据移到了几个寄存器中,我假设他正在传递参数,但是这些参数根本没有解释,让读者猜测)。
我正在寻找的是自修改程序代码中最简单,最直接的例子。 有些东西我可以看一下,用来理解x86程序集中的自修改代码是如何编写的,以及它是如何工作的。 是否有任何资源可以指向我,或者您可以提供哪些可以充分certificate这一点的例子?
我正在使用NASM作为我的汇编程序。
编辑:我也在Linux上运行这个代码。
哇,结果比我想象的要痛苦得多。 Linux 100%的痛苦是保护程序不被覆盖和/或执行数据。
下面两个解决scheme。 而且涉及到大量的使用googlesearch,所以有些简单的把一些指令字节放在一边,执行它们是我的,页面大小的mprotect和alignment是从谷歌search中剔除的,这个例子我不得不学习。
自修改代码是直接的,如果你使用这个程序,或者至less只是两个简单的函数,编译然后反汇编,你将得到这些指令的操作码。 或者使用nasm编译汇编程序块等等。从这里我确定了操作码立即加载到eax然后返回。
理想情况下,你只需将这些字节放在一些ram中并执行该ram。 为了让Linux做到这一点,你必须改变保护,这意味着你必须发送一个指针,在mmap页面上alignment。 因此,分配比您需要的更多,find该页面边界上的分配内的alignment地址,并从该地址获取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); }
由于您正在编写JIT编译器,因此您可能不希望自修改代码,而是希望在运行时生成可执行代码。 这是两件不同的事情。 自修改代码是已经开始运行后修改的代码。 自修改代码在现代处理器上有很大的性能损失,因此对于JIT编译器来说是不可取的。
在运行时生成可执行代码应该是用PROT_EXEC和PROT_WRITE许可mmap()存储一些内存的简单事情。 你也可以在你自己分配的一些内存上调用mprotect(),就像dwelch在上面做的那样。
你也可以看看GNU闪电这样的项目。 你给它一个简化的RISC型机器的代码,并且dynamic地生成正确的机器。
你应该考虑一个非常现实的问题,就是与外国图书馆进行交stream。 您可能需要至less支持一些系统级别的调用/操作才能使您的虚拟机有用。 Kitsune的build议是让你考虑系统级调用的一个好的开始。 您可能会使用mprotect来确保您修改的内存可以合法执行。 (@KitsuneYMG)
某些允许调用以C语言编写的dynamic库的FFI应该足以隐藏很多操作系统特定的细节。 所有这些问题都会影响你的devise,所以最好早点考虑一下。
基于上面的例子一个简单的例子。 感谢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); }
我从来没有写过自我修改的代码,虽然我对它是如何工作有一个基本的了解。 基本上你在内存上写你想执行的指令然后跳到那里。 处理器解释你写的指令的字节并(尝试)执行它们。 例如,病毒和反拷贝程序可以使用这种技术。
关于系统调用,你是对的,参数是通过寄存器传递的。 对于Linux系统调用的参考和他们的参数只是在这里检查。
这是写在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
Ctesting程序:
#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