【问题标题】:Why am I allowed to exit main using ret?为什么允许我使用 ret 退出 main?
【发布时间】:2020-04-27 22:10:46
【问题描述】:

我即将弄清楚程序堆栈是如何设置的。 我了解到使用

调用该函数
call pointer;

实际上等同于:

mov register, pc ;programcounter
add register, 1 ; where 1 is one instruction not 1 byte ...
push register
jump pointer

但是,这意味着当 Unix 内核调用 main 函数时,堆栈基应该指向调用 main 的内核函数中的重入。

因此在C - 代码中跳转“*rbp-1”应该重新进入主函数。

然而,这不是在以下代码中发生的情况:

#include <stdlib.h>
#include <unistd.h>

extern void ** rbp(); //pointer to stack pointing to function
int main() {
   void ** p = rbp();
   printf("Main: %p\n", main);
   printf("&Main: %p\n", &main); //WTF
   printf("*Main: %p\n", *main); //WTF
   printf("Stackbasepointer: %p\n", p);
   int (*c)(void) = (*p)-4;
   asm("movq %rax, 0");
   c();

   return 0;        //should never be executed...

}

汇编文件:rsp.asm

...

.intel_syntax

.text:

.global _rbp

_rbp:
  mov rax, rbp
  ret;

这是不允许的,不出所料,可能是因为此时的指令不完全是 64 位,可能是因为 UNIX 不允许这样做...

不允许此调用:

   void (*c)(void) = (*p);
   asm("movq %rax, 0"); //Exit code is 11, so now it should be 0
   c(); //this comes with stack corruption, when successful

这意味着我没有义务退出主调用函数。

然后我的问题是:为什么当我在每个 GCC 主函数的末尾都使用 ret 时,我的作用应该与上面的代码相同。 unix - 系统如何有效地检查此类尝试... 我希望我的问题很清楚......

谢谢。 P.S.:代码仅在 macOS 上编译,更改 linux 的程序集

【问题讨论】:

  • 这个问题很不清楚。简短回答:main 与任何其他功能没有太大区别。唯一的两个区别是 1:main 在程序启动时由系统自动调用。 2:如果main末尾没有return语句,则自动执行return 0;
  • 是的,但是为什么 ret 起作用,而不允许跳转到基指针,因为两条指令实际上是相等的......
  • 对不起,我不明白,为什么ret 不起作用?同样在评论中你有//Exit code is 11:为什么是11? ret 末尾的 main 简单地返回到任何系统代码。
  • @Niclas 就在那里 - 你的假设是错误的。 main 是从 libc 内部调用的,而不是内核本身。内核只是为进程运行设置环境。您可以通过弄乱堆栈指针来执行一些 libc 代码,但请放心,您无法轻易破坏内核。
  • 注意:main 可以有不同的堆栈结构(例如,保证 0 作为默认返回,调用退出函数(如果已注册))。 exec* 调用的函数不是 C 的 main 函数(因为 C 需要一些设置)

标签: c macos security assembly x86-64


【解决方案1】:

但是,这意味着当 Unix 内核调用 main 函数时,堆栈基应该指向调用 main 的内核函数中的重入。

绝对不是。

This 特定问题涵盖了 MacOS 的详细信息,请查看。在任何情况下,main 最有可能返回到 C 标准库的 start 函数。不同 *nix 操作系统的实现细节不同。

因此在C - 代码中跳转“*rbp-1”应该重新进入主函数。

当您调用rbp() 函数时,您无法保证编译器会发出什么以及 rsp/rbp 的状态是什么。你不能做出这样的假设。

顺便说一句,如果您想以 64 位访问堆栈条目,您可以以 +-8 为增量执行此操作(因此分别为 rbp+8 rbp-8 rsp+8 rsp-8)。

