CC_STACKPROTECTOR防止内核stack溢出补丁分析

本文详细介绍了CC_STACKPROTECT补丁的工作原理及其在内核中的应用方式。该补丁通过在每个buffer后放置一个预设的stackcanary来防止内核堆栈溢出,一旦stackcanary值被破坏,则内核会触发保护机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CC_STACKPROTECT补丁是Tejun Heo在09年给主线kernel提交的一个用来防止内核堆栈溢出的补丁。默认的config是将这个选项关闭的,可以在编译内核的时候,
修改.config文件为CONFIG_CC_STACKPROTECTOR=y来启用。未来飞天内核可以将这个选项开启来防止利用内核stack溢出的0day攻击。
这个补丁的防溢出原理是: 在进程启动的时候, 在每个buffer的后面放置一个预先设置好的stack canary,你可以
把它理解成一个哨兵, 当buffer发生缓冲区溢出的时候, 肯定会破坏stack canary的值, 当stack canary的值被破坏的时候, 内核就会直接当机。那么是怎么判断stack canary
被覆盖了呢? 其实这个事情是gcc来做的,内核在编译的时候给gcc加了个-fstack-protector参数, 我们先来研究下这个参数是做什么用的。

先写个简单的有溢出的程序:
[wzt@localhost csaw]$ cat test.c
#include <stdio.h>
#include <stdlib.h>

void test(void)
{
    char buff[64];

    memset(buff, 0x41, 128);     //向64大小的buffer拷贝128字节, 肯定会发生缓冲区溢出。
}

int main(void)
{
    test();

    return 0;
}
[wzt@localhost csaw]$ gcc -o test test.c
[wzt@localhost csaw]$ ./test
段错误

反汇编看看:
[wzt@localhost csaw]$ objdump -d test > hex

08048384 <test>:
 8048384:       55                      push   %ebp
 8048385:       89 e5                   mov    %esp,%ebp
 8048387:       83 ec 58                sub    $0x58,%esp
 804838a:       c7 44 24 08 80 00 00    movl   $0x80,0x8(%esp)
 8048391:       00
 8048392:       c7 44 24 04 41 00 00    movl   $0x41,0x4(%esp)
 8048399:       00
 804839a:       8d 45 c0                lea    0xffffffc0(%ebp),%eax
 804839d:       89 04 24                mov    %eax,(%esp)
 80483a0:       e8 e3 fe ff ff          call   8048288 <memset@plt>
 80483a5:       c9                      leave
 80483a6:       c3                      ret

没什么特别的,我们在加上-fstack-protector参数看看:
[wzt@localhost csaw]$ gcc -o test test.c -fstack-protector
[wzt@localhost csaw]$ ./test
*** stack smashing detected ***: ./test terminated
已放弃
这次程序打印了一条堆栈被溢出的信息,然后就自动退出了。

在反汇编看下:
[wzt@localhost csaw]$ objdump -d test > hex1

080483d4 <test>:
 80483d4:       55                      push   %ebp
 80483d5:       89 e5                   mov    %esp,%ebp
 80483d7:       83 ec 68                sub    $0x68,%esp
 80483da:       65 a1 14 00 00 00       mov    %gs:0x14,%eax
 80483e0:       89 45 fc                mov    %eax,0xfffffffc(%ebp)
 80483e3:       31 c0                   xor    %eax,%eax
 80483e5:       c7 44 24 08 80 00 00    movl   $0x80,0x8(%esp)
 80483ec:       00
 80483ed:       c7 44 24 04 41 00 00    movl   $0x41,0x4(%esp)
 80483f4:       00
 80483f5:       8d 45 bc                lea    0xffffffbc(%ebp),%eax
 80483f8:       89 04 24                mov    %eax,(%esp)
 80483fb:       e8 cc fe ff ff          call   80482cc <memset@plt>
 8048400:       8b 45 fc                mov    0xfffffffc(%ebp),%eax
 8048403:       65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 804840a:       74 05                   je     8048411 <test+0x3d>
 804840c:       e8 db fe ff ff          call   80482ec <__stack_chk_fail@plt>
 8048411:       c9                      leave
 8048412:       c3                      ret

使用-fstack-protector参数后, gcc在函数的开头放置了几条汇编代码:
 80483d7:       83 ec 68                sub    $0x68,%esp
 80483da:       65 a1 14 00 00 00       mov    %gs:0x14,%eax
 80483e0:       89 45 fc                mov    %eax,0xfffffffc(%ebp)
