最近取り組んでいる趣味の仮想マシン用のJITコンパイラーを書いています。少しアセンブリを知っています(私は主にCプログラマーです。ほとんどのアセンブリを理解できないオペコードを参照して読むことができ、いくつかの簡単なプログラムを書くことができます)。しかし、いくつかの例を理解するのに苦労していますオンラインで見つけた自己修正コードのセット。
これはそのような例の1つです。 http://asm.sourceforge.net/articles/smc.html
提供されているサンプルプログラムは、実行時に約4つの異なる変更を行いますが、どれも明確に説明されていません。 Linuxカーネル割り込みは数回使用され、説明も詳細もされていません。 (著者は割り込みを呼び出す前にデータをいくつかのレジスタに移動しました。私は彼が引数を渡していたと思いますが、これらの引数はまったく説明されていないため、読者は推測する必要があります。)
私が探しているのは、自己変更プログラムのコードの最も単純で最も簡単な例です。私が見て、x86アセンブリの自己変更コードをどのように記述する必要があるか、およびそれがどのように機能するかを理解するために使用できるもの。私に指摘できるリソース、またはこれを適切に実証するために提供できる例はありますか?
NASMをアセンブラとして使用しています。
編集:私はLinuxでもこのコードを実行しています。
うわー、これは思ったよりずっと痛いことがわかった。痛みの100%は、プログラムが上書きされたり、データが実行されたりしないようにLinuxを保護するLinuxでした。
以下に示す2つのソリューション。そして、グーグルの多くが関わっていたので、いくらか単純ないくつかの命令バイトを入れてそれらを実行しましたが、mprotectとページサイズの調整は、グーグル検索から除外されました。
自己修正コードは単純です。プログラムまたは少なくとも2つの単純な関数を使用してコンパイルし、逆アセンブルすると、これらの命令のオペコードが取得されます。または、nasmを使用して、アセンブラーのブロックをコンパイルします。これから、eaxに即値をロードして戻るためのオペコードを決定しました。
理想的には、それらのバイトをいくつかのRAMに入れ、そのRAMを実行するだけです。 Linuxでそれを行うには、保護を変更する必要があります。つまり、mmapページに配置されたポインターをLinuxに送信する必要があります。したがって、必要以上に割り当て、ページ境界にある割り当て内の境界整列されたアドレスを見つけ、そのアドレスからmprotectし、そのメモリを使用してオペコードを配置してから実行します。
2番目の例では、プログラムにコンパイルされた既存の関数を使用します。保護メカニズムのため、単純にそれをポイントしてバイトを変更することはできないため、書き込みから保護を解除する必要があります。したがって、前のページ境界にバックアップして、変更するコードをカバーするのに十分なバイト数とそのアドレスで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コンパイラーを作成しているため、おそらくself-modifyingコードは必要ありません。実行時に実行可能コードを生成する必要があります。これらは2つの異なるものです。自己変更コードは、変更されたコードですすでに実行を開始した後。自己変更コードは、最新のプロセッサではパフォーマンスが大幅に低下するため、JITコンパイラには望ましくありません。
実行時に実行可能コードを生成することは、PROT_EXECおよびPROT_WRITEパーミッションでメモリをmmap()することの簡単な問題です。上記のdwelchのように、自分で割り当てたメモリでmprotect()を呼び出すこともできます。
GNU lightning のようなプロジェクトも確認できます。単純化されたRISCタイプのマシンのコードを指定すると、正しいマシンが動的に生成されます。
あなたが考えるべき非常に現実的な問題は、外国の図書館とのインターフェースです。 VMを使用するには、少なくともいくつかのシステムレベルの呼び出し/操作をサポートする必要があります。キツネのアドバイスは、システムレベルの呼び出しについて考え始めるのに良いスタートです。 mprotectを使用して、変更したメモリが合法的に実行可能になるようにします(@KitsuneYMG)
Cで記述された動的ライブラリの呼び出しを許可するFFIは、OS固有の詳細の多くを隠すのに十分です。これらの問題はすべて、設計にかなりの影響を与える可能性があるため、早期に検討することをお勧めします。
上記の例に基づいた少し簡単な例。 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);
}
これはAT&Tアセンブリで記述されています。プログラムの実行からわかるように、自己変更コードのために出力が変更されています。
コンパイル:gcc -m32 modify.s modify.c
この例は32ビットマシンで動作するため、-m32オプションが使用されます。
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
私はx86アセンブリを教えるための自己修正ゲームに取り組んでおり、この正確な問題を解決する必要がありました。次の2つのライブラリを使用しました。
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上にありますが、Linuxでこれを機能させるには、VirtualProtect
をmprotect
にスワップアウトするだけです。
自己変更コードを記述したことはありませんが、その仕組みについては基本的には理解しています。基本的に、実行したい命令をメモリに書き込み、そこにジャンプします。プロセッサは、命令を書き込んだバイトを解釈し、それらを実行しようとします。たとえば、ウイルスやコピー防止プログラムがこの手法を使用する場合があります。
システムコールについては、正解でした。引数はレジスタを介して渡されます。 Linuxシステムコールとその引数のリファレンスについては、 here を確認してください。