ARMCC/GCC下的stack protector

本文介绍了ARMCC编译器如何通过插入canaryword监控堆栈破坏来防止stackoverflow攻击。通过对比不同编译选项生成的汇编代码,展示了stackprotector的具体实现及作用。

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

Stack overflow攻击是一种很常见的代码攻击,armcc和gcc等编译器都实现了stack protector来避免stack overflow攻击。虽然armcc和gcc在汇编代码生成有些不同,但其原理是相同的。这篇文章以armcc为例,看一看编译器的stack protector。

armcc提供了三个编译选项来打开/关闭stack protector。

  • –no_protect_stack 关闭stack protector
  • –protect_stack 为armcc认为危险的函数打开stack protector
  • –protect_stack_all 为所有的函数打开stack protector

armcc如何防止stack overflow攻击?

armcc在函数栈中的上下文和局部变量之间插入了一个数字来监控堆栈破坏,这个值一般被称作为canary word,在armcc中将这个值定义为__stack_chk_guard。当函数返回之前,函数会去检查canary word是否被修改,如果canary word被修改了,那么证明函数栈被破坏了,这个时候armcc就会去调用一个函数来处理这种栈破坏行为,armcc为我们提供了__stack_chk_fail这个回调函数来处理栈破坏。

因此,在armcc打开- –protect_stack之前需要在代码中设置__stack_chk_guard和__stack_chk_fail。我从ARM的官网上摘抄了一段它们的描述。

void *__stack_chk_guard

You must provide this variable with a suitable value, such as a random value. The value can change during the life of the program. For example, a suitable implementation might be to have the value constantly changed by another thread.

void __stack_chk_fail(void)

It is called by the checking code on detection of corruption of the guard. In general, such a function would exit, possibly after reporting a fault.

armcc stack protector产生了什么代码来防止stack overflow?

首先来看一下写的一个c代码片段, 代码很简单,__stack_chk_guard 设置为一个常数,当然这只是一个例子,最好的方法是设置这个值为随机数。然后重写了__stack_chk_fail这个回调接口。test_stack_overflow这个函数很简单,仅仅在函数栈上分配了i和c_arr这两个局部变量,并对部分成员赋值。

void __stack_chk_fail()
{
    print_uart0("__stack_chk_fail()\n");
    while(1);
}

void *__stack_chk_guard = (void *)0;

int test_stack_overflow(int a, int b, int c, int d, int e)
{
    int i;
    int c_arr[15];
    int *p = c_arr;
    i = 15;
    c_arr[0] = 2;
    c_arr[1] = 3;
    return 0;
}

OK,首先看一下在–no_protect_stack情况下armcc产生的汇编代码,仅仅只是在栈上分配c_arr这个局部数组,而i这个变量则使用r1寄存器来保存。

