【问题标题】:Executing machine code in memory在内存中执行机器代码
【发布时间】:2011-01-02 11:00:15
【问题描述】:

我正在尝试弄清楚如何执行存储在内存中的机器代码。

我有以下代码:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    FILE* f = fopen(argv[1], "rb");

    fseek(f, 0, SEEK_END);
    unsigned int len = ftell(f);
    fseek(f, 0, SEEK_SET);

    char* bin = (char*)malloc(len);
    fread(bin, 1, len, f);

    fclose(f);

    return ((int (*)(int, char *)) bin)(argc-1, argv[1]);
}

上面的代码在 GCC 中编译得很好,但是当我尝试像这样从命令行执行程序时:

./my_prog /bin/echo hello

程序段错误。我发现问题出在最后一行,因为注释掉它会停止段错误。

我认为我做的不太对,因为我仍在思考函数指针。

问题是演员阵容错误还是其他原因?

【问题讨论】:

  • 查理:如果你曾经理解所有这些答案,而不是像你拥有的那样使用指向函数的转换指针,你可能更适合编写一些动态管理堆栈参数的基本 thunk .如果使用 gcc,则声明为“function() attribute ((naked));”的函数有关更多示例,请参阅gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html。这样,您调用相同的函数来决定是否需要为动态加载的代码提供 N 个参数/调用约定等...无论哪种方式,您都应该查看 FFI 等。
  • 我很确定 OP 只是误解了可执行文件如何工作的基本原理。使用动态链接库执行您自己的动态代码,并使用 exec 执行其他应用程序。
  • @Jimbo - 你完全正确。我想看看我能不能做到这一点,所以我想“我在哪里可以找到机器代码?”,并决定直接抓取一个可执行文件而不加考虑:/
  • 你可能有一些运气编译到 Web 程序集。

标签: c segmentation-fault function-pointers casting


【解决方案1】:

您需要一个具有写入执行权限的页面。如果您在 unix 下,请参阅 mmap(2) 和 mprotect(2)。您不应该使用 malloc 来执行此操作。

另外,请阅读其他人所说的,您只能使用加载程序运行原始机器代码。如果您尝试运行 ELF 标头,它可能会出现相同的段错误。

关于回复和downmods的内容:

1- OP 说他正在尝试运行机器代码,所以我对此进行了回复,而不是执行可执行文件。

2- 了解为什么不混合使用 malloc 和 mman 函数:

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

int main()
{
    char *a=malloc(10);
    char *b=malloc(10);
    char *c=malloc(10);
    memset (a,'a',4095);
    memset (b,'b',4095);
    memset (c,'c',4095);
    puts (a);
    memset (c,0xc3,10); /* return */

    /* c is not alligned to page boundary so this is NOOP.
     Many implementations include a header to malloc'ed data so it's always NOOP. */
    mprotect(c,10,PROT_READ|PROT_EXEC);
    b[0]='H'; /* oops it is still writeable. If you provided an alligned
    address it would segfault */
    char *d=mmap(0,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);
    memset (d,0xc3,4096);
    ((void(*)(void))d)();
    ((void(*)(void))c)(); /* oops it isn't executable */
    return 0;
}

它在 Linux x86_64 上准确地显示了这种行为,其他丑陋的行为肯定会出现在其他实现上。

【讨论】:

  • 我会调查的。我有一种感觉,这可能与此有关。
  • 其实不太对,用malloc也可以,只需要用mprotect就可以了。
  • 好的,如果你阅读他的代码,你会看到他正在加载一个文件,以执行。事实上,它是一个编译的二进制文件,意味着它的文本区域是 PAGE SIZE ALIGNED ALLREADY。如果他 mprotect 是 HEAP,那么唯一可能的问题是他加载到 EXECUTE 的文件,如果他自己没有调整,可能会有一些 .data 可能标记为 EXEC。但是他们让 HEAP +x、JAVA 和 MONO 一直都在做这件事是没有问题的。
  • 不要太激动,mmap、mprotect等只在页面中保护/取消保护,而不是字节。 malloc 实现将 malloc 的数据放在预先分配的块中,因此如果您更改块中的保护,它可能会附加或附加到共享相同页面的其他 malloc 数据。如果您使用 mprotect 保护将是 (r|)w|x 或 r|x,在任何情况下,页面中的 r|w 数据都不会喜欢它,即。段错误您将保留该数据以引入可执行代码。
  • 是的,别担心,我让一切都平静下来,甚至认为您的帖子在您的代码示例之后很有帮助。但是无论如何,如果您从我的代码中看到,malloc 工作得很好+rwx,即使您将空闲的添加到我展示的示例调用的所有 3 个堆分配内存中,它们也没有问题或任何稳定性问题。唯一的问题是您可能会无意中将堆上的一些内存稍微许可为 +x,但这真的没什么大不了的。
