【问题标题】:How can we know the minimum stack size needed by a program launched with exec()?我们如何知道使用 exec() 启动的程序所需的最小堆栈大小?
【发布时间】: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


【解决方案1】:

调用 exec() 时,调用进程的内存映射无效。它已更改以适应新的可执行文件。为新的可执行文件分配堆栈内存、堆内存、全局数据和代码内存。映射通常在链接时定义,内存在调用 main() 之前由语言库分配

参考:http://man7.org/linux/man-pages/man2/execve.2.html

【讨论】:

  • 这是真的它应该保留。拿我上面的测试结果来说,CentOS 7 上的 4-megs-and-change 值来自哪里,程序如何在调用 exec() 之前知道该值?
【解决方案2】:

您的程序已成功启动,因此您的程序被隐含地赋予正确的堆栈大小以依次启动其他程序:在您的程序启动期间,在设置新的下限之前获取当前限制:

struct rlimit g_default_stack_limit;  /* put in global scope */
getrlimit(RLIMIT_STACK, &g_default_stack_limit);

struct rlimit our_stack_limit;
memcpy(&our_stack_limit, &g_default_stack_limit, sizeof(our_stack_limit));
our_stack_limit.rlim_cur = 2000000;   /* some lower value */
setrlimit(RLIMIT_STACK, &our_stack_limit);

然后在启动外部程序之前恢复该初始值,并在创建子 fork() 或程序的同步调用(例如通过 system())退出后重新应用新限制:

struct rlimit our_stack_limit;
getrlimit(RLIMIT_STACK, &our_stack_limit);
setrlimit(RLIMIT_STACK, &g_default_stack_limit);

if (system(...) == 0) {
    ....
}

setrlimit(RLIMIT_STACK, &our_stack_limit);

此初始值可能是操作系统的默认值,也可能是调用您的程序的程序设置的限制。无论哪种方式,它几乎肯定是依次传递给程序调用的正确起始值。

【讨论】:

  • 您是否检查了您正在(隐式)传递给新可执行文件的环境的大小? (对于environ 中的每个元素ent,计数strlen(ent)+1+sizeof char*。)如果它在你的Centos 系统上是一个兆字节左右,那么谜团就解开了。
  • @rici 我已经在上面的测试程序中添加了一个计算。在需要超过 4 MB 堆栈才能执行另一个程序的 CentOS 系统上,环境大小约为 2.4 kB。但是在我测试的几乎未使用的 CentOS 7 虚拟机上,堆栈要求只有几十 kB。还有什么想法吗?
  • 不,但这里有一些东西我很想追查。我收集了一小部分关于神秘 E2BIG 的明显可靠的报告,但我唯一能重现的是环境或 argv 列表以某种方式膨胀的报告。
猜你喜欢
  • 2013-03-22
  • 2019-09-17
  • 2015-02-13
  • 2010-12-22
  • 2020-06-25
  • 1970-01-01
  • 2023-02-13
  • 1970-01-01
相关资源
最近更新 更多