【发布时间】:2023-04-11 11:47:02
【问题描述】:
为了避免stack clash 对程序的攻击,we 尝试将setrlimit(RLIMIT_STACK) 的堆栈大小限制设置为大约 2 MB。
这个限制可以满足我们程序自身的内部需求,但我们随后注意到,尝试exec() 外部程序在某些具有这个新限制的系统上开始失败。我们使用下面的测试程序调查的一个系统似乎对于exec()'d 程序的最小堆栈大小略高于 4 MiB。
我的问题是,我们如何才能知道给定系统上堆栈大小的安全最小值,以便exec() 不会失败?
我们不想在我们目前测试的所有系统停止失败之前提出这个问题,因为这可能会导致将来出现故障,因为程序被移植到具有更高最低要求的新系统类型。
下面的 C 测试程序是根据 system() 编写的,但较低级别的症状是 execl() 系统调用失败。根据您测试的主机操作系统,当您给被调用程序的堆栈空间太小而无法启动时,您可能会在被调用程序中收到errno == E2BIG 或段错误。
构建:
$ CFLAGS="-std=c99 -D_POSIX_C_SOURCE=200809" make stacklim
这个问题与“To check the E2BIG error condition in exec”有切线相关,但我们的实际问题是不同的:我们对设置此限制导致的潜在可移植性问题感兴趣。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/wait.h>
#include <unistd.h>
static enum try_code {
raise_minimum,
lower_maximum,
failed_to_launch
}
try_stack_limit(rlim_t try)
{
// Update the stack limit to the given value
struct rlimit x;
getrlimit(RLIMIT_STACK, &x);
static int first_time = 1;
if (first_time) {
first_time = 0;
printf("The default stack limit here is %llu bytes.\n", x.rlim_cur);
}
x.rlim_cur = try;
setrlimit(RLIMIT_STACK, &x);
// Test the limit with a do-nothing shell launch
int status = system("exit");
switch (status) {
case 0: return lower_maximum;
case -1:
perror("Failed to start shell");
return failed_to_launch;
default:
if (WIFEXITED(status) && WEXITSTATUS(status) == 127) {
// system() couldn't run /bin/sh, so assume it was
// because we set the stack limit too low.
return raise_minimum;
}
else if (WIFSIGNALED(status)) {
fprintf(stderr, "system() failed with signal %s.\n",
strsignal(WTERMSIG(status)));
return failed_to_launch;
}
else {
fprintf(stderr, "system() failed: %d.\n", status);
return failed_to_launch;
}
}
}
int main(void)
{
extern char **environ;
size_t etot = 0;
for (size_t i = 0; environ[i]; ++i) {
etot += strlen(environ[i]) + 1;
}
printf("Environment size = %lu\n", etot + sizeof(char*));
size_t tries = 0;
rlim_t try = 1 * 1000 * 1000, min = 0, max = 0;
while (1) {
enum try_code code = try_stack_limit(try);
switch (code) {
case lower_maximum:
// Call succeded, so lower max and try a lower limit.
++tries;
max = try;
printf("Lowered max to %llu bytes.\n", max);
try = min + ((max - min) / 2);
break;
case failed_to_launch:
if (tries == 0) {
// Our first try failed, so there may be a bug in
// the system() call. Stop immediately.
return 2;
}
// Else, consider it a failure of the new limit, and
// assume we need to limit it.
case raise_minimum:
// Call failed, so raise minimum and try a higher limit.
++tries;
min = try > min ? try : min;
rlim_t next = max ?
min + ((max - min) / 2) :
try * 2;
if (next == try) {
printf("Min stack size here for exec is %llu.\n", max);
return 0;
}
else {
printf("Raising limit from %llu to %llu.\n", try, next);
try = next;
}
break;
default:
return 1;
}
}
}
【问题讨论】:
-
您是否尝试在启动时调用
getrlimit,保存值,然后在调用execl之前使用该值调用setrlimit? -
我什至不明白这个问题:'但是如果它增长太多并且离另一个内存区域太近,程序可能会将堆栈与另一个内存区域混淆。嗯..如何,不产生页面错误?
-
如果您试图通过设置资源限制来防止利用堆栈冲突,那么可以预期某些程序将无法在您指定的限制下运行。从本质上讲,这就是该方法在解决问题方面的有效方式——就其有效程度而言。尚不清楚您可以设置任何限制以允许所有好的程序充分保护免受攻击。当然,允许任何程序运行所需的任何东西似乎完全违背了目的。
-
@JohnBollinger:我们只对防止针对我们自己的程序的堆栈冲突攻击感兴趣。如果您尝试使用上面的测试程序对一个更重要的程序(例如,Vim)尝试一个太低的限制,您会注意到它在死前甚至没有启动。当然,您不能说所有程序在启动之前就开始在堆栈上分配兆数据?
-
IMO,这是对真正解决方案的创可贴:对数组索引进行适当的限制检查。只是不要通过检查限制超过堆栈上的固定数组大小(例如
int arr[100]),不要让 VLA 变得太大(也限制alloca),并限制递归,你不会得到堆栈溢出到堆区域(这将要求堆栈在碰撞之前在 64 位系统上增长到 TB)。大多数 pgms 都是为默认堆栈大小构建的(例如 Linux:8MB)——所以不要破坏它们。重新编码您的程序以检查所有限制,而不是使用rlimit。那么其他 pgms 呢?
标签: c stack posix limit portability