将代码段gs偏移0x14内存处的值赋值给了ebp-4, 也就是第一个变量值的后面。

在call完memeset后,有如下汇编代码:
 80483fb:       e8 cc fe ff ff          call   80482cc <memset@plt>
 8048400:       8b 45 fc                mov    0xfffffffc(%ebp),%eax
 8048403:       65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 804840a:       74 05                   je     8048411 <test+0x3d>
 804840c:       e8 db fe ff ff          call   80482ec <__stack_chk_fail@plt>
在memset后,gcc要检查这个操作是否发生了堆栈溢出, 将保存在ebp-4的这个值与原来的值对比一下,
如果不相同, 说明堆栈发生了溢出,那么就会执行__stack_chk_fail这个函数, 这个函数是glibc实现的,
打印出上面看到的信息, 然后进程退出。

从这个例子中我们可以看出gcc使用了-fstack-protector参数后,会自动检查堆栈是否发生了溢出, 但是有一个前提就是
内核要给每个进程提前设置好一个检测值放置在%gs:0x14位置处, 这个值称之为stack canary。所以我们可以看到防止
堆栈溢出是由内核和gcc共同来完成的。

gcc的任务就是放置几条汇编代码, 然后和%gs:0x14位置处的值进行对比即可。 主要任务还是内核如何来设置stack canary, 也是
CC_STACKPROTECTOR补丁要实现的目的, 下面我们仔细来看下这个补丁是如何实现的。

既然gcc硬性规定了stack canary必须在%gs的某个偏移位置处, 那么内核也必须按着这个规定来设置。

对于32位和64位内核, gs寄存器有着不同的功能。

64位内核gcc要求stack canary是放置在gs段的40偏移处, 并且gs寄存器在每cpu变量中是共享的,每cpu变量irq_stack_union的结构如下:

arch/x86/include/asm/processor.h

union irq_stack_union {
        char irq_stack[IRQ_STACK_SIZE];
        /*
         * GCC hardcodes the stack canary as %gs:40.  Since the
         * irq_stack is the object at %gs:0, we reserve the bottom
         * 48 bytes of the irq stack for the canary.
         */
        struct {
                char gs_base[40];
                unsigned long stack_canary;
        };
};

DECLARE_PER_CPU_FIRST(union irq_stack_union, irq_stack_union);
gs_base只是一个40字节的站位空间, stack_canary就紧挨其后。
并且在应用程序进出内核的时候,内核会使用swapgs指令自动更换gs寄存器的内容。

32位下就稍微有点复杂了。由于某些处理器在加载不同的段寄存器时很慢, 所以内核使用fs段寄存器替换了
gs寄存器。 但是gcc在使用-fstack-protector的时候, 还要用到gs段寄存器, 所以内核还要管理gs寄存器,

我们要把CONFIG_X86_32_LAZY_GS选项关闭, gs也只在进程切换的时候才改变。 32位用每cpu变量stack_canary保存stack canary。


前面讲过当gcc检测到堆栈溢出的时候, 会调用glibc的__stack_chk_fail函数, 但是当内核堆栈发生溢出的时候,
不能调用glibc的函数,所以内核自己实现了一个__stack_chk_fail函数:

kernel/panic.c

#ifdef CONFIG_CC_STACKPROTECTOR

/*
 * Called when gcc's -fstack-protector feature is used, and
 * gcc detects corruption of the on-stack canary value
 */
void __stack_chk_fail(void)
{
        panic("stack-protector: Kernel stack is corrupted in: %p\n",
                __builtin_return_address(0));
}
EXPORT_SYMBOL(__stack_chk_fail);

#endif

当内核堆栈发生溢出的时候,就会执行__stack_chk_fail函数, 内核当机。 这就是这个补丁的原理,不懂的同学请参考:
http://git.kernel.org/?p=linux/kernel/git/next/linux-next.git;a=commitdiff;h=60a5317ff0f42dd313094b88f809f63041568b08 


