假设我在C中运行此代码:
#include <stdio.h>
int main() {
int (*ret)();
ret = getenv("SOME_ENV_VAR");
ret();
}
ret();
的功能是什么?
更一般地说,当我在C中使用括号时,会发生什么?我们是否从某个位置开始运行内存? C遇到括号时该怎么办?我在哪里可以读到更多有关此类内容的信息?
答案 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
进行了编译,它将实际上退出而不会崩溃。 0xC3
是x86 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.)
或者,如果我包括适当的头文件,则不需要-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。