assembly 如何在x86汇编中编写自修改代码

jfgube3f  于 2023-10-19  发布在  其他
关注(0)|答案(8)|浏览(122)

我正在考虑为我最近一直在做的一个业余虚拟机写一个JIT编译器。我知道一点汇编,(我主要是C程序员。我可以阅读大多数汇编,并参考我不理解的操作码,并编写一些简单的程序。)但我很难理解我在网上找到的几个自修改代码的例子。
这是一个这样的例子:http://asm.sourceforge.net/articles/smc.html
所提供的示例程序在运行时会进行大约四种不同的修改,其中没有一种是明确解释的。Linux内核中断被多次使用,并且没有解释或详细说明。(作者在调用中断之前将数据移入几个寄存器。我假设他是在传递参数,但这些参数根本没有解释,让读者去猜测。)
我在寻找的是一个最简单,最直接的例子,在代码中的自我修改程序。我可以查看并使用它来理解x86汇编中的自修改代码是如何编写的,以及它是如何工作的。有没有什么资源你可以给我,或者任何例子,你可以给予,将充分证明这一点?
我使用NASM作为我的汇编程序。
编辑:我也在Linux上运行这段代码。

6g8kf2rb

6g8kf2rb1#

哇,这比我想象的要痛苦得多。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);
}
x759pob2

x759pob22#

因为你在写一个JIT编译器,你可能不想要自我修改的代码,你想在运行时生成可执行的代码。这是两码事。自修改代码是指 * 在已经开始运行 * 之后被修改的代码。自修改代码在现代处理器上有很大的性能损失,因此对于JIT编译器来说是不可取的。
在运行时生成可执行代码应该是一个简单的问题,mmap()使用PROT_EXEC和PROT_WRITE权限访问一些内存。你也可以在你自己分配的一些内存上调用mprotect(),就像上面的dwelch所做的那样。

q35jwt9p

q35jwt9p3#

一个简单的例子,基于上面的例子。感谢davich帮助了很多。

#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);
}
amrnrhlw

amrnrhlw4#

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

ogq8wdun5#

我正在开发一个自我修改的游戏来教x86汇编,并且必须解决这个确切的问题。我使用了以下三个库:
AsmJit + AsmTk用于组装:https://github.com/asmjit/asmjit + https://github.com/asmjit/asmtk UDIS 86用于拆卸:https://github.com/vmt/udis86
使用Udis 86读取指令,用户可以将其编辑为字符串,然后使用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

pvcm50d1

pvcm50d16#

你也可以看看像GNU lightning这样的项目。你给予它一个简化的RISC类型的机器的代码,它会动态地生成正确的机器。
你应该考虑的一个非常真实的问题是与外国图书馆的接口。您可能需要至少支持一些系统级调用/操作,以使您的VM有用。Kitsune的建议是一个很好的开始,可以让你考虑系统级调用。您可能会使用mprotect来确保您修改的内存可以合法地执行。(@KitsuneYMG)
一些允许调用用C编写的动态库的FFI应该足以隐藏许多操作系统特定的细节。所有这些问题都会对您的设计产生相当大的影响,因此最好尽早开始考虑它们。

bis0qfac

bis0qfac7#

这个问题被标记为“assembly”和“x86”,但没有标记为“C”。虽然问这个问题的人提到他们主要使用C语言,但这个问题很可能会被寻找纯汇编解决方案的人(包括我过去)遇到。因此,这是我试图以最简单的方式演示JIT程序的尝试,它受到old_timer的答案的启发,但用纯汇编重写。

.bss
.align 4096 # page size on my machine. You can automate this process using
            # libc's getpagesize() to make it bit more portable, but hey!,
            # this is a minimum viable product!
exec: 
    .skip 10000



.text
mprotectoutput: .asciz "mprotect output value %d\n"

.global main
main:
    # prologue
    pushq %rbp
    movq %rsp, %rbp

    # body
    movq $exec, %rdi
    movq $10000, %rsi
    movq $7, %rdx
    call mprotect

    # print output from the mprotect function. If other than 0, the code will
    # segfault on `jmp *%rax`.
    movq $mprotectoutput, %rdi
    movq %rax, %rsi
    xor %rax, %rax
    call printf

    # the subroutine will move 0x45 to %rax, the return to the address
    # in register %r15

    # set the return address
    movq $back, %r15

    # rdi will be a counter that counts how many program bytes were written
    xor %rdi, %rdi
    # 48 c7 c0 45 00 00 00  mov    $0x45,%rax
    movq $0x0000000045c0c748, %rax
    movq %rax, exec(%rdi)
    addq $7, %rdi
    # 41 ff e7              jmp    *%r15
    movl $0x00e7ff41, %eax
    movl %eax, exec(%rdi)
    addq $3, %rdi

    movq $exec, %rax
    jmp *%rax

back:
    # epilogue
    movq %rbp, %rsp
    popq %rbp
    ret
nszi6y05

nszi6y058#

我从来没有写过自我修改的代码,尽管我对它的工作原理有一个基本的了解。基本上,你在内存中写入你想要执行的指令,然后跳转到那里。处理器解释你写的指令的字节并尝试执行它们.例如,病毒和反拷贝程序可能使用这种技术。
关于系统调用,你是对的,参数通过寄存器传递。对于Linux系统调用及其参数的参考,只需检查here

相关问题