<think>嗯,用户想了解Linux内核中的CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG和CONFIG_STACKPROTECTOR_PER_TASK这两个配置选项的作用和用途。首先,我需要回忆一下内核中栈保护机制的相关知识,特别是Stack Protector(栈保护)功能。 我记得栈保护主要是为了防止溢出攻击,通过在函数栈帧中插入一个随机的canary值,在函数返回前检查这个值是否被修改。如果被修改,说明发生了栈溢出,触发__stack_chk_fail,导致内核panic。引用[1]中的代码就是处理这种情况的。 接下来,用户提到的两个配置选项。先看CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG。这个配置选项应该和编译器是否支持使用系统寄存器来存储canary值有关。在ARM架构中,比如ARM64,可能有专门的系统寄存器来存储canary,这样可以提高性能,避免从内存中读取带来的开销。需要确认这个选项是否是编译器特性,表示编译器是否支持生成这样的代码。例如,如果编译器能够使用类似sysreg来存储canary,内核就可以启用这种优化方式。 然后是CONFIG_STACKPROTECTOR_PER_TASK。这个选项听起来像是为每个任务(进程/线程)生成不同的canary值。默认情况下,栈保护可能在内核启动时生成一个全局的canary值,所有任务共享。但如果启用这个选项,每个任务都有自己的canary,这样即使攻击者成功获取了一个任务的canary,也无法利用它攻击其他任务,提高了安全性。不过,这可能会增加上下文切换的开销,因为每次切换任务时都需要更新canary值。需要查看相关的内核代码,比如thread_info或task_struct结构中是否有存储canary的字段,引用[2]中的thread_union可能与之相关,因为线程的栈和结构体有关。 另外,用户提供的引用中提到了ARM64架构和内核版本5.14,所以需要确保这些配置选项在该架构下的具体实现。例如,ARM64可能通过系统寄存器来存储canary,而x86可能使用gs段寄存器或其他方式。CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG可能是在检测编译器是否支持ARM64的系统寄存器特性,如GCC的相关补丁是否被应用。 还需要考虑这些配置选项的依赖关系,比如是否互斥,或者需要其他选项的配合。例如,CONFIG_STACKPROTECTOR_PER_TASK可能依赖于某些架构特定的支持,或者需要编译器支持动态生成canary值。同时,可能涉及到内核的线程信息结构(thread_info)和任务结构(task_struct)的修改,引用[2]中的thread_union可能存储了每个任务的canary值。 总结来说,CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG是启用基于系统寄存器的栈保护机制,提升性能;而CONFIG_STACKPROTECTOR_PER_TASK是增强安全性,为每个任务单独设置canary。需要将这些内容用中文清晰解释,并确保引用正确的参考资料,比如引用[1]中的栈保护处理函数和引用[2]中的内核栈结构。</think>### Linux内核中`CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG`与`CONFIG_STACKPROTECTOR_PER_TASK`的解析 #### 1. **`CONFIG_CC_HAVE_STACKPROTECTOR_SYSREG`** - **用途**:此配置选项用于检测编译器是否支持通过**系统寄存器**(System Register)实现栈保护(Stack Protector)。在ARM64架构中,若编译器支持,内核可通过系统寄存器(如`ARM64_SYSREG_SP_EL0`)存储栈保护金丝雀值(Canary),而非传统的全局变量方式,从而提升性能并减少内存访问开销[^3][^4]。 - **依赖条件**:需同时满足: 1. 架构支持系统寄存器存储Canary(如ARM64)。 2. 编译器(如GCC)具备相关功能支持。 - **优势**:通过硬件加速的寄存器访问,减少内核态栈保护的性能损耗。 #### 2. **`CONFIG_STACKPROTECTOR_PER_TASK`** - **用途**:启用**“每任务栈保护”**机制,为每个进程/线程生成独立的Canary值。传统实现中,所有任务共享全局Canary值,攻击者可利用此特性绕过保护。此选项通过为每个任务分配唯一Canary,增强安全性[^1][^2]。 - **实现原理**:Canary值存储在任务的元数据中(如`struct task_struct`或`thread_info`),上下文切换时动态更新。例如,ARM64架构中通过`thread_info->stack_canary`字段实现。 - **代码关联**: ```c // 任务切换时更新Canary(伪代码) __switch_to(next_task) { write_sysreg(next_task->thread_info.stack_canary, SP_EL0); } ``` - **性能影响**:因上下文切换时需额外操作Canary,可能引入轻微开销。 #### 3. **配置关系** - **互斥性**:`CONFIG_STACKPROTECTOR_PER_TASK`需架构支持动态Canary分配,可能与某些栈保护模式不兼容。 - **典型场景**: - 安全敏感场景:同时启用两者,兼顾性能与安全。 - 嵌入式系统:若编译器不支持系统寄存器,则仅启用基础栈保护。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值