【讨论】:

  • 所有 x86-64 *nix 操作系统都使用 x86-64 System V ABI,它确实为_start 定义了进程启动环境。操作系统之间的一些细节可能有所不同,比如我认为 MacOS 在 libc 本身中包含 _start,而 Linux 将 _start 静态链接到可执行文件并让它调用 __libc_start_main(将 main 的地址作为 arg 传递)。
  • 在 MacOS 中,_start 来自 libdyld.dylib,dyld 使用它来设置进程启动。
  • 在 Linux 上,/lib/ld.so 确实包含 _start,这是在动态链接的可执行文件中在用户空间中运行的实际第一件事。但在其工作完成后,它会跳转到可执行文件 (ELF) 的入口点(也称为 _start,与 RSP 相同的环境指向 argc),而不是通过 libc 函数调用可执行文件的 main。在带有 glibc 的 GNU/Linux 上,可执行文件的 _start 调用它的 main(间接)。 MacOS 从 dyld 调用可执行文件的 main,我认为(通过 libc?),所以可执行文件本身不包含 RSP 指向 argc 执行的块,对吧?
  • libc 不涉及,并且可执行文件中没有特殊块。 _dyld_start直接跳转到mainmain返回_startopensource.apple.com/source/dyld/dyld-195.5/src/…
  • 哦,有道理。在 Linux 上,我认为在 libc 中使用 __libc_start_main 的意义在于,不必将大量代码静态链接到每个可执行文件中。但是在 MacOS 上,如果在 dyld 中有相同的东西,那就更简单地解决了同样的问题。所以在_dyld_start 调用libc init 函数之后(这必须在某个时候发生),它可以直接调用main 然后运行atexit 函数是有意义的。
【解决方案2】:

我认为您在这里有很多误解。首先,main 不是内核调用的对象。内核分配一个进程并将我们的二进制文件加载到内存中——如果您使用的是基于 Unix 的操作系统,通常从 ELF 文件中加载。此 ELF 文件包含需要映射到内存的所有部分以及作为 ELF 中代码的“入口点”的地址(除其他外)。 ELF 可以指定加载程序跳转到的任何地址,以便开始启动程序。在使用 GCC 构建的应用程序中,这是一个名为 _start 的函数。 _start 然后设置堆栈并在调用 __libc_start_main 之前进行所需的任何其他初始化,这是一个 libc 函数,可以在调用 main main 之前进行额外设置。

这里是一个启动函数的例子:

00000000000006c0 <_start>:


 6c0:   31 ed                   xor    %ebp,%ebp
 6c2:   49 89 d1                mov    %rdx,%r9
 6c5:   5e                      pop    %rsi
 6c6:   48 89 e2                mov    %rsp,%rdx
 6c9:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
 6cd:   50                      push   %rax
 6ce:   54                      push   %rsp
 6cf:   4c 8d 05 0a 02 00 00    lea    0x20a(%rip),%r8        # 8e0 <__libc_csu_fini>
 6d6:   48 8d 0d 93 01 00 00    lea    0x193(%rip),%rcx        # 870 <__libc_csu_init>
 6dd:   48 8d 3d 7c ff ff ff    lea    -0x84(%rip),%rdi        # 660 <main>
 6e4:   ff 15 f6 08 20 00       callq  *0x2008f6(%rip)        # 200fe0 <__libc_start_main@GLIBC_2.2.5>
 6ea:   f4                      hlt    
 6eb:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

如您所见,此函数设置堆栈的值和堆栈基指针。因此,此函数中没有有效的堆栈帧。在您调用 main 之前,堆栈帧甚至不会设置为 0(至少通过此编译器)

现在重要的是看到堆栈在这段代码中被初始化,并且通过加载器,它不是内核堆栈的延续。每个程序都有自己的堆栈,这些堆栈都与内核的堆栈不同。事实上,即使您知道内核中堆栈的地址,您也无法从程序中读取或写入堆栈,因为您的进程只能看到已由 MMU 分配给它的内存页面,即由内核控制。

澄清一下,当我说堆栈是“创建的”时,我并不是说它已被分配。我只是说这里设置了栈指针和栈基。它的内存是在程序加载时分配的,每当由于写入堆栈的未分配部分而触发页面错误时,就会根据需要向其中添加页面。进入 start 后,显然存在一些堆栈作为 pop rsi 指令的证据,但这不是程序将使用的最终堆栈值的堆栈。这些是在_start 中设置的变量(也许这些变量稍后会在__libc_start_main 中更改,我不确定。)