【解决方案2】:

使用 malloc 效果很好。

好的,这是我的最终答案,请注意我使用了原始海报的代码。 我正在从磁盘加载此代码的编译版本到堆分配区域“bin”,就像原始代码一样(名称固定不使用 argv,值 0x674 来自;

objdump -F -D foo|grep -i hoho
08048674 <hohoho> (File Offset: 0x674):

这可以在运行时使用 BFD(二进制文件描述符库)或其他东西进行查找,您可以调用其他二进制文件(不仅仅是您自己),只要它们静态链接到同一组库即可。

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

unsigned char *charp;
unsigned char *bin;

void hohoho()
{
   printf("merry mas\n");
   fflush(stdout);
}

int main(int argc, char **argv)
{
   int what;

   charp = malloc(10101);
   memset(charp, 0xc3, 10101);
   mprotect(charp, 10101, PROT_EXEC | PROT_READ | PROT_WRITE);

   __asm__("leal charp, %eax");
   __asm__("call (%eax)" );

   printf("am I alive?\n");

   char *more = strdup("more heap operations");
   printf("%s\n", more);

   FILE* f = fopen("foo", "rb");

   fseek(f, 0, SEEK_END);
   unsigned int len = ftell(f);
   fseek(f, 0, SEEK_SET);

   bin = (char*)malloc(len);
   printf("read in %d\n", fread(bin, 1, len, f));
   printf("%p\n", bin);

   fclose(f);
   mprotect(&bin, 10101, PROT_EXEC | PROT_READ | PROT_WRITE);

   asm volatile ("movl %0, %%eax"::"g"(bin));
   __asm__("addl $0x674, %eax");
   __asm__("call %eax" );
   fflush(stdout);

   return 0;
}

正在运行...

co tmp # ./foo
am I alive?
more heap operations
read in 30180
0x804d910
merry mas

您可以使用UPX 来管理文件的加载/修改/执行。

附:抱歉之前断开的链接:|

【讨论】:

  • 请注意这个 IS 跨平台和完全抽象的文件格式规范的细节或使用页面保护等的任何类型的要求。
  • Pffft,我喜欢毫无理由地被否决,变得真实。 UPX 是这样做的方式,使用其他任何东西都是天真的。您可以轻松地使用它为您加载 exe,或者它是较低级别的 api,它发出动态程序集存根,可以加载/运行压缩或其他方式的任意内存块。
  • 好吧,我们不知道他将如何将机器代码存入内存。如果他正在编写一个字节码解释器并且代码将在内存中生成怎么办?加载“echo”(与代码一样不正确)可能是一个概念验证,即可以动态生成和执行代码。
  • malloc 不能确保页面对齐,您的代码可能有效,也可能无效。您可以使用 mallocd 块的页面对齐子集,这将是安全的,或者可能使用 posix_memalign 如果你有它
  • 希望你不介意我的编辑,你的 UPX 链接指向了一个肮脏的地方
【解决方案3】:

在我看来,您正在加载 ELF 图像,然后试图直接跳转到 ELF 标头? http://en.wikipedia.org/wiki/Executable_and_Linkable_Format

如果您尝试执行另一个二进制文件,为什么不为您使用的任何平台使用进程创建功能?

【讨论】:

  • 我认为这是因为他试图在他分配的内存中执行一个应用程序,我不相信任何进程创建功能都是这样运行的。线程创建函数可能,但他正在将磁盘文件加载到内存中,然后尝试执行该内存。
  • 如果内存没有被标记为执行他将无法执行它,但他也在将一个ELF文件加载到内存中然后尝试调用ELF头,其中的前四个字节是 0x7f 'E' 'L' 'F'
  • 有趣的事实:0x7F 是 JNLE 的主要操作码。所以也许代码试图做的第一件事就是跳转到垃圾地址?无论哪种方式:执行 ELF 标头都行不通。
【解决方案4】:

一个典型的可执行文件有:

  • 标题
  • main(int, char **)之前调用的入口代码

第一个意味着您通常不能期望文件的字节 0 是可执行的;相反,标题中的信息描述了如何将文件的其余部分加载到内存中以及从哪里开始执行它。

第二个意思是当你找到入口点时,你不能期望把它当作一个接受参数(int, char **)的C函数。也许,它可以用作不带参数的函数(因此在调用它之前不需要推送任何内容)。但是您确实需要填充环境,然后入口代码将使用该环境来构造传递给 main 的命令行字符串。

在给定的操作系统下手动执行此操作会进入我无法理解的深度;但我确信有一种更好的方式来做你想做的事情。您是否尝试将外部文件作为开关操作执行,或者加载外部二进制文件并将其功能视为程序的一部分?两者都由 Unix 中的 C 库提供。

【讨论】:

    【解决方案5】:

    更有可能是通过函数指针调用跳转到的代码导致了段错误,而不是调用本身。您发布的代码无法确定加载到 bin 中的代码是否有效。最好的办法是使用调试器,切换到汇编器视图,中断 return 语句并进入函数调用,以确定您希望运行的代码确实在运行,并且它是有效的.

    还要注意,为了运行所有代码,需要位置独立并完全解析。

    此外,如果您的处理器/操作系统启用数据执行预防,那么尝试可能注定要失败。无论如何,这充其量是不明智的,加载代码是操作系统的目的。

    【讨论】:

    • 是的,独立的位置很好,如果使用 gcc,Charlie 可以使用 -fPIC,但不幸的是在 Windows 上,它们不是简单的方法来获得编译的 PIC C 应用程序。
    【解决方案6】:

    您可以 dlopen() 一个文件,查找符号“main”并使用 0、1、2 或 3 个参数(均为 char* 类型)通过强制转换为指向函数返回的整数来调用它-take-0,1,2,or3-char*

    【讨论】:

    • 使用这样的方法你可能想要查找 __libc_start_main
    【解决方案7】:

    使用操作系统加载和执行程序。

    在 unix 上,exec 调用可以做到这一点。

    问题中的 sn-p 可以重写:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char* argv[])
    {
        return execv(argv[1],argv+2);
    }
    

    【讨论】:

    • exec 没有这样做,他试图手动将应用程序加载到内存中。 exec expect 是一个文件路径参数,而不是 &memory 地址。
    • 他使用 fopen 打开二进制文件,然后尝试跳入其中。如果他只是将这条路径传递给 exec ......谢谢 downmod。
    • 如果您向我澄清您认为 exec 实际上是如何按照他的要求执行的,即“在内存中执行机器代码”,我会立即取消对您的任何反对,但这完全不是他问我什么。感谢相关的反对票。
    • 我没有对 UPX 投反对票。我在原始问题中添加了代码的剪切粘贴更改。
    • 正如李小龙曾经说过的“我的风格?就像不战而战的艺术。”不错。
    【解决方案8】:

    您正在尝试做的事情类似于口译员所做的事情。除了解释器读取用 Python 等解释语言编写的程序,即时编译该代码,将可执行代码放入内存然后执行。

    您可能还想了解更多关于即时编译的信息:

    Just in time compilation
    Java HotSpot JIT runtime

    如果您有兴趣,可以使用 GNU lightninglibJIT 等库来生成 JIT 代码。但是,您必须做的不仅仅是从文件中读取并尝试执行代码。一个示例使用场景是:

    1. 读取用脚本语言编写的程序(也许 您自己的)。
    2. 解析源代码并将其编译为 被理解的中间语言 JIT 库。
    3. 使用 JIT 库生成代码 对于这个中间 表示,适用于您的目标平台的 CPU。
    4. 执行 JIT 生成的代码。

    为了执行代码,您必须使用诸如使用 mmap() 将可执行代码映射到进程的地址空间、标记该页面可执行并跳转到该内存块等技术。它比这更复杂,但它是一个很好的开始,以便了解所有脚本语言(如 Python、Ruby 等)的解释器背后发生的事情。

    Linkers and Loaders”一书的online version 将为您提供有关目标文件格式、执行程序时幕后发生的事情、链接器和加载器的角色等方面的更多信息。这是一本很好的读物。

    【讨论】:

      【解决方案9】:

      可执行文件包含的不仅仅是代码。标头、代码、数据、更多数据,这些东西被操作系统及其库分离并加载到不同的内存区域。您不能将程序文件加载到单个内存块中并期望跳转到它的第一个字节。

      如果您尝试执行自己的任意代码,则需要查看动态库,因为这正是它们的用途。

      【讨论】:

      • 不是 MSDOS .COM 文件 - 它们只是机器代码的二进制映像 - 太糟糕了,它们被限制为 64K...
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-06-27
      • 1970-01-01
      • 2017-06-11
      • 1970-01-01
      • 1970-01-01
      • 2011-01-27
      • 2021-04-28
      相关资源
      最近更新 更多