在运行时,汇编程序或机器代码(它是什么?)应该在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;
}
这真太了不起了!谢谢你们!
机器代码被加载到内存中。从理论上讲,您可以像访问程序的任何其他内存部分一样读写它。
在实践中可能会遇到一些障碍。现代操作系统尝试将内存的数据部分限制为读/写操作但不执行,并且内存的机器代码部分可以读取/执行但不能写入。这是为了尝试限制潜在的安全漏洞,这些漏洞允许执行程序感觉放入内存的任何东西(例如它可能从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。
正如其他人所说,这是一种动态加载代码的坏方法。动态库或插件是首选方法。
原则上,实际上您的操作系统可以保护自己免受危险代码的侵害!
在计算机拥有非常微小的记忆(20世纪50年代)的时代,自我修改代码可能被视为一种“巧妙的技巧”。它后来(当它不再需要时)被认为是不好的做法 - 导致代码很难维护和调试。
在更现代的系统中(在20世纪末),它成为一种表明病毒和恶意软件的行为。因此,所有现代桌面操作系统都不允许修改程序的代码空间,并且还阻止执行注入数据空间的代码。具有MMU的现代系统可以将存储器区域标记为只读,并且例如不可执行。
如何获取代码空间地址的简单问题 - 这很简单。例如,函数指针值通常是函数的地址:
int main()
{
printf( "Address of main() = %p\n", (void*)main ) ;
}
另请注意,在现代系统中,此地址将是虚拟而非物理地址。
实现此目的的最直接和实用的方法是使用函数指针。您可以声明一个指针,例如:
void (*contextual_proc)(void) = default_proc;
然后用语法contextual_proc();
调用它。您也可以为contextual_proc
分配一个具有相同签名的不同函数,例如contextual_proc = proc_that_logs;
,然后调用contextual_proc()
的任何代码(模数线程安全)将调用新代码。
这有点像自修改代码,但它更容易理解,可移植,并且实际上在可执行内存不可写且缓存指令的现代CPU上工作。
在C ++中,您可以使用子类;静态调度将在引擎盖下以相同的方式实现它。
在大多数操作系统(Linux,Windows,Android,MacOSX等)上,一个程序不会(直接)在RAM中执行,但它有virtual address space并在其中运行(严格意义上,代码不是 - 总是或必然 - 在RAM中;在某些页面错误将其透明地放入RAM中后,您可以拥有不在RAM中并执行的代码。 RAM由(直接)由操作系统管理,但是你的process只能看到它的虚拟地址空间(在execve(2)时间初始化并用mmap(2),munmap
,mprotect
,mlock(2)修改......)。使用proc(5)并在Linux shell中尝试cat /proc/$$/maps
以了解更多shell进程的虚拟地址空间。在Linux上,您可以通过读取/proc/self/maps
文件来查询进程的虚拟地址空间(顺序地,它是文本伪文件)。
阅读Operating Systems: Thee Easy Pieces以了解有关操作系统的更多信息。
实际上,如果你想增加程序中的代码(在一些常见的操作系统上运行),你最好使用plugins和dynamic 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库,例如libgccjit或asmjit(以及其他),以在内存中生成机器代码。
当你为一个新的可执行文件添加execve
时,它的大部分代码都还没有(仍然)放在RAM中。但是(从应用程序中的用户代码的角度来看)你可以运行它(并且内核将透明地,但懒洋洋地将代码页带入RAM,通过demand paging)。这就是我试图通过说你的程序在其虚拟地址空间(而不是直接在RAM中)运行来解释的。需要整本书来进一步解释。
例如,如果您有一个巨大的可执行文件(为简单起见,假设它是静态链接的),则为一千兆字节。当您启动该可执行文件(使用execve
)时,整个千兆字节不会被带入RAM。如果您的程序快速退出,大部分千兆字节都没有被带入RAM并保留在磁盘上。即使你的程序运行了很长时间,但从不调用一百兆字节的代码,那个代码部分(从未使用的例程的100Mbyte)也不会在RAM中。
BTW,stricto sensu,self modifying code这些天很少使用(当前的处理器甚至不能有效地处理,例如因为缓存和分支预测器)。因此在实践中,您不会完全修改您的机器代码(即使这是可能的)。
并且malware不必修改当前执行的代码。它可以(并且经常会)在内存中注入新代码并以某种方式跳转到它(更确切地说,通过某个函数指针调用它)。因此,一般情况下,您不会覆盖现有的“主动使用”代码,而是在其他地方创建新代码,然后调用它或跳转到它。
如果你想在C的其他地方创建新代码,插件工具(例如Linux上的dlopen
和dlsym
)或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"
格式字符串。