【讨论】:

【解决方案3】:

Cmain 是从 CRT 启动代码调用(间接),而不是直接从内核调用。

main 返回后,该代码调用 atexit 函数来执行诸如刷新 stdio 缓冲区之类的操作,然后将 main 的返回值传递给原始的 _exit 系统调用。或者exit_group 退出所有线程。


你做了几个错误的假设,我认为都是基于对内核如何工作的误解。

  • 内核以不同于用户空间的特权级别运行(环 0 与 x86 上的环 3)。即使用户空间知道要跳转到的正确地址,它也无法跳转到内核代码。 (即使可以,它也不会以内核特权级别运行)。

    ret 不是魔法,它基本上只是pop %rip,并且不会让你跳转到其他指令无法跳转到的任何地方。也不会更改权限级别1

  • 当用户空间代码运行时,内核地址不能被映射/访问;这些页表条目被标记为仅主管。 (或者它们根本没有映射到缓解 Meltdown 漏洞的内核中,因此进入内核需要通过更改 CR3 的“包装”代码块。)

    虚拟内存是内核保护自己免受用户空间影响的方式。 用户空间不能直接修改页表,只能通过mmapmprotect 要求内核进行修改系统调用。 (并且用户空间不能执行像 mov cr3, rax 这样的特权指令来安装新的页表。这就是设置 ring 0(内核模式)与 ring 3(用户模式)的目的。)

  • 内核堆栈独立于进程的用户空间堆栈。 (在内核中,每个任务(又名线程)还有一个小的内核堆栈,在该用户空间线程运行时在系统调用/中断期间使用。至少 Linux 是这样做的,IDK 关于其他人。)

  • 内核并不是字面上的call 用户空间代码;用户空间堆栈不会将任何返回地址保存回内核。 内核->用户转换涉及交换堆栈指针以及更改特权级别。例如使用iret(中断返回)之类的指令。

    另外,在用户空间可以看到的任何地方留下内核代码地址会破坏内核 ASLR。

脚注 1:(编译器生成的ret 将始终是ret 附近的正常值,而不是可以通过调用门或其他方式返回特权cs 值的retf。x86 处理特权级别通过 CS 的低 2 位,但没关系。MacOS / Linux 不要设置用户空间可以用来调用内核的调用门;这是通过 syscallint 0x80 完成的说明。)


在一个新进程中(在execve 系统调用用新的PID 替换之前的进程后),执行从进程入口点开始(通常标记为_start),不是 直接在 C main 函数处。

C 实现附带 CRT (C RunTime) 启动代码,该代码具有(除其他外)_start 的手写 asm 实现,它(间接)调用 main,根据调用约定将 args 传递给 main。

