首页
社区
课程
招聘
[原创] linux 内核安全增强(一)— stack canary
发表于: 2021-12-11 16:46 22751

[原创] linux 内核安全增强(一)— stack canary

2021-12-11 16:46
22751

 版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/120318707


更多内容可关注微信公众号 

1. 栈生长方向与push/pop操作

    栈是一种运算受限的线性表, 入栈的一端为栈顶,另一端则为栈底, 其生长方向和操作顺序理论上没有限定. 而在aarch64平台上:

2. 返回地址的存储

    x86平台是call指令时自动push函数返回地址到栈; ret指令自动pop函数返回地址出栈;  这两步操作都是在callee执行前硬件自动完成的.

    而在arm/aarch64平台发生函数调用时(blx),硬件负责将函数的返回地址设置到通用寄存器LR(/X30)中, callee中的代码负责将LR保存到栈中(需保存的寄存器参考AAPCS标准)

 3. 函数栈分配

    在不考虑动态分配的情况下, 函数中使用的栈大小在编译阶段就已经确定了(见备注1), 一个aarch64中的典型的程序栈如下所示:

   和x86/arm平台的不同在于:

      在此两个平台中若发生了栈溢出则直接可以覆盖到当前函数的返回地址.

    最终的函数栈如上图所示, 由于变量是向高地址方向生长的,故:

  stack canary是一个比较久远的安全特性,linux内核在2.6版本便已经引入, 在5.0又引入了增强的per-task stack canary, 其原理比较简单,即: 

  stack canary并不能检测到所有的栈溢出问题, 只有在满足:

  两个前提条件时才能检测到栈溢出,故其并非一种理论上安全的防御方式,也只能针对顺序覆盖的栈溢出提供一定的缓解.

    虽然原理简单,但实现上还是要解决两个主要问题:

        函数入口需要向函数栈push一个原始的canary,函数出口需要将函数栈中的canary(后续称为stack_canary)和原始值做对比,在此过程中原始值需要保持不变并且可以被代码获取到:

      默认stack canary使用全局符号(变量) __stack_chk_guard 作为原始的canary(后续称为全局canary), 在gcc/clang中均使用相同的名字.

      per-cpu 变量的引入是为了实现per-task的stack canary, 每个cpu上同时只能运行一个进程/线程, per cpu变量可以随进程的切换而切换,故通过一个per-cpu变量完全可以为每个进程/线程解引用到不同的canary地址(后续称为per-cpu canary),以实现per-task的canary.

        通常stack canary的桩代码都是由编译器来插入的,但对具体硬件平台, 不同编译器的支持也有所不同

        gcc/llvm中编译选项-fstack-protector/-fstack-protector-strong均已支持, 开启后函数出入口会从全局变量__stack_chk_guard中获取全局canary

clang 主线目前也已支持-mstack-protector-guard* 系列选项,但目前尚无可用发行版[1]

clang --target=--target=aarch64-linux-android 中支持per cpu的stack canary,但其只能使用默认的 tpidr_el0系统寄存器作为索引, 偏移值也是默认的0x40

      arm linux kernel 通过一个gcc plugin(arm_ssp_per_task_plugin)基于per-cpu 寄存器sp实现了 per-task canary功能

四、编译器中全局canary的实现

    这里以aarch64平台,gcc + -fstack-protector-strong为例,其实现逻辑如下(源码分析见备注2):

函数出口对比全局canary和stack_canary是否还一致,一致则跳转到4)

检测到栈溢出, 调用__stack_chk_fail函数

    在aarch64的汇编代码如下:

    per-cpu canary时编译器会通过  *(reg + offset)的方式获取当前cpu上的canary(如下面例子中的 * (sp_el0  + 16), 而程序自身需要确保线程切换时per-cpu的canary也要随之切换, 在aarch64下的汇编代码如下(源码分析见备注2):

   linux内核中与stack canary相关的配置项主要有三个,分别是:

   1) CONFIG_STACKPROTECTOR:

       平台无关的编译选项,其决定是否开启 stack canary保护, 开启则默认指定编译选项 -fstack-protector,使用__stack_chk_guard 作为全局canary对比

   2) CONFIG_STACKPROTECTOR_STRONG

       平台无关的编译选项,其决定是否开启strong保护,开启则额外指定编译选项 -fstack-protector-strong.

   3) CONFIG_STACKPROTECTOR_PER_TASK

       平台相关的编译选项, 其决定是否开启内核per-task的stack canary保护(此时需编译器的per-cpu canary和对应硬件平台支持)

    全局canary对于内核来说并没有太多的工作,只需要在系统启动时设置好__stack_chk_guard并定义检测失败的回调__stack_chk_fail 即可,插桩代码均由编译器实现(见四), 代码如下:

