C如何执行括号?

Gra*_*don -5 c linux assembly shellcode

假设我在C中运行此代码:

#include <stdio.h>
int main() {
    int (*ret)();
    ret = getenv("SOME_ENV_VAR");
    ret();
}
Run Code Online (Sandbox Code Playgroud)

ret();功能怎么样?

我想更普遍的是,当我在C中使用括号时,会发生什么?我们是否开始在某个位置运行内存?C在括号中运行时做了什么,我在哪里可以阅读更多关于这类事情的内容?

Pet*_*des 6

您不是"运行括号",而是将环境变量的值转换为函数指针并将作为机器代码执行. 在正常的C实现(如gcc)上,这意味着call它就像是本机机器代码一样.

(在语法上,()在这个上下文中是一个调用函数或函数指针的运算符.在其他上下文中,不是在变量名之后,它是表达式的分组运算符,如(1+2)*3.)

但通常env vars在堆栈上(内核在进程启动之前放置它们),并且堆栈通常不可执行.所以你只是段错误,除非你用它编译-zexecstack或以其他方式使堆栈内存可执行.

此外,stdio.h没有声明getenv,所以假设它返回int.因此,您在64位体系结构上出于这个原因进行了段错误,除非您构建32位代码,其中int可以保存指针而不截断它.(堆栈通常位于虚拟内存的用户空间部分的顶部,因此它位于低32位的地址空间之外).

但令人惊讶的是,现代gcc和clang实际上只用警告编译它,而不是错误,这与你使用C++模式不同.(当然,非常非常严重的警告.)

<source>: In function 'main':
<source>:4:11: warning: implicit declaration of function 'getenv'; did you mean 'getline'? [-Wimplicit-function-declaration]
     ret = getenv("SOME_ENV_VAR");
           ^~~~~~
           getline
<source>:4:9: warning: assignment to 'int (*)()' from 'int' makes pointer from integer without a cast [-Wint-conversion]
     ret = getenv("SOME_ENV_VAR");
         ^
Compiler returned: 0
Run Code Online (Sandbox Code Playgroud)

x86 gcc 8.2的asm输出-xc -O3 -Wall -m32(来自Godbolt编译器浏览器:https://godbolt.org/z/9uPIbN )是:

.LC0:
    .string "SOME_ENV_VAR"
main:
    lea     ecx, [esp+4]
    and     esp, -16
    push    DWORD PTR [ecx-4]
    push    ebp
    mov     ebp, esp
    push    ecx
    sub     esp, 16
    push    OFFSET FLAT:.LC0
    call    getenv
    call    eax                    # use getenv ret value as a function pointer
    mov     ecx, DWORD PTR [ebp-4]
    add     esp, 16
    xor     eax, eax
    leave
    lea     esp, [ecx-4]
    ret
Run Code Online (Sandbox Code Playgroud)

因此,如果你运行这个程序SOME_ENV_VAR=$'\xc3' ./a.out,它实际上会退出而不会崩溃,如果你编译它-zexecstack. 0xC3x86ret的操作码,$''是用于处理C样式转义序列的bash语法,用于创建包含\n换行符或任何字节的字符串(除了0之外,因为bash内部使用C样式的隐式长度字符串).

peter@volta:/tmp$ gcc -Wall -m32 -O3 run-env.c -zexecstack
run-env.c: In function ‘main’:
run-env.c:4:11: warning: implicit declaration of function ‘getenv’; did you mean ‘getline’? [-Wimplicit-function-declaration]
     ret = getenv("SOME_ENV_VAR");
           ^~~~~~
           getline
run-env.c:4:9: warning: assignment makes pointer from integer without a cast [-Wint-conversion]
     ret = getenv("SOME_ENV_VAR");
         ^
peter@volta:/tmp$ SOME_ENV_VAR=$'\xc3' ./a.out
peter@volta:/tmp$ ./a.out 
Segmentation fault (core dumped)
Run Code Online (Sandbox Code Playgroud)

如果我遗漏了任何-m32或者-zexecstack,那将是段错误.退出env var,得到genenv返回NULL,或者任何非干净地返回的机器代码的值也将是段错误的.或者我可以使用(指令)使它成为SIGILL .0F 0Bud2

或者,如果我包含正确的标题,我不需要-m32,因为从char*函数指针隐式转换将"工作".就C标准而言,这显然是完全未定义的行为,但这种行为是我们从asm gcc发出的行为.(这应该是任何其他架构的情况,除非你需要输入返回指令的字节.通常是2或4个字节;大多数架构使用固定长度的指令字,与x86不同.这个答案是以x86为中心的因为这是我在桌面上轻松测试的内容.)

您可以而且应该通过GDB单步执行asm来了解正在发生的事情.

您可以将更长的机器代码序列放入env var中,甚至可以使用它来测试shellcode.printf除非您为libc禁用了ASLR,否则您无法通过此方式注入代码 轻松调用任何内容.即便如此,您还需要查看GDB以查看env var最终的地址.

有关指南,教程和x86 asm以及体系结构手册的更多链接,请参阅https://stackoverflow.com/tags/x86/info.