_start 本身不是一个函数。 在进程进入时,RSP 指向argc,而在用户空间堆栈上的上方是argv[0]argv[1] 等。 (即char *argv[] 数组按值就在那里,在envp 数组之上。)_startargc 加载到寄存器中,并将指向argv 和envp 的指针放入寄存器中。 (MacOS 和 Linux 都使用的 x86-64 System V ABI 记录了所有这些,包括进程启动环境和调用约定。

如果您尝试_startret,您只需将argc 弹出到RIP,然后从绝对地址12 获取代码(或其他小数字)将出现段错误。例如,Nasm segmentation fault on RET in _start 显示从进程入口点尝试ret(链接没有 CRT 启动代码)。它有一个手写的_start,正好落入main


当您运行gcc main.c 时,gcc 前端会运行多个其他程序(使用gcc -v 显示详细信息)。这就是 CRT 启动代码链接到您的进程的方式:

  • gcc 预处理 (CPP) 并编译 + 汇编 main.cmain.o(或临时文件)。在 MacOS 上,gcc 命令实际上是具有内置汇编程序的 clang,但真正的 gcc 确实会编译为 asm,然后在其上运行 as。 (不过,C 预处理器是编译器内置的。)
  • gcc 运行类似ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie /usr/lib/Scrt1.o /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/crtbeginS.o main.o -lc -lgcc /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/crtendS.o 的东西。这实际上简化了很多,省略了一些 CRT 文件,并且规范化了路径以删除 ../../lib 部分。此外,它不直接运行ld,它运行collect2,它是ld 的包装器。但无论如何,这会静态链接那些包含_start 和其他东西的.o CRT 文件,并动态链接 libc (-lc) 和 libgcc(用于 GCC 辅助函数,例如实现 __int128 乘除以 64-位寄存器,以防您的程序使用它们)。

.intel_syntax

.text:

.global _rbp

_rbp:
  mov rax, rbp
  ret;

这是不允许的,...

不组装的唯一原因是您试图将.text: 声明为标签,而不是使用.text 指令。如果您删除尾随的 :,它确实会与 clang 组合在一起(将 .intel_syntax 视为与 .intel_syntax noprefix 相同)。

要让 GCC / GAS 组装它,您还需要 noprefix 告诉它寄存器名称不以 % 为前缀。 (是的,您可以拥有 Intel op dst、src 命令,但仍然使用%rsp 寄存器名称。不,您不应该这样做!)当然 GNU/Linux 没有'不要使用前导下划线。

不是说如果你调用它,它总是会做你想做的事!如果您在没有优化的情况下编译了main(所以-fno-omit-frame-pointer 有效),那么是的,您会得到一个指向返回地址下方的堆栈槽的指针。


而且你肯定用错了值(*p)-4; 加载保存的 RBP 值 (*p),然后偏移四个 8 字节空指针。 (因为这就是 C 指针数学的工作原理;*p 的类型为 void*,因为 p 的类型为 void **)。

我认为您正在尝试获取自己的返回地址并重新运行到达 main 的 call 指令(在 main 的调用者中),最终导致由于推送更多返回地址而导致堆栈溢出。在 GNU C 中,使用 void * __builtin_return_address (0) to get your own return address

x86 call rel32 指令为 5 个字节,但调用 main 的 call 可能是间接调用,使用寄存器中的指针。所以它可能是一个2字节的call *%rax或一个3字节的call *%r12,除非你反汇编你的调用者,否则你不知道。 (我建议在反汇编模式下使用调试器在main 末尾通过指令(GDB / LLDB stepi)单步执行。如果它有任何主调用者的符号信息,您将能够向后滚动看看之前的指令是什么。

如果没有,您可能必须尝试看看看起来是否正常; x86 机器代码不能明确地向后解码,因为它是可变长度的。您无法区分指令中的字节(如立即数或 ModRM)与指令的开头之间的区别。这一切都取决于您从哪里开始反汇编。如果你尝试几个字节偏移,通常只有一个会产生看起来正常的任何东西。


   asm("movq %rax, 0"); //Exit code is 11, so now it should be 0

这是 RAX 到绝对地址 0 的存储,采用 AT&T 语法。 这当然是段错误。退出代码 11 来自 SIGSEGV,即信号 11。(使用 kill -l 查看信号编号)。

也许你想要mov $0, %eax。尽管这在这里仍然毫无意义,但您将通过函数指针进行调用。在调试模式下,编译器可能会将其加载到 RAX 中并踩到您的值。

此外,当您不告诉编译器您正在修改哪些寄存器(使用约束)时,在 asm 语句中编写寄存器永远不会安全。


   printf("Main: %p\n", main);
   printf("&Main: %p\n", &main); //WTF

main&amp;main 是一回事,因为 main 是一个函数。这就是 C 语法对函数名的作用。 main 不是可以获取其地址的对象。 & operator optional in function pointer assignment

数组也是类似的:数组的裸名可以分配给一个指针,也可以作为指针arg传递给函数。但是&amp;array也是同一个指针,和&amp;array[0]一样。这仅适用于像int array[10] 这样的数组,不适用于像int *ptr 这样的指针;在后一种情况下,指针对象本身有存储空间,可以有自己的地址。

【讨论】:

    猜你喜欢
    • 2021-02-02
    • 2013-04-11
    • 1970-01-01
    • 2011-07-16
    • 2015-08-03
    • 2013-05-08
    • 1970-01-01
    • 2021-04-14
    • 2019-12-20
    相关资源
    最近更新 更多