C如何执行括号?

时间:2018-09-08 03:06:38

标签: c linux assembly shellcode

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

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

ret();的功能是什么?

更一般地说,当我在C中使用括号时,会发生什么?我们是否从某个位置开始运行内存? C遇到括号时该怎么办?我在哪里可以读到更多有关此类内容的信息?

1 个答案:

答案 0 :(得分:6)

您不是“运行括号”,而是将环境变量的值强制转换为函数指针,并将那个作为机器代码执行。在普通C语言上的实现(例如gcc),这意味着call将其视为本机代码。

(在语法上,()在此上下文中是调用函数或函数指针的运算符。在其他上下文中,不是在变量名之后,它是诸如(1+2)*3这样的表达式的分组运算符。)< / p>

但是通常env vars在堆栈上(内核在进程启动之前将它们放在其中),并且堆栈通常是不可执行的。因此,除非您使用-zexecstack进行编译或以其他方式使堆栈存储器可执行,否则您只会进行段错误。

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

但是令人惊讶的是,与您使用C ++模式不同,现代的gcc和clang实际上仅使用警告而不是错误进行了编译。 (当然,非常严重的警告。)

<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

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

.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

因此,如果您使用SOME_ENV_VAR=$'\xc3' ./a.out运行该程序,那么如果您使用-zexecstack进行了编译,它将实际上退出而不会崩溃0xC3x86 ret的操作码,$''是bash语法,用于处理C样式转义序列以创建包含诸如\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)

如果我遗漏了-m32-zexecstack中的任何一个,它将导致段错误。忽略env var,让genenv返回NULL,或者任何不是干净返回的机器代码的值也将出现段错误。或者我可以使用0F 0B (the ud2 instruction.)

使其成为SIGILL

或者,如果我包括适当的头文件,则不需要-m32,因为从char*到函数指针的隐式转换将“起作用”。就C标准而言,这显然是完全未定义的行为,但这是我们从gcc发出的asm中获得的行为。 (在其他任何体系结构上都应该是这种情况,除非您需要为返回指令放入字节。通常为2或4个字节;与x86不同,大多数体系结构都使用定长指令字。此答案是以x86为中心的因为这是我在台式机上可以轻松测试的内容。)

您可以并且应该单步使用GDB来了解最新情况。

您可以将更长的机器代码序列放入一个env变量中,甚至可以将其用于测试shellcode。您无法轻易地调用printf或注入代码中的任何内容除非您为libc禁用了ASLR。即使那样,您仍然需要与GDB一起查看env var最终指向的地址。

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