openlang

-fstack-protector-strong 且没有 libc,%fs 加载 0x28 处 segfault

Freestanding 的 hex dumper,x86-64 Linux,自己写的 _start,直接用裸 syscall,链接参数是 -nostdlib -static -no-pie。编译链接都干净,没有未定义符号,但 ./hexd -v < /etc/hostname 在第一条 note() 输出之前就段错误了。

在 gdb 里看,出错位置在 hexd_main 的最开头,是一条通过 %fs 的加载,不是我写的,出错地址 0x28。别的代码都还没跑。加上 -fno-stack-protector 就正常。

我定义了我以为栈保护需要的两个符号,如下。

unsigned long __stack_chk_guard = 0x595e9fbd94fda766UL;

__attribute__((noreturn))
void __stack_chk_fail(void) { /* write to fd 2, sys_exit(134) */ }
CFLAGS = -std=c11 -O2 -Wall -Wextra -ffreestanding -fno-pic -fno-asynchronous-unwind-tables -fstack-protector-strong

guard 明明就作为普通全局变量放在那儿,为什么编译器还要去读 %fs?_start 里的栈对齐是对的,-mno-red-zone 也没变化。是不是我得自己先用 prctl 和 ARCH_SET_FS 设置好线程指针,才能调用任何东西?

从英语翻译查看原文

后续讨论 6

顺着彼此的依据读下去
  1. 固定链接

    x86-64 上默认不就是全局的 __stack_chk_guard 吗,%fs 那种是要主动开启的?(我是这么记的。)有没有类似 -mstack-protector-guard=global 的参数可以强制指定?

    从英语翻译查看原文
  2. 固定链接

    只对了一半,kiln。全局的 __stack_chk_guard 在其他一些目标上是默认做法,但在 x86-64 Linux 上 GCC 会在函数序言里生成 mov %fs:0x28, %rax 来加载 canary,返回前 ret 之前再读一次同一个槽位做比较。我 fault 的就是这条加载。没有设置 TLS 的话,%fs 基址是 0,所以地址就是 0x28,和 gdb 显示的完全一致。

    所以我定义的那个 guard 符号根本没人读,就在那儿放着。__stack_chk_fail 仍然需要,因为两种方式的失败路径是一样的。

    这个参数确实存在,就是 -mstack-protector-guard=global,我正在试。用上之后,序言应该变成从 __stack_chk_guard(%rip) 做一次普通加载,在 -fno-pic 下则是绝对地址,完全不会碰 %fs。我会先用 objdump -d 看一下反汇编再下结论,看一眼序言的反汇编比任何猜测都快。

    另一条路是在第一次调用 C 函数之前,自己用 prctl 设置好 FS 基址,然后在这块内存偏移 0x28 处放一个值。能行,但对一个独立环境的转储工具来说太折腾了。

    从英语翻译查看原文
  3. 固定链接

    对 prctl 这条路有点好奇,ARCH_SET_FS 能不能直接指向 .bss 里的一个静态数组?(偏移 0x28 就落在它里面。)

    从英语翻译查看原文
  4. 固定链接

    没错,可以,kiln。FS base 就是一个地址,所以 %fs:0x28 就是从它往后 0x28 字节处的内容。把数组开到至少 0x30 字节,让 FS 指向开头,在第一个 C 函数运行前往偏移 0x28 处存入你的 canary 值。

    给 lathe 挑个小毛病,这个 syscall 是 arch_prctl,x86-64 上编号 158,ARCH_SET_FS = 0x1002。普通的 prctl 做不到。汇编里就是这样。

    mov  eax, 158
    mov  edi, 0x1002
    lea  rsi, [rel tcb]
    syscall

    我读 man 手册的理解是,这块内存要页对齐,所以我会给它加 .align 4096,不过我还是会再确认一下。这一步必须在 _start 里、任何编译出来的代码之前完成,因为哪怕是带保护的小辅助函数也会崩。另外谢谢 lathe 提的 objdump -d,用来检查这个也行。

    从英语翻译查看原文
  5. 固定链接

    编译器到底为什么要读 %fs,明明 guard 就作为一个普通全局变量放在那儿?

    我也一样,用 -nostdlib 时碰到了一模一样的段错误。

    从英语翻译查看原文
  6. 固定链接

    谢了 whittle,就是 arch_prctl,我在 #1 和 #3 里写成 prctl 了,太马虎。这两个不能互换,158 是对的号。

    关于页对齐,我觉得内核并不要求。FS base 只是一个写进 MSR 的 64 位值,没有对齐检查,所以 .align 16 足够了。页对齐是 glibc 的习惯,不是这个 syscall 的要求。0x28 这个偏移只是相对于你传入的任意地址。

    走 FS 这条路的人还要注意一点,canary 槽位在 .bss 里,在你存入东西之前是零。零 canary 能正常跑,只是对任何把字符串写到它上面的情况起不到保护作用。在第一个受保护函数之前把它填好,想要真正随机的话可以用 auxv 里的 AT_RANDOM,那是内核放在初始栈上的 16 字节。

    另外,_start 本身不能带栈保护编译。如果它是 C 写的,就给它加 __attribute__((optimize("no-stack-protector"))),或者干脆用汇编写,否则它在设置任何东西之前就崩了。

    对于 dumper,我还是用 -mstack-protector-guard=global。

    从英语翻译查看原文

同一个问题的其他角度

  1. STM32H7 UART TX DMA 在开启 D-cache 时发出旧字节

    STM32H7,arm-none-eabi-gcc。用 DMA 发送 64 字节的帧。关闭缓存时一切正常。加上 -DUSE_DCACHE 后,分析仪显示帧是错的。复位后的第一帧是对的。之后的帧带着上一帧的字节,按 32 字节一块。有时前面是新的,尾部…

    回复 8最近活动 c
  2. 从 C 回调进来会丢掉我设置好的 arena

    一个 C 库会回调我的 Odin 代码,而回调看不到我在注册它之前设置好的 context。proc "c" 没有隐式 context,所以得自己建一个,而 runtime.default_context() 给的是默认分配器,不是注册代码里 con…

    回复 5最近活动 odinc
  3. TMR3 ISR 丢失捕获事件,尽管标志位是 w1c 且已清除

    回流焊炉控制器,Cortex-M0 兼容芯片,arm-none-eabi-gcc -O2,Fedora。TMR3 跑在 1 MHz。更新事件是 1 ms 的 tick,CH1 捕获市电过零,CH2 比较输出触发 triac。 问题是,50 Hz 下跑…

    回复 5最近活动 c