广告

谈谈QNX安全之软件编译安全

2023-03-13 汽车电子与软件 阅读:
QNX之所以那么小,是因为作为微内核,它将驱动、系统服务、应用、协议栈、文件系统等都剔除在内核外,以可加载模块的形式运行,QNX内核只完成最基础的服务,如时钟同步、进程间通信、进程调度等。Linux作为宏内核,包含了许多服务,如文件系统、网络、驱动程序等。
   QNX作为微内核系统,被广泛的应用在车载娱乐系统、Tbox和智驾系统中。相对于Unix这类宏内核系统,QNX的微内核结构只有几十KB,因此启动速度非常快,稳定性也非常高。那么QNX有哪些安全机制可以使用呢?
     笔者在此推出QNX安全系列,给大家介绍下QNX的安全防护技术,本篇是QNX安全系列的第一篇,我们重点讲述软件编译保护机制。在进入正文之前,笔者还是给大家简单介绍下QNX与Linux的区别。

1. QNX与Linux的区别

如图12所示,QNX之所以那么小,是因为作为微内核,它将驱动、系统服务、应用、协议栈、文件系统等都剔除在内核外,以可加载模块的形式运行,QNX内核只完成最基础的服务,如时钟同步、进程间通信、进程调度等。Linux作为宏内核,包含了许多服务,如文件系统、网络、驱动程序等。
1 QNX内核架构
2 Linux内核架构

2. 软件编译保护