60010044 <test_stack_overflow>:
60010044:   e92d4070    push    {r4, r5, r6, lr}
60010048:   e24dd03c    sub sp, sp, #60 ; 0x3c
6001004c:   e1a04000    mov r4, r0
60010050:   e1a05001    mov r5, r1
60010054:   e1a06002    mov r6, r2
60010058:   e59dc04c    ldr ip, [sp, #76]   ; 0x4c
6001005c:   e1a0200d    mov r2, sp
60010060:   e3a0100f    mov r1, #15
60010064:   e3a00002    mov r0, #2
60010068:   e58d0000    str r0, [sp]
6001006c:   e3a00003    mov r0, #3
60010070:   e58d0004    str r0, [sp, #4]
60010074:   e3a00000    mov r0, #0
60010078:   e28dd03c    add sp, sp, #60 ; 0x3c
6001007c:   e8bd8070    pop {r4, r5, r6, pc}

其栈上的内存map如下图所示
没有开启栈保护的函数栈
然后看一看使用–protect_stack_all选项编译后产生的汇编代码

600100a0 <test_stack_overflow>:
600100a0:   e92d47f0    push    {r4, r5, r6, r7, r8, r9, sl, lr}
600100a4:   e24dd040    sub sp, sp, #64 ; 0x40
600100a8:   e1a07000    mov r7, r0
600100ac:   e1a08001    mov r8, r1
600100b0:   e1a09002    mov r9, r2
600100b4:   e1a0a003    mov sl, r3
600100b8:   e59d6060    ldr r6, [sp, #96]   ; 0x60
600100bc:   e59f0094    ldr r0, [pc, #148]  ; 60010158 <c_entry+0x58>
600100c0:   e5904000    ldr r4, [r0]
600100c4:   e58d403c    str r4, [sp, #60]   ; 0x3c
600100c8:   e1a00000    nop         ; (mov r0, r0)
600100cc:   e1a00000    nop         ; (mov r0, r0)
600100d0:   e3a00002    mov r0, #2
600100d4:   e58d0000    str r0, [sp]
600100d8:   e3a00003    mov r0, #3
600100dc:   e3a05000    mov r5, #0
600100e0:   e58d0004    str r0, [sp, #4]
600100e4:   e59d003c    ldr r0, [sp, #60]   ; 0x3c
600100e8:   e1500004    cmp r0, r4
600100ec:   0a000000    beq 600100f4 <test_stack_overflow+0x54>
600100f0:   ebffffc2    bl  60010000 <__stack_chk_fail>
600100f4:   e1a00005    mov r0, r5
600100f8:   e28dd040    add sp, sp, #64 ; 0x40
600100fc:   e8bd87f0    pop {r4, r5, r6, r7, r8, r9, sl, pc}

两段代码主要的差异在于如下

600100bc:   e59f0094    ldr r0, [pc, #148]  ; 60010158 <c_entry+0x58>
600100c0:   e5904000    ldr r4, [r0]
600100c4:   e58d403c    str r4, [sp, #60]   ; 0x3c

这段代码很简单,就是从60010158 这个地址取出一个值,再将这个值作为地址取出他的值,将它保存到了sp, #60这个位置,这个位置就是位于上下文的下方和c_arr数组的上方。可以看一下此时的函数栈内存map是什么样子,如下图
开启栈保护的函数栈

还有一段差异代码如下,很简单就是在函数return之前拿出这个stack_chk_guard比较了一下,如果这个值被修改了就证明函数栈被破坏,如果没被修改就说明函数可以正常返回。

600100e4:   e59d003c    ldr r0, [sp, #60]   ; 0x3c
600100e8:   e1500004    cmp r0, r4
600100ec:   0a000000    beq 600100f4 <test_stack_overflow+0x54>
600100f0:   ebffffc2    bl  60010000 <__stack_chk_fail>

armcc中stack protector的作用

这个段落中写了一段代码来模拟这个stack overflow攻击,大部分代码与之前没有什么差异,在test_stack_overflow中*(p + 15) = 1234这一句表示修改stack_chk_guard,从图2中可以看到c_arr是15个整形变量数组,那么p+15就正好位于c_arr上方,即stack_chk_guard。同样根据上图推算出p+23就是栈中保存的返回地址,在这里将返回地址修改为attack_attack这个函数地址,来模拟栈被攻击后跳转到黑客想去运行的地址。attack_attack只是打印了一句话而已。

int test_stack_overflow(int a, int b, int c, int d, int e)
{
    int i;
    int c_arr[15];
    int *p = c_arr;
    i = 15;
    c_arr[0] = 2;
    c_arr[1] = 3;
    *(p + 15) = 1234;            /* modify the guard word, see fig.2*/
    *(p + 23) = (int)attack_attack;     /* modify return address as the attack function, see fig.2*/
    return 0;
}
int c_entry() 
{
    print_uart0("befroe test_stack_overflow\n");
    test_stack_overflow(1, 2, 3, 4, 5);
    print_uart0("after test_stack_overflow\n");
    return 0;
}
void attack_attack()
{
    print_uart0("attack attack!\n");
}

将代码编译后,运行于qemu-system-arm上,得到打印如下,正如我们所预期的一样,由于stack_chk_guard被修改了,证明函数栈已经被破坏,所以并没有运行到attack_attack函数,而是跳转到了__stack_chk_fail进行处理。
这里写图片描述

不过需要注意的是,如果攻击者能够绕过stack_chk_guard而去直接修改pc值,那么stack protector是没有效果的,任何编译器都是一样的。但其实由于stack overflow的特性,攻击者是很难绕过stack_chk_guard这个值而去直接修改pc。假设他能绕过stack_chk_guard,那么实际上他可以去任意修改栈中的数据,也就不需要使用stack overflow来进行攻击。还是以上面那段代码做一个实验,在test_stack_overflow函数中将 *(p + 15) = 1234注释到,那么最终还是能够运行到attack_attack函数。代码如下:

int test_stack_overflow(int a, int b, int c, int d, int e)
{
    int i;
    int c_arr[15];
    int *p = c_arr;
    i = 15;
    c_arr[0] = 2;
    c_arr[1] = 3;
    //*(p + 15) = 1234;          /*no modify the guard word, see fig.2*/
    *(p + 23) = (int)attack_attack;     /* modify return address, see fig.2*/
    return 0;
}
int c_entry() {
    print_uart0("befroe test_stack_overflow\n");
    test_stack_overflow(1, 2, 3, 4, 5);
    print_uart0("after test_stack_overflow\n");
    return 0;
}

实验结果
这里写图片描述

小结

armcc中stack protector通过一些简单的设置就能实现,其他编译器的实现的原理也是大同小异,至少我看过gcc的stack protector,它的实现方式是跟armcc一样的。

<think>我们正在使用KeilMDK进行STM32开发,默认使用的是ARMCC(即ARMCompiler5或6),而不是GCC。但是,用户要求添加GCC的编译选项`-fstack-protector-all`,这通常是在GCC工具链中使用的。因此,我们需要考虑两种情况:1.如果用户使用的是Keil默认的ARMCompiler,那么ARMCompiler并不直接支持`-fstack-protector-all`选项。但是,ARMCompiler有类似的栈保护功能,我们可以使用ARMCompiler的相应选项。2.如果用户已经在Keil中切换到了GCC工具链(如arm-none-eabi-gcc),那么就可以直接添加GCC的编译选项。根据引用[1],我们知道在Keil中可以切换编译器为GNUCompiler(即GCC)。因此,我们需要先确认用户使用的是哪种编译器。###步骤1:确认当前使用的编译器在Keil中,打开目标选项(TargetOptions),在“Target”标签页下的“CodeGeneration”部分,可以看到当前使用的编译器。如果是ARMCompiler,则显示为“Usedefaultcompilerversion5”或“Usedefaultcompilerversion6”。如果是GCC,则显示为“UseGNUCompiler”。###步骤2:根据编译器类型设置栈保护选项####情况A:使用ARMCompiler(ARMCC)ARMCompiler6(ARMCLANG)支持栈保护选项,类似于GCC的`-fstack-protector`。但是,选项名称不同。在ARMCompiler6中,可以使用以下选项:-`-fstack-protector-strong`:相当于GCC的`-fstack-protector-strong`-`-fstack-protector-all`:相当于GCC的`-fstack-protector-all`-`-fstack-protector`:相当于GCC的`-fstack-protector`因此,如果用户使用的是ARMCompiler6,可以在“C/C++”标签页的“MiscControls”框中输入`-fstack-protector-all`。但是,如果用户使用的是ARMCompiler5(即旧版的ARMCC),则不支持这些选项。ARMCompiler5没有内置的栈保护选项。在这种情况下,可能需要考虑升级到ARMCompiler6或者使用GCC。####情况B:使用GCC(arm-none-eabi-gcc)如果用户已经按照引用[1]的方法切换到了GCC工具链,那么可以直接在Keil中添加GCC的编译选项。在Keil中,打开目标选项(TargetOptions),然后:1.选择“C/C++”标签页。2.在“MiscControls”框中,添加`-fstack-protector-all`。注意:Keil在调用GCC时,会将这个选项直接传递给GCC编译器。###步骤3:验证设置编译项目后,可以通过查看生成的汇编代码来验证是否开启了栈保护。例如,在函数入口处应该可以看到对`__stack_chk_guard`的引用,以及在函数返回前有对栈的检查。另外,也可以使用引用[3]中的命令来反汇编生成的elf文件,查看是否有栈保护相关的代码。###注意事项1.使用栈保护功能需要运行时库的支持。在嵌入式系统中,如果使用标准的GCC库,通常已经包含了栈保护的支持。但是,如果使用的是自定义的运行时环境,可能需要提供`__stack_chk_guard`和`__stack_chk_fail`的实现。2.栈保护会增加代码大小和运行时间开销,对于资源受限的嵌入式系统需要权衡。###操作步骤总结1.打开Keil工程,进入目标选项(TargetOptions)。2.在“Target”标签页确认当前使用的编译器。3.根据编译器类型:-如果是ARMCompiler6:在“C/C++”标签页的“MiscControls”框中添加`-fstack-protector-all`。-如果是GCC:同样在“C/C++”标签页的“MiscControls”框中添加`-fstack-protector-all`。-如果是ARMCompiler5:则无法直接支持,建议切换编译器。4.重新编译工程,并验证栈保护是否生效。###相关问题1.如何在Keil中切换编译器GCC?2.栈保护选项(如`-fstack-protector-all`)在嵌入式系统中的优缺点是什么?3.如果使用ARMCompiler5,如何实现栈保护功能?</think>### 在 Keil 中设置 `-fstack-protector-all` 编译选项的方法 在 Keil MDK 中设置 GCC 的 `-fstack-protector-all` 选项需要以下步骤,因为 Keil 默认使用 ARM Compiler(而非 GCC),需先切换工具链: #### 📌 步骤 1:切换编译器为 GNU GCC 1. 打开 Keil 工程,进入 **Options for Target**(快捷键 `Alt+F7`) 2. 在 **Target** 选项卡 → **Code Generation** 区域: - 将 `Use Default Compiler Version 5/6` 改为 **Use GNU Compiler** - 在 `GNU-Tool Prefix` 中指定 GCC 工具链路径(如 `arm-none-eabi-`) - 在 `GCC Toolchain Path` 中设置工具链安装目录(需包含 `arm-none-eabi/bin` 子目录)[^1] #### ⚙ 步骤 2:添加编译选项 1. 切换到 **C/C++** 选项卡 2. 在 **Misc Controls** 框中添加: ```plaintext -fstack-protector-all ``` ![Keil 选项设置示意图](https://example.com/keil-option.png) (此选项会强制对所有函数启用栈溢出保护) #### 🔍 步骤 3:验证配置 1. 编译工程后,在 **Build Output** 窗口检查编译命令,应包含: ```bash arm-none-eabi-gcc ... -fstack-protector-all ... ``` 2. 使用反汇编验证(参考引用[3]): ```bash arm-none-eabi-objdump -S --demangle output.elf > disasm.txt ``` 在反汇编中搜索 `__stack_chk_guard` 确认保护代码已插入 #### ⚠ 注意事项 1. **依赖库支持**:需链接 `libssp.a`(栈保护库),在 **Linker** 选项卡添加: ```plaintext -lssp ``` 2. **资源开销**:该选项会增加代码大小(约 5-15%)和栈使用量,需评估资源限制 3. **ARM Compiler 替代方案**:若坚持用 ARMCC,可使用 `--protect_stack` 选项(功能类似但语法不同) > 📌 关键提示:栈保护原理是通过在函数栈帧中插入金丝雀值(canary),在函数返回前验证该值是否被篡改,能有效防御栈溢出攻击[^2]。 --- ### 🔍 相关问题 1. **`-fstack-protector-all` 和 `-fstack-protector` 有何区别?** (前者保护所有函数,后者仅保护含数组/局部变量的函数) 2. **如何在 Keil 中验证栈保护机制已生效?** (通过反汇编分析函数入口/出口指令,或故意制造栈溢出触发保护中断) 3. **嵌入式系统中使用栈保护会带来哪些性能影响?** (分析额外指令周期、RAM/ROM 占用增加量及中断响应延迟) 4. **Keil 的 ARM Compiler 是否支持类似 GCC 的栈保护选项?** (对比 ARMCC 的 `--protect_stack` 与 GCC 的实现差异) [^1]: 编译器切换步骤参考 Keil 官方文档《Managing Toolchains》 [^2]: GCC 栈保护机制详解:https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html [^3]: 反汇编验证方法源自 ARM 开发社区实践指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值