C:如何在运行时在我的程序中更改自己的程序?

问题描述 投票:-2回答:4

在运行时,汇编程序或机器代码(它是什么?)应该在RAM中的某个位置。我可以以某种方式访问​​它,读取甚至写入它吗?

这仅用于教育目的。

所以,我只能编译这段代码。我真的在这里读书吗?

#include <stdio.h>
#include <sys/mman.h>

int main() {
    void *p = (void *)main;
    mprotect(p, 4098, PROT_READ | PROT_WRITE | PROT_EXEC);
    printf("Main: %p\n Content: %i", p, *(int *)(p+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(p+i)) );
    }
}

但是,如果我补充一下

*(int*)p =4;

那是一个分段错误。


从答案中,我可以构造以下代码,在运行时修改它自己:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment) {
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

// pattern is a 0-terminated string
char* find(char *string, unsigned int stringLen, char *pattern) {
    unsigned int iString = 0;
    unsigned int iPattern;
    for (unsigned int iString = 0; iString < stringLen; ++iString) {
        for (iPattern = 0;
            pattern[iPattern] != 0
            && string[iString+iPattern] == pattern[iPattern];
            ++iPattern);
        if (pattern[iPattern] == 0) { return string+iString; }
    }
    return NULL;
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }

    // Correct a part of THIS program directly in RAM
    char programSubcode[12] = {'H','e','l','l','o',
                                ' ','W','o','r','l','t',0};
    char *programCode = (char *)main;
    char *helloWorlt = find(programCode, 1024, programSubcode);
    if (helloWorlt != NULL) {
        helloWorlt[10] = 'd';
    }   
    printf("Hello Worlt\n");
    return 0;
}

这真太了不起了!谢谢你们!

c self-reference self-modifying
4个回答
4
投票

机器代码被加载到内存中。从理论上讲,您可以像访问程序的任何其他内存部分一样读写它。

在实践中可能会遇到一些障碍。现代操作系统尝试将内存的数据部分限制为读/写操作但不执行,并且内存的机器代码部分可以读取/执行但不能写入。这是为了尝试限制潜在的安全漏洞,这些漏洞允许执行程序感觉放入内存的任何东西(例如它可能从Internet下载的随机内容)。

Linux提供mprotect系统调用,以允许一些自定义内存保护。 Windows提供SetProcessDEPPolicy系统调用。

编辑更新的问题

看起来你在Linux上尝试这个,并使用mprotect。您发布的代码不会检查mprotect的返回值,因此您不知道调用是成功还是失败。这是一个检查返回值的更新版本:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment)
{
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);

    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }
    printf("Main: %p\n Content: %i", main, *(int *)(main+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(main+i)) );
    }
}  

请注意传递给mprotect的length参数的更改以及将指针与系统页边界对齐的函数。您需要调查您的特定系统。我的系统有一个4096字节的对齐(通过运行getconf PAGE_SIZE确定)和对齐指针并将长度参数更改为mprotect到这个工作的页面大小,并允许您将指针写入main。

正如其他人所说,这是一种动态加载代码的坏方法。动态库或插件是首选方法。


6
投票

原则上,实际上您的操作系统可以保护自己免受危险代码的侵害!

在计算机拥有非常微小的记忆(20世纪50年代)的时代,自我修改代码可能被视为一种“巧妙的技巧”。它后来(当它不再需要时)被认为是不好的做法 - 导致代码很难维护和调试。

在更现代的系统中(在20世纪末),它成为一种表明病毒和恶意软件的行为。因此,所有现代桌面操作系统都不允许修改程序的代码空间,并且还阻止执行注入数据空间的代码。具有MMU的现代系统可以将存储器区域标记为只读,并且例如不可执行。

如何获取代码空间地址的简单问题 - 这很简单。例如,函数指针值通常是函数的地址:

int main()
{
    printf( "Address of main() = %p\n", (void*)main ) ;
}

另请注意,在现代系统中,此地址将是虚拟而非物理地址。


1
投票

实现此目的的最直接和实用的方法是使用函数指针。您可以声明一个指针,例如:

void (*contextual_proc)(void) = default_proc;

然后用语法contextual_proc();调用它。您也可以为contextual_proc分配一个具有相同签名的不同函数,例如contextual_proc = proc_that_logs;,然后调用contextual_proc()的任何代码(模数线程安全)将调用新代码。

这有点像自修改代码,但它更容易理解,可移植,并且实际上在可执行内存不可写且缓存指令的现代CPU上工作。

在C ++中,您可以使用子类;静态调度将在引擎盖下以相同的方式实现它。


0
投票

在大多数操作系统(Linux,Windows,Android,MacOSX等)上,一个程序不会(直接)在RAM中执行,但它有virtual address space并在其中运行(严格意义上,代码不是 - 总是或必然 - 在RAM中;在某些页面错误将其透明地放入RAM中后,您可以拥有不在RAM中并执行的代码。 RAM由(直接)由操作系统管理,但是你的process只能看到它的虚拟地址空间(在execve(2)时间初始化并用mmap(2)munmapmprotectmlock(2)修改......)。使用proc(5)并在Linux shell中尝试cat /proc/$$/maps以了解更多shell进程的虚拟地址空间。在Linux上,您可以通过读取/proc/self/maps文件来查询进程的虚拟地址空间(顺序地,它是文本伪文件)。

阅读Operating Systems: Thee Easy Pieces以了解有关操作系统的更多信息。


实际上,如果你想增加程序中的代码(在一些常见的操作系统上运行),你最好使用pluginsdynamic loading工具。在Linux和POSIX系统上,你将使用dlopen(3)(使用mmap等...)然后使用dlsym(3)你将获得一些新函数的(虚拟)地址,你可以调用它(通过将它存储在你的某个函数指针中) C代码)。

