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

时间:2018-09-08 18:57:24

标签: c self-reference self-modifying

在运行时,汇编代码或机器代码(是吗?)应该位于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;
}

这太神奇了!谢谢大家!

4 个答案:

答案 0 :(得分:6)

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

在计算机内存很小的时代(1950年代),自我修改代码可能被视为“巧妙的技巧”。后来(不再需要时)被认为是不好的做法-导致难以维护和调试的代码。

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

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

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

还要注意,在现代系统上,此地址将是虚拟地址,而不是物理地址。

答案 1 :(得分: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确定),在对齐指针并将length参数更改为mprotect至页面大小后,此方法有效,并允许您将指针覆盖到main

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

答案 2 :(得分:1)

完成此操作的最直接,最实用的方法是使用函数指针。您可以声明一个指针,例如:

void (*contextual_proc)(void) = default_proc;

然后使用语法contextual_proc();进行调用。您还可以为contextual_proc分配一个具有相同签名的不同功能,例如contextual_proc = proc_that_logs;,然后任何调用contextual_proc()的代码都将(模安全性)调用新代码。 / p>

这实际上就像是自我修改的代码,但是它易于理解,可移植,并且实际上可在无法写入可执行内存和缓存指令的现代CPU上工作。

在C ++中,您将为此使用子类。静态分派将在后台以相同的方式实现它。

答案 3 :(得分:0)

在大多数操作系统(Linux,Windows,Android,MacOSX等)上,程序不会(直接)在RAM中执行,但是具有其virtual address space并在其中运行(stricto sensu,代码并非总是或不一定在RAM中;您可以让代码不在RAM中并在某些页面错误后将其透明地放入RAM中而执行。 RAM由操作系统直接管理,但是您的process仅看到其虚拟地址空间(在execve(2)时间初始化,并用mmap(2)munmap,{{ 1}},mlock(2) ...)。使用proc(5)并在Linux Shell中尝试mprotect可以进一步了解Shell进程的虚拟地址空间。在Linux上,您可以通过读取cat /proc/$$/maps文件(依次为文本伪文件)来查询进程的虚拟地址空间。

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


在实践中,如果要在程序中扩展代码(在某些常见的OS上运行),则最好使用pluginsdynamic loading工具。在Linux和POSIX系统上,您将使用dlopen(3)(使用/proc/self/maps等),然后使用dlsym(3),您将获得一些新功能的(虚拟)地址,并且可以调用它(通过将其存储在C代码的某些函数指针中)。

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

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

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

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

当您dlopen新建一个新的可执行文件时,其大部分代码(尚未)位于RAM中。但是(从应用程序中用户代码的角度来看),您可以运行它(内核将透明地(但懒惰地)通过demand paging将代码页带入RAM)。这就是我试图通过说您的程序在其虚拟地址空间(而不是直接在RAM中)中运行来解释的原因。需要整本书来进一步解释。

例如,如果您有一个1 GB的巨大可执行文件(为简单起见,假定它是静态链接的)。当您启动该可执行文件(使用/tmp/geneplug.so)时,整个GB都将被 not 带入RAM。如果您的程序迅速退出,则大多数千兆字节尚未被带入RAM并停留在磁盘上。即使您的程序运行了很长时间,但从未调用过一个数百兆代码的庞大例程,该代码部分(从未使用过的例程的100 MB)也不会位于RAM中。


BTW,严格意义上的self modifying code如今很少使用(并且当前处理器甚至由于缓存和分支预测器的原因而无法有效地处理该问题)。因此,在实践中,您不会精确地修改您的机器代码(即使可能的话)。

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

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

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

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

您可以更改程序的现有代码,但是然后您应该解释一下这到底意味着什么(“代码”对您来说完全意味着什么?当前执行的机器指令还是程序的整个代码段?)。您认为dynamic software updating的自修改代码,生成新代码吗?

  

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

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

我是metaprogramming的忠实粉丝,但我通常会生成一些 new 代码并加入其中。在当前的计算机上,覆盖现有代码没有任何价值。并且(在Linux上),我的manydl.c程序演示了您可以在单个程序中生成C代码,编译并动态链接超过一百万个插件(以及dlsym所有插件)。实际上,在当前的便携式计算机或台式计算机上,您可以生成许多新代码(在受到限制之前)。 C足够快(在编译时和运行时),您可以在每次用户交互时生成数千条C行(每秒数次),进行编译和动态加载(我十年前就这样做了)已终止的GCC MELT项目)。

如果要覆盖磁盘上的executable文件(这样做没有任何价值,创建 fresh 可执行文件要简单得多),则需要深入了解它们的结构。对于Linux,请深入研究ELF的规范。


在已编辑的问题中,您忘记测试mprotect是否失败。它可能失败了(因为4098不是2的幂和一页的倍数)。因此,请至少输入代码:

dlopen

即使使用4096(而不是4098),mprotect也可能因int c = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC); if (c) { perror("mprotect"); exit(EXIT_FAILURE); }; 而失败,因为mprotect可能未与4K页面对齐。 (不要忘记您的可执行文件还包含crt0代码)。

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

main

,您可以在末尾添加类似的代码块。您可以将main的{​​{1}}格式字符串替换为 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));