2. per-task canary的实现

    per-task canary时内核除了初始化外还需要负责为每个进程生成随机的canary,并负责在进程切换时同步per-cpu的寄存器与进程的关系,此时内核新增的配置项和数据结构如下:

    根据编译选项可知,per-task模式下内核指定编译器通过 *(sp_el0 + TSK_STACK_CANARY) 来解引用per-cpu canary, sp_el0在内核中用来存储当前进程task_struct的指针,即对于内核来说对 *(sp_el0 + TSK_STACK_CANARY) 的解引用即相当于访问 current->stack_canary

    由于sp_el0在内核中是随着进程切换而切换的(见__switch_to),故stack canary特性并不需要做额外的操作,其只需要在每个线程创建时为其生成新的canary即可:

    故在aarch64内核中 per-task canary的思路可整理如下:

    ARM平台全局canary的实现和aarch64基本相同,都是基于变量"__stack_chk_guard"完成的,但由于在arm平台gcc并不支持-mstack-protector-guard系列选项,故ARM平台的per cpu canary是通过gcc plugin完成的, 在arm平台:

    此插件需要两个参数:

    插件主要代码如下:

    此插件的作用是将指定了-fstack-protector的源码中所有对 __stack_chk_guard的引用,都替换为对 ((sp & mask) + offset)的引用, sp&mask在linux内核中为当前进程thread_info的基地址, 需同时在thread_info中新增一个字段代表per-task canary:

   而内核同样仅需要在内核初始化和线程fork时为其新生成一个随机的canary即可,代码同arm64实现

   注: 此patch已经被作者Ard Biesheuvel 提交到gcc主线,期待在gcc 12中可以被合入[6]

备注(TLDR):

    通常不考虑动态分配的情况下一个函数执行过程中使用到的栈大小在编译阶段就已经确定了, pass_expand => expand_used_vars 中会为当前函数中用到的所有局部/临时变量分配栈空间, 而在rtl最后的会在当前的pro/epilogue中插入预留函数栈的代码:

      以prologue为例:

  stack canary的代码是在一个函数从gimple指令序列转换为rtl指令序列的过程中(pass_expand)实现的,主要包括三个步骤:

  1. 函数局部变量栈分配时为canary分配存储空间(stack_canary)

  2. 在函数开头(prologue)将全局canary保存到栈中(stack_canary)

     其中targetm.gen_stack_protect_set会根据当前编译选项决定具体插入何种代码:

  3. 在函数结尾(epilogue)检查stack_canary是否与全局canary一致,不一致则报错

    其中targetm.gen_stack_protect_test =>gen_stack_protect_test 生成的指令序列:

参考资料:

[1] rG0f417789192e

[2] Android逆向中的Canary机制 | cataLoc's Blog

[3] https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=359c1bf35e3109d2f3882980b47a5eae46123259

[4] https://github.com/bugsnag/llvm/blob/master/test/CodeGen/AArch64/stack-protector-target.ll     //LLVM TPIDR_EL0支持

[5] https://www.workofard.com/2018/01/per-task-stack-canaries-for-arm64/    

[6] [v5,1/1,ARM] Add support for TLS register based stack protector canary access - Patchwork


000005fc <test>:            
5fc:   b590            push    {r4, r7, lr}                /* 先push通用寄存器和函数返回地址 */
5fe:   b089            sub     sp, #36 ; 0x24              /* 再为局部变量预留存储空间 */
600:   af00            add     r7, sp, #0
602:   6078            str     r0, [r7, #4]
    ......
634:   3724            adds    r7, #36 ; 0x24
636:   46bd            mov     sp, r7
638:   bd90            pop     {r4, r7, pc}
0000000000400654 <test>:
                                                                             /* 预留栈         ||  在栈顶保存函数返回地址       */            
  400654:       a9bc7bfd        stp     x29, x30, [sp, #-64]!                /* sp = sp - 64;      sp[0] = x29; sp[1] = x30; */
  400658:       910003fd        mov     x29, sp
  40065c:       b9001fe0        str     w0, [sp, #28]
  400660:       b9801fe1        ldrsw   x1, [sp, #28]
  ......
  400680:       a8c47bfd        ldp     x29, x30, [sp], #64                  /* x29 = sp[0]; x30 = sp[1]; sp = sp +64;  */
  400684:       d65f03c0        ret

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2021-12-11 16:50 被ashimida编辑 ,原因:
收藏
免费 7
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//