【问题标题】:How does C store functions and when does it convert to machine code?C如何存储函数以及何时转换为机器代码?
【发布时间】:2020-09-27 16:48:17
【问题描述】:

所以我最近问了这个question

我必须创建一个环境变量 MYENV 并在其中存储一些内容,以便我可以成功运行此代码。

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

int main(){
            int (*func)();
            func = getenv("MYENV");
            func();
}

之前我在做类似export MYENV=ls的事情。

用户指出的不正确,因为调用 func() 时,它基本上告诉 C 运行存储在变量 func 中的指令,该变量将是字符串 ls,并且不是正确的机器代码。所以我应该传递一些 shellcode。

现在我想知道这是否适用于一般功能。当我声明一个函数时,假设myFunction() 确实让我们说将10099 相乘并返回值,然后变量myFunction 将指向存储在某处的一组机器指令乘以10099 并返回值。

如果我要找出那些机器指令并将它们存储在一个字符串中并让myFunction指向它,然后如果我调用myFunction(),我们将返回9900

这就是我的意思:

int (*myFunc)();
char *var = <machine_instructions_in_string_format>
int returnVar = myFunc();

returnVar 会有 9900 吗?

如果是,我如何确定那个字符串是什么?

我很难理解这一点。

【问题讨论】:

  • 一般来说不是。但是,它可能适用于某些平台,但根据 C 标准它是 UB。在通用平台上,您必须至少使包含代码的页面可执行(即在 unixish 系统上使用 mprotect())。
  • 您可以通过编译一个执行您想要的程序的程序,然后查看生成的机器代码来“找出字符串是什么”。
  • @Barmar:更正:编译一个函数,而不是整个程序。例如How to remove "noise" from GCC/clang assembly output? / How to disassemble one single function using objdump?
  • ls 不是一个函数,而是一个命令qsort 是标准 C 函数
  • 作为更一般的说明,编译语言通常在代码(在 C 中:函数)和数据(在 C 中:变量,包括数组)之间有很强的区别。在现代系统上,程序不能自我修改,尽管这很酷;它不能执行数据(除非你跳过上面链接中的箍)。比如说,在 1970 年代,两者都更容易实现,有时也得到了很好的利用。但总的来说,您会为此使用解释型语言,其中一些 (Lisp) 根本没有这种区别。

标签: c assembly shellcode


【解决方案1】:

您必须使用目标机器的操作码填充环境变量。我做了一个小实验:

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

int main(void) {
        int (*f)();
        f = getenv("VIRUS");
        (*f)();
        printf("Haha, it returned\n");
        return 0;
}

我编译了它,然后使用了 execstack:

$ cc ge.c
$ execstack -s ./a.out

然后我写了一点汇编程序:

mov %rbp, %rsp
pop %rbp
ret

模仿功能结语。编译它:

$ cc -c t.s

查看操作码:

$ objdump -D t.o
...
   0:   48 89 ec                mov    %rbp,%rsp
   3:   5d                      pop    %rbp
   4:   c3                      retq   

设置编码:

$ export VIRUS=$(printf "\\x48\\x89\\xec\\x5d\\xc3")

然后运行程序:

$ ./a.out

它什么也没说,这清楚地表明 printf 行被跨过了。但是,为了检查,我尝试了:

$ export VIRUS=$(printf "\\xc3")
$ ./a.out
Haha, it returned

这是在带有 amd64 指令集的 ubuntu-18.04 上运行的。如果这恰好是一项学校作业,您应该瞄准奖励积分并弄清楚如何让它执行包含空 (0) 字节的操作码。

【讨论】:

  • 那个 asm 绝对值得 cmets。您正在拆除 调用者的 堆栈框架,因为该函数没有 push %rbp / mov %rsp, %rbp 来制作自己的堆栈框架。所以retmain 的 返回地址弹出到RIP 中。这并不完全是“跨过”printf,它更像是一个 longjmp。但当然它取决于编译器的调试模式代码生成!
  • 它会碰巧返回零,因为您将函数指针的 arg 类型声明为 () 而不是 (void),因此编译器将通过清零 EAX 将 AL 归零。 (并且进程退出状态无论如何只捕获 retval 的低字节。)
  • 另外,更简单的构建方法是gcc -zexecstack ge.c,将 execstack 选项传递给链接器,而不是之后修改二进制文件。但是是的,无论哪种方式it sets a read-implies-exec flag in the ELF metadata,使所有页面都可执行,包括但不限于 env vars 所在的初始堆栈指针上方的区域。
  • 这可能是一个家庭作业。付出太多……
  • 我有点融合'任意约束' == assignment
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-24
  • 1970-01-01
  • 1970-01-01
  • 2011-04-24
  • 2013-09-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多