你没有真正定义一个程序是什么。我声称一个程序不仅是一个可执行文件,而且还由其他资源(如特定库,可能是字体或配置文件等)组成,这就是为什么当你安装一些程序时,通常比移动或复制可执行文件(查看make install对大多数自由软件程序的作用,即使像GNU coreutils一样简单)。因此,一个程序(在Linux上)生成一些C代码(例如在一些临时文件/tmp/genecode.c中),将C代码编译成插件/tmp/geneplug.so(通过运行gcc -Wall -O -fPIC /tmp/genecode.c -o /tmp/geneplug.so),然后dlopen /tmp/geneplug.so插件真正修改自己。如果您使用C语言进行编码,这是一种编写自修改程序的理智方式。

通常,您的机器代码位于code segment中,并且该代码段是只读的(有时甚至是仅执行;读取有关NX bit的内容)。如果你真的想要覆盖代码(而不是扩展代码),你需要使用工具(可能是Linux上的mprotect(2))来更改权限并在代码段内启用重写。

一旦代码段的某些部分可写,您就可以覆盖它。

还要考虑一些JIT-compiling库,例如libgccjitasmjit(以及其他),以在内存中生成机器代码。

当你为一个新的可执行文件添加execve时,它的大部分代码都还没有(仍然)放在RAM中。但是(从应用程序中的用户代码的角度来看)你可以运行它(并且内核将透明地,但懒洋洋地将代码页带入RAM,通过demand paging)。这就是我试图通过说你的程序在其虚拟地址空间(而不是直接在RAM中)运行来解释的。需要整本书来进一步解释。

例如,如果您有一个巨大的可执行文件(为简单起见,假设它是静态链接的),则为一千兆字节。当您启动该可执行文件(使用execve)时,整个千兆字节不会被带入RAM。如果您的程序快速退出,大部分千兆字节都没有被带入RAM并保留在磁盘上。即使你的程序运行了很长时间,但从不调用一百兆字节的代码,那个代码部分(从未使用的例程的100Mbyte)也不会在RAM中。


BTW,stricto sensu,self modifying code这些天很少使用(当前的处理器甚至不能有效地处理,例如因为缓存和分支预测器)。因此在实践中,您不会完全修改您的机器代码(即使这是可能的)。

并且malware不必修改当前执行的代码。它可以(并且经常会)在内存中注入新代码并以某种方式跳转到它(更确切地说,通过某个函数指针调用它)。因此,一般情况下,您不会覆盖现有的“主动使用”代码,而是在其他地方创建新代码,然后调用它或跳转到它。

如果你想在C的其他地方创建新代码,插件工具(例如Linux上的dlopendlsym)或JIT库就足够了。

请注意,提及“更改程序”或“编写代码”在您的问题中非常模糊。

您可能只想扩展程序的代码(然后使用插件技术或JIT编译库是相关的)。请注意,某些程序(例如SBCL)能够在每次用户交互时生成机器代码。

你可以改变程序的现有代码,但是你应该解释一下这究竟意味着什么(“代码”对你来说意味着什么?它只是当前执行的机器指令还是你程序的整个代码段?) 。您是否考虑过dynamic software updating的自修改代码,生成新代码?

我可以以某种方式访问​​它,读取甚至写入它吗?

当然是的。您需要在代码的虚拟地址空间中更改保护(例如使用mprotect),然后在某些“旧代码”部分上写入许多字节。你为什么要这样做是一个不同的故事(你没有解释原因)。我没有看到任何教育目的 - 你可能会很快崩溃你的程序(除非你采取了很多预防措施在内存中写出足够好的机器代码)。

我是metaprogramming的忠实粉丝,但我通常会生成一些新代码并跳入其中。在我们当前的机器上,我认为覆盖现有代码没有任何价值。而且(在Linux上),我的manydl.c程序演示了你可以在一个程序中生成C代码,编译和动态链接超过一百万个插件(以及dlopen所有这些插件)。实际上,在当前的笔记本电脑或台式电脑上,您可以生成大量新代码(在受到限制之前)。并且C足够快(在编译时和运行时),你可以在每次用户交互时生成数千个C行(每秒几次),编译并动态加载它(我十年前在我的已解散的GCC MELT项目)。

如果你想覆盖磁盘上的executable文件(我认为这样做没有价值,创建新的可执行文件要简单得多),你需要深入了解它们的结构。对于Linux,请深入了解ELF的规格。


在编辑的问题中,你忘了测试mprotect的失败。它可能是失败的(因为4098不是2的幂和页面倍数)。所以请至少代码:

int c = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
if (c) { perror("mprotect"); exit(EXIT_FAILURE); };

即使有了4096(而不是4098)mprotect可能会因EINVAL而失败,因为main可能没有与4K页面对齐。 (不要忘记您的可执行文件还包含crt0代码)。

顺便说一句,出于教育目的,您应该在main的开头附近添加以下代码:

 char cmdbuf[80];
 snprintf (cmdbuf, sizeof(cmdbuf), "/bin/cat /proc/%d/maps", (int)getpid());
 fflush(NULL);
 if (system(cmdbuf)) 
   { fprintf(stderr, "failed to run %s\n", cmdbuf); exit(EXIT_FAILURE));

你可以在最后添加一个类似的代码块。您可以使用snprintf替换cmdbuf"pmap %d"格式字符串。

© www.soinside.com 2019 - 2024. All rights reserved.