首页 文章

如何在x86程序集中编写自修改代码

提问于
浏览
43

我正在为最近一直在研究的业余爱好虚拟机编写JIT编译器 . 我知道有点汇编,(我主要是一个C程序员 . 我可以阅读大多数汇编参考我不理解的操作码,并编写一些简单的程序 . )但我很难理解这几个例子我在网上找到的自修改代码 .

这是一个这样的例子:http://asm.sourceforge.net/articles/smc.html

提供的示例程序在运行时进行了大约四种不同的修改,其中没有一个被清楚地解释 . Linux内核中断被多次使用,没有解释或详细说明 . (作者在调用中断之前将数据移动到几个寄存器中 . 我假设他正在传递参数,但这些参数根本没有解释,让读者猜测 . )

我正在寻找的是自修改程序代码中最简单,最直接的例子 . 我可以看到的东西,用于理解如何编写x86程序集中的自修改代码,以及它是如何工作的 . 您是否有任何资源可以指向我,或者您可以提供的任何示例都能充分证明这一点?

我正在使用NASM作为我的汇编程序 .

编辑:我也在Linux上运行此代码 .

7 回答

  • 45

    哇,这比我想象的要痛苦得多 . 100%的痛苦是linux保护程序不被覆盖和/或执行数据 .

    两个解决方案如下所示并且涉及了很多谷歌搜索,所以有点简单地把一些指令字节和执行它们是我的,在页面大小上的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&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);
    }
    
  • 2

    由于您不想要自修改代码,因此您希望在运行时生成可执行代码 . 这是两件不同的事情 . 自修改代码是在已经开始运行后修改的代码 . 自修改代码在现代处理器上具有很大的性能损失,因此对于JIT编译器来说是不合需要的 .

    在运行时生成可执行代码应该是mmap()使用PROT_EXEC和PROT_WRITE权限的一些内存的简单问题 . 你也可以在你自己分配的一些内存上调用mprotect(),正如dwelch所做的那样 .

  • 1

    您还可以查看GNU lightning等项目 . 您为简化的RISC类型机器提供代码,并动态生成正确的机器 .

    您应该考虑的一个非常现实的问题是与外国图书馆的接口 . 您可能需要至少支持一些系统级调用/操作才能使VM有用 . Kitsune的建议是让你考虑系统级调用的良好开端 . 您可能会使用mprotect来确保您修改的内存变得合法可执行 . (@KitsuneYMG)

    允许调用用C编写的动态库的一些FFI应足以隐藏许多操作系统特定的细节 . 所有这些问题都会对您的设计产生相当大的影响,因此最好尽早开始考虑它们 .

  • 0

    基于上面的例子,一个简单的例子 . 感谢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);
    }
    
  • 3

    我正在研究一种自我修改游戏来教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 repo在这里(非常轻量级):

    https://github.com/Squalr/SelfHackingApp

    这些示例都在Windows上,但这只是将 VirtualProtect 换成 mprotect 以使其适用于Linux的问题

  • 9

    这是用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
    

    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
    
  • 3

    我've never written self-modifying code, although I have a basic understanding about how it works. Basically you write on memory the instructions you want to execute then jump there. The processor interpret those bytes you'已经写了一个指令和(尝试)来执行它们 . 例如,病毒和反复制程序可能使用此技术 .
    关于系统调用,你是对的,参数通过寄存器传递 . 有关linux系统调用及其参数的参考,请查看here .

相关问题