我正在为最近一直在研究的业余爱好虚拟机编写JIT编译器。我知道有点汇编,(我主要是一个C程序员。我可以阅读大多数汇编参考我不理解的操作码,并编写一些简单的程序。)但我很难理解这几个例子我在网上发现的自我修改代码。
这是一个这样的例子:http://asm.sourceforge.net/articles/smc.html
提供的示例程序在运行时进行了大约四种不同的修改,其中没有一种被清楚地解释。 Linux内核中断被多次使用,没有解释或详细说明。 (作者在调用中断之前将数据移动到几个寄存器中。我假设他正在传递参数,但这些参数根本没有解释,让读者猜测。)
我正在寻找的是自修改程序代码中最简单,最直接的例子。我可以看到的东西,用于理解如何编写x86程序集中的自修改代码,以及它是如何工作的。是否有任何资源可以指出我,或者你能给出的任何可以充分证明这一点的例子?
我正在使用NASM作为我的汇编程序。
编辑:我也在Linux上运行此代码。答案 0 :(得分:45)
以下显示的两种解决方案。并且涉及了大量的谷歌搜索,所以稍微简单地说一些指令字节并执行它们是我的,在页面大小上的mprotect和对齐是从谷歌搜索中挑选出来的,我必须为这个例子学习。
自修改代码是直截了当的,如果您接受程序或至少只是两个简单的函数,编译然后反汇编,您将获得这些指令的操作码。或者使用nasm来编译汇编程序的块等。从此确定操作码将立即加载到eax然后返回。
理想情况下,您只需将这些字节放在某个ram中并执行该ram。要让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<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);
}
答案 1 :(得分:9)
由于您正在编写JIT编译器,您可能不希望自修改代码,您希望在运行时生成可执行代码。这是两件不同的事情。自修改代码是在已经开始运行后修改的代码。自修改代码在现代处理器上具有很大的性能损失,因此对于JIT编译器来说是不合需要的。
在运行时生成可执行代码应该是mmap()使用PROT_EXEC和PROT_WRITE权限的一些内存的简单问题。你也可以在你自己分配的一些内存上调用mprotect(),正如dwelch所做的那样。
答案 2 :(得分:3)
您还可以查看GNU lightning等项目。您为简化的RISC类型的机器提供代码,并动态生成正确的机器。
您应该考虑的一个非常现实的问题是与国外图书馆的接口。您可能需要至少支持一些系统级调用/操作才能使VM有用。 Kitsune的建议是让你考虑系统级调用的良好开端。您可能会使用mprotect来确保您修改的内存变得合法可执行。 (@KitsuneYMG)
允许调用用C编写的动态库的一些FFI应足以隐藏许多操作系统特定的细节。所有这些问题都会对您的设计产生相当大的影响,因此最好尽早开始考虑它们。
答案 3 :(得分:3)
基于上面示例的一个更简单的示例。感谢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);
}
答案 4 :(得分:2)
我正在开发一个自我修改的游戏来教授x86汇编,并且不得不解决这个确切的问题。我使用了以下两个库:
FASM汇编器https://github.com/ZenLulz/Fasm.NET
UDIS86反汇编程序:https://github.com/vmt/udis86
使用Udis86读取指令,用户可以将其编辑为字符串,然后使用FASM汇编新的字节。这些可以写回内存,并且正如其他用户指出的那样,写回要求在Windows上使用VirtualProtect或在Unix上使用mprotect。
代码示例对于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上运行
答案 5 :(得分:1)
这是用AT&amp; 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
答案 6 :(得分:0)
我从未编写自修改代码,尽管我对它的工作方式有基本的了解。基本上你在内存上写下你想要执行的指令然后跳转到那里。处理器将您编写指令的字节解释为(尝试)执行它们。例如,病毒和反复制程序可能会使用这种技术 关于系统调用,你是对的,参数通过寄存器传递。有关linux系统调用及其参数的参考,请检查here。