Qnx为我们提供了很多安全编译选项,这些选项有的可以帮我们在开发时定位问题,编译出更健壮的代码,有的可以帮助我们对软件进行保护。典型的有下面几个编译选型:
  • 编译时警告选项
    检测到易受攻击的代码时输出警告。有问题的代码容易崩溃、产生错误或意外行为,或为攻击创造机会,在开发阶段如果能充分识别并处理这些信息,能显著提高我们的代码健壮性,典型的编译如表1所示。
     
    1 QNX警告编译选项
    编译选项
    描述
    -Wall
    启用有关可疑代码构造的附加警告。包括 -Wformat,它检查对 printf() scanf() 样式函数的调用,以确保参数与格式字符串匹配。
    -Wcast-align
    如果指针cast导致目标所需的对齐方式增加,则发出警告。
    -Wcast-qual
    每当转换指针的方式从目标type中删除type限定符时发出警告。
    -Wconversion
    警告可能改变值的隐式转换。
    -Wduplicated-branches
    if-else 具有相同的分支时发出警告。
    -Wduplicated-cond
    警告 if-else-if 链中的重复条件。
    -Werror
    将警告视为错误,使构建失败。可能不适用于所有环境。
    -Wextra
    启用 -Wall 未启用的其他警告。
    -Wfloat-equal
    如果在相等比较中使用浮点值,则发出警告。
    -Wformat=2
    检查对 printf() scanf() 样式函数的调用,以确保参数与格式字符串 (-Wformat) 匹配,以及额外的格式检查 (-Wformat-nonliteral-Wformat-security-Wformat-y2k)
    -Winit-self
    警告自己初始化的未初始化变量(需要 -Wuninitialized,包含在 -Wall 中)。
    -Wlogical-op
    警告表达式中逻辑运算符的可疑使用。
    -Wmissing-declarations
    如果在没有先前声明的情况下定义了全局函数,则发出警告。
    -Wmissing-prototypes
    如果缺少原型,请发出警告。
    -Wnull-dereference
    如果编译器检测到由于取消引用空指针而触发错误或未定义行为的路径,则发出警告。
    -Wpointer-arith
    警告任何取决于“size of”函数type void
    -Wshadow
    每当局部变量或type声明遮蔽另一个变量、参数、type或类成员(在 C++ 中),或者当一个内置函数被遮蔽时发出警告。
    -Wsuggest-attribute=format
    警告可能成为格式属性候选的函数指针。设置格式属性后,-Wformat=2 标志会查找函数调用的问题。
    -Wswitch-default
    每当 switch 语句没有默认情况时发出警告。
    -Wswitch-enum
    只要 switch 语句具有枚举type的索引并且缺少该枚举的一个或多个命名代码的case,就会发出警告。
    -Wtrampolines
    当为指向嵌套函数的指针生成“trampolines”时发出警告。(trampoline是运行时取嵌套函数地址时在栈上创建的一小段数据或代码,用于间接调用嵌套函数。)
    -Wunreachable-code
    如果编译器检测到永远不会执行的代码,则发出警告。
    -Wwrite-strings
    如果 const char * 转换为(非 constchar *,则发出诊断消息。
    • 地址无关代码DJWednc

     
    对可执行文件、库文件编译时开启下面的编译选型,并启用ASLR。启用该选项后系统对可执行文件、库文件装载时地址空间布局会随机化。一般而言,QNX会默认开启-fPIC-fPIEASLR
    2 地址无关代码编译选项
    编译选项
    描述
    -fPIC
    将库编译为与位置无关的代码 (PIC)
    -fPIE
    将可执行文件编译为与位置无关(与位置无关的可执行文件 (PIE))。
    在这里简单介绍下ASLRASLR,即地址空间布局随机化(Address Space Layout Randomization),如图3所示,每次加载可执行文件时,地址空间布局随机化会改变数据和指令的位置。要支持 ASLR,二进制文件必须编译为与位置无关,因此需要使用 -fPIC编译库,使用-fPIE编译二进制文件。

    DJWednc

    3 地址随机化效果
    地址空间布局随机化针对以下系统元素实施:
    Ø可执行代码——由动态加载器加载的标记为可执行的程序段。应用程序必须使用 PIE 编译。
    Ø堆栈——页面和页面内的偏移量都是随机的。
    Ø——页面是随机的。
    Ø共享内存——多个进程之间共享的内存映射到每个进程中不同的随机虚拟地址。
    Ølibc 和动态加载器——libc 的位置和动态加载器的位置在进程的虚拟地址空间中都是随机的。虽然 libc 和动态加载器等共享对象映射到随机地址,但如果加载顺序保持不变,则它们相对于彼此映射到相同地址。
    Ø命令行参数——命令行参数在进程的虚拟地址空间中的位置是随机的。
    • 堆栈保护
    编译时针对应用程序、共享库使用,在函数返回之前检查堆栈canary以确保它们没有被修改(例如,由于堆栈分配的变量上的缓冲区溢出)。对堆栈上的堆栈canary的任何修改都将导致进程中止。可用的编译选项如表3所示。
    3 堆栈保护编译选项
    编译选项
    描述
    -fstack-protector
    保护在其堆栈上声明长度为 8 个或更多字节的字符数组的任何函数
    -fstack-protector-all
    保护所有函数
    -fstack-protector-strong
    默认,在 -fstack-protector -fstack-protector-all 之间进行平衡,此option的目的是通过扩大堆栈保护范围而不将其扩展到程序中的每个函数来获得性能,同时牺牲很少的安全性。
    • 阻止未定义符号DJWednc

    编译时防止目标文件中有未定义符号,通过指定-Wl,-z,defs选项,这也是减少攻击面的措施之一。当然也可以不指定,未定义符号在进行代码审计后也基本能识别出。
    4 阻止未定义符号编译选项
    编译选项
    描述
    -Wl,-z,defs
    防止目标文件中的未定义符号
    • GOT表保护DJWednc

     
    编译时对可执行指定表5中编译选项,可利用RELRO技术,在动态加载程序完成加载和链接可执行文件后,编译器可以将可执行文件的重定位部分标记为只读 如果出现 .bss 或数据溢出错误,RELRO(只读重定位)保护 ELF 二进制文件中的全局偏移表 (GOT) 不被覆盖。
    5 GOT表保护编译选项
    编译选项
    描述
    -Wl,-z,relro
    使能partial RELRO,不会将整个GOT作为read-only
    -Wl,-z,now
    使能full RELRO,使进程链接表 (PLT) 以及 .got .dtors 文件成为只读文件。相对于partial RELRO,会影响性能
    检查
    qchecksec命令
    • 检查未定义行为DJWednc

    编译时启用sanitizer编译代码,从而增加在代码中发现错误的可能性。目前,QNX Neutrino 支持 Undefined Behavior Sanitizer (UBSAN)
    6 检查未定义行为选项
    编译option
    描述
    --fsanitize=undefined
    启用 UBSAN。各种计算被用来检测运行时未定义的行为。
    在此强调一下,

    3.查看编译时保护选项

    上面介绍了那么多编译选项,特别是对于堆栈保护、GOT表保护、地址无关代码,我们如何识别运行的应用程序或库文件已经开启了对应的编译呢?笔者在此推荐两种方法:
    • 通过readelf读取
     
    readelf是一个可用于检查ELFExecutable and Linkable Format)文件的工具,该文件格式是LinuxQNX等操作系统上可执行文件和共享库的标准格式之一。
    检查项
    步骤
    期望结果
    fPIE/fPIC
    readelf -d | grep 'FL_PIC|FL_PIE'
    是要检查的二进制文件
    如果该文件已启用fpicfpie,则输出应包含以下内容:
    0x0000000E (FLAGS)              PN_FLAGS 0x7:
    FL_PIE|FL_EXEC_PSEUDO_RELOCS|FL_EXEC_STACK
    堆栈保护
    readelf -s | grep '__stack_chk_fail'
    输出包含__stack_chk_fail符号,则表示堆栈保护已启用
    GOT表保护
    readelf -r | grep '__guard'
    输出包含__guard符号,则表示GOT表保护已启用
    • 通过qchecksec读取DJWednc

     
    类似于Linux中的checksecqchecksec是一个用于检查二进制文件的安全性选项的工具。可以从github上的qchecksec存储库下载qchecksec源代码,然后使用GNU Autotools进行编译和安装。一旦安装完成,可以使用qchecksec命令来检查二进制文件的安全性选项。例如,要检查名为test的二进制文件的选项,可以运行以下命令:
    qchecksec test
    这将输出有关二进制文件的各种安全选项的信息。例如,以下是一些可能的输出:
    RELRO STACK  CANARY  NX  PIE  RPATH    RUNPATH  FORTIFY  Fortified
    Full No   NX enabled    PIE enabled    No         No        Yes     2
    输出中的每一列都对应于一种安全选项,其中:
    ·RELRO:表示RELRO保护的级别。
    ·STACK CANARY:表示堆栈保护是否已启用。
    ·NX:表示内存不可执行是否已启用。
    ·PIE:表示位置无关执行是否已启用。
    ·RPATH:表示是否设置了RPATH
    ·RUNPATH:表示是否设置了RUNPATH
    ·FORTIFY:表示是否启用了FORTIFY Source
    输出中的最后一列"FORTIFIED"表示是否已应用FORTIFY_SOURCE保护。如果数字大于0,则该文件已使用FORTIFY_SOURCE进行了保护,并且数字表示保护级别。

    小结

    这些编译器选项并不意味着在量产二进制文件中启用。 每个选项都针对代码的不同方面。 要充分利用这些选项的功能,请确保检测代码像在量产环境一样被执行。
     
责编:Ricardo
文章来源及版权属于汽车电子与软件,EDN电子技术设计仅作转载分享,对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。如有疑问,请联系Demi.xia@aspencore.com
汽车电子与软件
汽车电子与软件
  • 微信扫一扫
    一键转发
  • 最前沿的电子设计资讯
    请关注“电子技术设计微信公众号”
广告
广告
热门推荐
广告
广告
EE直播间
在线研讨会
广告
面包芯语
广告
向右滑动:上一篇 向左滑动:下一篇 我知道了