RTOS上栈破坏分析--编译器栈金丝雀保护

1.简述要求

  在进行分析RTOS平台上栈破坏相关的内容,首先理解一些必要概念是必须的。

1.1 栈的相关理解

PC,Program Counter, cpu当前正在执行的指令地址,cpu每执行一条指令,pc就跳到下一条指令的地址。 LR,Link Register,保存函数执行完后需要返回的地址。 SP,Stack Pointer,指向栈内存的当前顶部地址。

1.2 编译器ARMCC的栈金丝雀保护机制

  在armcc中可以允许启用--protect_stack 栈金丝雀保护,这个是针对栈内存保护的核心编译选项, 主要是用于检测、防御栈溢出、栈内存踩踏(如局部数组越界,栈缓冲区溢出)等问题。 基本原理

  • 编译时,在函数的栈帧中插入一个随机的“栈金丝雀(stack canary)”,位于局部变量于栈帧返回地址之间
  • 函数执行完毕返回前,检查金丝雀值是否被篡改。
    1.被篡改,说明栈内存被越界写入、踩踏,立即触发硬件异常或打印错误日志,终止程序运行
    2.未篡改,正常返回
    另外补充:这个是保护栈内存,对于堆内存问题(比如malloc越界)无效
    开启--protect_stack,要搭配--debug 开启调试符号。
    

2. 实例使用

  首先对于编译器,需要支持栈金丝雀保护,--protect_stack 对于armcc/armclang 有些差异,比如,对于RTOS平台需要定义 __stack_chk_fail()

ARMCC 5:需手动定义 __stack_chk_guard
ARMCC 6armclang):默认会生成 __stack_chk_guard,仅需实现 __stack_chk_fail

2.1 编译器编译参数设置

  以紫光展锐UWS6121E, cortex-a5架构,编译器是armcc,添加“--protect_stack” 开启栈金丝雀保护。 参考,省略部分参数

set(__CPU__ "cortex-a5")
set(CPU "--cpu")
set(APCS "--apcs")
set(VFP "--fpu VFPv2")
set(C_FORMAT_FLAG "--c99")
set(STACK_PROTECTED "--protect_stack")开启栈金丝雀保护,--debug搭配进行分析
set(CFLAGS "${CPU} ${__CPU__} ${C_FORMAT_FLAG} ${STACK_PROTECTED} --debug -O3")

2.2 RTOS平台上完成栈金丝雀异常函数

  栈金丝雀校验失败后的异常处理函数(必须全局可见,无 static) void __stack_chk_fail(void); 栈金丝雀的初始值,可设为固定值或随机值,通过这个值来判断是否发生了栈金丝雀破坏。 uint32_t __stack_chk_guard = 0x12345678;

typedef struct {
    uint32_t task_id;       // 触发异常的任务ID
    uint32_t pc;            // 程序计数器(当前执行地址)
    uint32_t lr;            // 链接寄存器(返回地址)
    uint32_t sp;            // 栈指针(任务栈地址)
    uint32_t stack_chk_guard;  // 被篡改的金丝雀值
} CrashContext_t;
CrashContext_t g_crash_ctx;


extern void SCI_Assert(void *exp, void *file, int line);
extern BLOCK_ID SCI_IdentifyThread(void);

//get sp lr pc
static __inline void SaveMainReg (uint32 *cur_sp, uint32 *cur_lr, uint32 *cur_pc)
{
#ifndef WIN32
#if __ARMCC_VERSION > 220000  /*lint !e553*/

#pragma arm
    __asm
    {
        MOV *cur_sp,    __current_sp()
        MOV *cur_lr,    __return_address()
        MOV *cur_pc,    __current_pc()
    }
#if  (1) //defined(RVCT_VERSION_V504)
#pragma arm
#else
#pragma thumb
#endif
#else
    __asm
    {
        MOV *cur_sp,    r13
        MOV *cur_lr,    r14
        MOV *cur_pc,    r15
    }

#endif // __ARMCC_VERSION
#endif // WIN32
}

// (栈金丝雀的初始值,可设为固定值或随机值,裸机场景固定值即可)
uint32_t __stack_chk_guard = 0x12345678;  // 自定义随机值,避免被轻易猜到

// 栈金丝雀校验失败后的异常处理函数(必须全局可见,无 static)
void __stack_chk_fail(void) {
    // 1. 可选:打印栈溢出告警(若有串口/日志模块)
    // SCI_TRACE_LOW("Stack corruption detected! __stack_chk_fail triggered\n");

    g_crash_ctx.task_id = SCI_IdentifyThread();
    // 2. 保存寄存器(通过内联汇编读取PC/LR/SP)
    SaveMainReg(&g_crash_ctx.sp, &g_crash_ctx.lr, &g_crash_ctx.pc);

    // 3. 保存被篡改的金丝雀值(若未被完全覆盖)
    extern uint32_t __stack_chk_guard;
    g_crash_ctx.stack_chk_guard = __stack_chk_guard;

    SCI_Assert("balabala", "protect_sysbol.c", __LINE__);
}

上面主要是在出现栈破坏后,在函数返回时,调用栈金丝雀值检测判断,看看是否调用__stack_chk_fail()

3.分析

  其实分析这个主要看实际发生场景,基于其原理,栈金丝雀值,位于局部变量和栈帧返回地址之间. 根据实际破坏程度,也就是LR是否被破坏严重

3.1 栈金丝雀值被篡改,但LR未被覆盖

  这种情况较好分析,因为LR没有被破坏,上面方法获取到LR后,可以通过LR分析出,函数执行完要返回的地址。 这种覆盖往往,都是小范围栈越界,提早发现出来,有助于排查bug。

3.2 栈金丝雀值+LR都被覆盖

  这种情况无法通过LR定位,因为越界写入范围很大,不仅覆盖了金丝雀值,也覆盖了栈帧中的LR值。 这个时候需要从__stack_chk_fail 中保存的PC寄存器+sp栈指针,手动解析栈内存,找到未被 覆盖的指令地址,逆向定位写坏栈的代码行(稍微复杂,需要一定的观察能力)

4.总结

  理解原理,--protect_stack 是在局部变量栈内存和栈返回地址间,插入一个标志,通过判断这个标准来看看,是否 有出现栈破坏,分析的话,也是需要通过仔细观察进行判断,但是用于检测代码是否出现了栈破坏,确实是一个非常 好的方法。