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 6(armclang):默认会生成 __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 是在局部变量栈内存和栈返回地址间,插入一个标志,通过判断这个标准来看看,是否 有出现栈破坏,分析的话,也是需要通过仔细观察进行判断,但是用于检测代码是否出现了栈破坏,确实是一个非常 好的方法。
您还没有登录,请您登录后发表评论。