【问题标题】:How to write self-modifying code in x86 assembly如何在 x86 汇编中编写自修改代码
【发布时间】:2011-06-16 07:40:40
【问题描述】:

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

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

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

我正在寻找的是自修改程序代码中最简单、最直接的示例。我可以查看并使用它来了解如何编写 x86 程序集中的自修改代码,以及它是如何工作的。你有什么资源可以指点我,或者你可以举出任何可以充分证明这一点的例子吗?

我使用 NASM 作为我的汇编程序。

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

【问题讨论】:

  • linux.die.net/man/2/mprotect 应该解释 mprotect 的参数是什么。要调用的函数 ID 在 EAX 中传递,下一个参数在 EBX ECX 和 EDX 中传递。

标签: assembly x86 jit vm-implementation self-modifying


【解决方案1】:

哇,结果比我预期的要痛苦得多。 100% 的痛苦是 linux 保护程序不被覆盖和/或执行数据。

如下所示的两种解决方案。并且涉及到大量的谷歌搜索,所以有些简单的放置一些指令字节并执行它们是我的,mprotect 和页面大小对齐是从谷歌搜索中挑选出来的,这是我必须为这个例子学习的东西。

自修改代码是直截了当的,如果您使用程序或至少只是两个简单的函数,编译然后反汇编您将获得这些指令的操作码。或使用 nasm 编译汇编程序块等。由此我确定操作码将立即数加载到 eax 然后返回。

理想情况下,您只需将这些字节放入某个 ram 中并执行该 ram。要让 linux 做到这一点,您必须更改保护,这意味着您必须向它发送一个在 mmap 页面上对齐的指针。因此,分配比您需要的更多的内存,在页面边界上的该分配中找到对齐的地址,并从该地址 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&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);
}

【讨论】:

  • 不仅 Linux,大多数现代操作系统也保护可写内存不被执行
  • 这可以在 Windows 中完成,即取消对 RAM 页面的保护,或者我们会遇到蓝屏死机吗?我想用这种方法创建一个自修改加密系统。
  • 该代码在 32 位 Arch Linux 上运行良好,但在 64 位 RHEL 上失败(当然是 64 位 ELF,但在使用 32 位 ELF 时也是如此)。不知道这是否与 RHEL 或其他方面的额外内存保护有关。输出为:``` 0x9a00008 0x9a01000 mprotect failed ```
  • 这不是 self 修改代码,它只是普通的 JIT 进入缓冲区。那些mov-immediate 指令不会修改它们自己的指令字节。
  • @Alexander mprotect 失败,因为 64 位的页面对齐计算中断。你需要long pagesize。否则地址是 64 位值,使用的位掩码是 32 位。
【解决方案2】:

由于您正在编写 JIT 编译器,您可能不希望 自我修改 代码,而希望在运行时生成可执行代码。这是两个不同的东西。自修改代码是在已经开始运行之后修改的代码。自修改代码对现代处理器有很大的性能损失,因此对于 JIT 编译器来说是不可取的。

在运行时生成可执行代码应该很简单,只需使用 PROT_EXEC 和 PROT_WRITE 权限对一些内存进行 mmap() 操作。你也可以在你自己分配的一些内存上调用 mprotect(),就像上面 dwelch 所做的那样。

【讨论】:

  • 自我修改代码并不总是会对现代处理器造成性能损失。你必须小心你所做的改变,并确保 CPU 缓存是同步的并且分支保护没有被改变。改变这些会降低你的表现。
  • 如果自修改发生的频率相对较低和/或发生在当前未执行的部分代码上,临时性能影响可以忽略不计吗?
  • @ErikAllik:是的,这是一次性的命中,需要花费管道刷新,可能与内存顺序错误推测的成本相似。可能有数百个周期,因此很容易通过重复使用更新的代码来摊销。
【解决方案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);
}

【讨论】:

  • 如果您不为hola() 生成与位置无关的代码,这可能会严重失败。
【解决方案4】:

我正在开发一个自修改游戏来教授 x86 汇编,并且必须解决这个确切的问题。我使用了以下三个库:

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

一个正常运行的 repo 在这里(非常轻量级):

https://github.com/Squalr/SelfHackingApp

【讨论】:

    【解决方案5】:

    您还可以查看 GNU lightning 之类的项目。你给它一个简化的 RISC 类型机器的代码,它会动态生成正确的机器。

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

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

    【讨论】:

      【解决方案6】:

      这是用 AT&T 汇编编写的。从程序的执行中可以看出,由于代码自修改,输出发生了变化。

      编译:gcc -m32 modify.s modify.c

      使用 -m32 选项是因为该示例适用于 32 位机器

      组装:

      .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
      

      【讨论】:

      • 你可以放弃pushl %ebpmovl %esp,%ebppopl %ebp
      【解决方案7】:

      我从未编写过自修改代码,尽管我对它的工作原理有基本的了解。基本上,您将要执行的指令写入内存,然后跳转到那里。处理器解释您编写的指令并(尝试)执行它们的那些字节。例如,病毒和反复制程序可能会使用这种技术。
      关于系统调用,你是对的,参数是通过寄存器传递的。有关 linux 系统调用及其参数的参考,请查看here

      【讨论】:

        猜你喜欢
        • 2012-05-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多