57

最近取り組んでいる趣味の仮想マシン用のJITコンパイラを作成することを検討しています。私は少しアセンブリを知っています(私は主にCプログラマーです。理解できないオペコードを参照してほとんどのアセンブリを読み、いくつかの簡単なプログラムを書くことができます)が、いくつかの例を理解するのに苦労しています。私がオンラインで見つけた自己修正コードの。

これはそのような例の1つです:http://asm.sourceforge.net/articles/smc.html

提供されているサンプルプログラムは、実行時に約4つの異なる変更を行いますが、いずれも明確に説明されていません。Linuxカーネル割り込みは数回使用され、説明も詳細もされていません。(作成者は、割り込みを呼び出す前にデータをいくつかのレジスタに移動しました。彼は引数を渡していたと思いますが、これらの引数はまったく説明されておらず、読者に推測を任せています。)

私が探しているのは、自己変更プログラムのコードの中で最も単純で最も単純な例です。私が見て、x86アセンブリの自己変更コードをどのように記述しなければならないか、そしてそれがどのように機能するかを理解するために使用できるもの。あなたが私に指摘できるリソース、またはこれを適切に示すことができるあなたが与えることができる例はありますか?

私はアセンブラとしてNASMを使用しています。

編集:私はLinuxでもこのコードを実行しています。

4

7 に答える 7

60

うわー、これは私が予想していたよりもはるかに苦痛であることが判明しました。苦痛の100%は、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&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);
}
于 2011-01-27T16:38:33.320 に答える
12

JITコンパイラを作成しているので、自己変更コードはおそらく必要ありません。実行時に実行可能コードを生成する必要があります。これらは2つの異なるものです。自己変更コードは、実行を開始した後に変更されるコードです。自己変更コードは、最新のプロセッサではパフォーマンスが大幅に低下するため、JITコンパイラには望ましくありません。

実行時に実行可能コードを生成するには、PROT_EXECおよびPROT_WRITE権限でメモリをmmap()するだけです。上記のdwelchのように、自分で割り当てたメモリでmprotect()を呼び出すこともできます。

于 2011-01-30T07:48:37.457 に答える
4

上記の例に基づいた、もう少し簡単な例。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);
}
于 2012-04-22T08:58:05.693 に答える
4

私はx86アセンブリを教えるために自己修正ゲームに取り組んでおり、この正確な問題を解決する必要がありました。次の3つのライブラリを使用しました。

組み立て用のAsmJit+AsmTk:https ://github.com/asmjit/asmjit + https://github.com/asmjit/asmtk 分解用のUDIS86:https ://github.com/vmt/udis86

命令はUdis86で読み取られ、ユーザーはそれらを文字列として編集できます。次に、AsmJit/AsmTkを使用して新しいバイトをアセンブルします。これらはメモリに書き戻すことができ、他のユーザーが指摘しているように、書き戻しにはWindowsのVirtualProtectまたはUnixのmprotectを使用してメモリページのアクセス許可を修正する必要があります。

コードサンプルはStackOverflowの場合は少し長いので、コードサンプルを使用して作成した記事を参照します。

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

機能しているリポジトリはここにあります(非常に軽量):

https://github.com/Squalr/SelfHackingApp

于 2018-08-31T04:57:00.880 に答える
3

GNUlightningのようなプロジェクトも見ることができます。簡略化されたRISCタイプのマシンのコードを指定すると、正しいマシンが動的に生成されます。

あなたが考えるべき非常に現実的な問題は、外国の図書館とのインターフェースです。VMを使用するには、少なくともいくつかのシステムレベルの呼び出し/操作をサポートする必要があります。Kitsuneのアドバイスは、システムレベルの呼び出しについて考えるための良いスタートです。おそらくmprotectを使用して、変更したメモリが合法的に実行可能になるようにします。(@KitsuneYMG)

Cで記述されたダイナミックライブラリへの呼び出しを許可する一部のFFIは、OS固有の詳細の多くを隠すのに十分なはずです。これらの問題はすべて設計に大きな影響を与える可能性があるため、早めに検討を開始することをお勧めします。

于 2011-01-27T07:18:50.320 に答える
3

これはAT&Tアセンブリで書かれています。プログラムの実行からわかるように、自己変更コードのために出力が変更されています。

コンパイル:gcc -m32 modify.s modify.c

この例は32ビットマシンで動作するため、-m32オプションが使用されます

アセンブリ:

.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
于 2016-05-21T18:27:54.543 に答える
0

私は自己変更コードを書いたことがありませんが、それがどのように機能するかについての基本的な理解はあります。基本的には、実行したい命令をメモリに書き込んでから、そこにジャンプします。プロセッサは、命令を書き込んだバイトを解釈し、それらを実行しようとします。たとえば、ウイルスやコピー防止プログラムがこの手法を使用する場合があります。
システムコールに関しては、あなたは正しかった、引数はレジスタを介して渡される。Linuxシステムコールとその引数のリファレンスについては、こちらを確認してください。

于 2011-01-27T13:18:27.960 に答える