简介

iOS 中,有类过错或许并不常见:____stack_chk_fail。假如引进过 c 相关的代码,而且该代码中履行的时分呈现了栈溢出的状况,就有或许在仓库中呈现 ____stack_chk_fail。

从名字上来看,这是栈查看失利的时分抛出的反常。假如咱们扩大查找规模,能够发现各个体系中都有针对这个反常的阐明,引申到如栈溢出进犯等场景。那在 iOS 中,这个过错一般代表什么呢?

iOS 中,这个办法和它在其他 c 环境中的作用类似:用于检测回来值是否被修正了。它的基础逻辑如下:在函数开头的方位,分配出一个专门的栈空间,存储一个肯定不会改动的 canary 值,如 pc 寄存器的地址,这个方位一般坐落函数和回来值之间的方位;在回来时查看这个值,假如这个值被改动了,阐明有意外的地方修正了这个值,所以直接抛出 ____stack_chk_fail。

示意图来自 nocbtm.github.io/2020/04/28/…

 Low Address |                 |
             +-----------------+
     esp =>  | local variables |
             +-----------------+
             |    buf[0-3]     |
             +-----------------+
             |    buf[4-7]     | <= buffer overflow,顺序往下写,导致覆盖了 canary 值
             +-----------------+
             |     canary      |
             +-----------------+
     ebp =>  |     old ebp     |
             +-----------------+
             |   return addr   |
             +-----------------+
             |      args       |
             +-----------------+
High Address |                 |

一般来说假如经过栈溢出进犯修正到回来地址 addr,就会把它前面的所有值都修正(经过 index 溢出的办法,往超出分配规模的内存中写入值,所以是会接连写入)。由于不知道 canary 值是什么,修正的时分就无法写入相同的值,从而检测出 canary 被修正了。

这儿的 EBP 指的是用于康复回来办法仓库的指针。

内存问题和常见解决办法

咱们先来了解一下现在关于内存问题的常见解决办法,这些咱们或许都有听说过。

软件安全中,内存问题是一个比较常见的问题。进犯者能够经过修正内存来到达控制程序履行流程的意图。常见的办法包含缓冲区溢出进犯(buffer overflow),经过溢出来写入数据,修正函数的回来地址。现在也有三种常见的防护办法:

  1. 将敏感数据和指针放入只读区:对静态指针作用很好,但对动态指针没有作用。
  2. 运用指针前验证:Control Flow Integrity (CFI) 和 Return Oriented Programming (ROP) 会验证一些跳转和回来地址的特点,来防止问题。
  3. 随机化技能:包含对堆和栈的随机,使地址更难找到;Address Space Layout Randomization (ASLR) 地址空间布局随机化:iOS 4.3 引进的机制,每次发动都会加一个随机的偏移量,函数的调用会带上这个偏移量,更难获取要修正的地址。某些栈防护战略是建立在不可猜测性上的,比方上述提到的 ____stack_chk_fail。

这些战略一般需求合作运用,当时的防护战略规划也是建立在多个战略的组合运用上。

Demo 验证

咱们直接跑个 demo 看看检测作用,运行后能够看到 __stack_chk_fail 的仓库。

github.com/wiilen/stac…

依据运用的 c 函数,实测会刺进不同的栈维护办法。这儿尝试了几个或许导致 overflow 的 c 函数。

strcpy、memcpy、sprintf

这些个办法会刺进 __xxx_chk 来做校验,比方 __strcpy_chk,逻辑也比较简单,验证 copy 目标的长度,短的话就履行 __chk_fail_overflow ()。类似的还有 memcpy、sprintf 函数,都被换为类似的 chk 函数用来维护溢出的状况。

以下代码来自苹果的开源代码 Libc 1534.81.1 感兴趣也能够直接阅览源码。

github.com/apple-oss-d…

#ifndef UNIFDEF_DRIVERKIT
#if __has_builtin(__builtin___strcpy_chk) || defined(__GNUC__)
#undef strcpy
/* char *strcpy(char *dst, const char *src) */
#define strcpy(dest, ...) \
        __builtin___strcpy_chk (dest, __VA_ARGS__, __darwin_obsz (dest))
#endif
char *
__strcpy_chk (char *restrict dest, char *restrict src, size_t dstlen)
{
  // stpcpy returns a pointer to the \0
  size_t len = stpcpy(dest, src) - dest + 1;
  if (__builtin_expect (dstlen < len, 0))
    __chk_fail_overflow ();
  if (__builtin_expect (__chk_assert_no_overlap, 1))
    __chk_overlap(dest, len, src, len);
  return dest;
}

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

未敞开 Stack Protection

这儿运用 for 循环来检测这个问题,能够看到呈现了 EXC_BAD_ACCESS,内存被改坏了。

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

咱们阅览时或许发现图里的仓库也丢失了,这是由于 x29/x30 写坏了,记录仓库靠的是 x30 来回溯,所以这儿的指针写坏也就无法回溯了。

往后修正值都为 ‘a’,这儿发现报的错不是 __stack_chk_fail,而是 EXC_BAD_ACCESS,地址则是被 ‘a’ 覆盖了的地址 0x61616161。(‘a’ ascii 码为 0x61)。这儿阐明这样写实际上没有刺进 __stack_chk_fail。

敞开 Stack Protection

需求在 Build Settings 中加上 -fstack-protector-all 来敞开。

注:由于 Stack Protection 默许只对 vulnerable function 敞开,为了展现敞开后详细仓库,这儿运用了 -all 参数。

经过在控制台输入 clang --help | grep stack-protect,能够看到相关的类型:

  -fno-stack-protector      Disable the use of stack protectors
  -fstack-protector-all     Enable stack protectors for all functions
  -fstack-protector-strong  Enable stack protectors for some functions vulnerable to stack smashing. Compared to -fstack-protector, this uses a stronger heuristic that includes functions containing arrays of any size (and any type), as well as any calls to alloca or the taking of an address from a local variable
  -fstack-protector         Enable stack protectors for some functions vulnerable to stack smashing. This uses a loose heuristic which considers functions vulnerable if they contain a char (or 8bit integer) array or constant sized calls to alloca , which are of greater size than ssp-buffer-size (default: 8 bytes). All variable sized calls to alloca are considered vulnerable. A function with a stack protector has a guard value added to the stack frame that is checked on function exit. The guard value must be positioned in the stack frame such that a buffer overflow from a vulnerable variable will overwrite the guard value before overwriting the function's return address. The reference stack guard value is stored in a global variable.

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

敞开之后就会发现,仓库中呈现了 __stack_chk_fail,阐明溢出的问题被检测到了。

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

常见栈维护下刺进的汇编

在看过上面的 demo 之后,咱们能够更进一步,看看编译器是如何刺进 __stack_chk_fail 的。

注:按照之前在项目中遇到的状况,stack protection 默许会针对 vulnerable function 敞开,行为比较契合 -fstack-protector 的行为,所以假如没有手动填入其他的 stack-protector 参数,也有或许会看到被维护的栈。

苹果在 WWDC 2018 中提到,Xcode 会默许敞开 stack protect devstreaming-cdn.apple.com/videos/wwdc… ,视频备份 wwdctogether.com/wwdc2018/40… ,也便是至少在 Xcode 9 开始就有了。中文解析能够参阅 iweiyun.github.io/2018/10/15/… 。

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

Swift 本身是一门比较安全的语言,但也有 case 或许导致内存问题。这儿有相关的评论:forums.swift.org/t/stack-pro… ,比方 unsafeMutablePointer。

弥补:详细编译器用哪种校验办法都能够,基本都是栈上额外放置一个不应该被写的数据,然后函数回来前检测一下。看 Clang 实现是,往 x19 寄存器,和栈上校验用变量存放了相同值,退出前校验值相同。

这儿截取了高通文档中描绘的,惯例状况下栈维护增加的代码:

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

比较于左面的代码,这儿在函数的开始方位,这儿多分配了 0x10 的空间,用来做最后标红的三个指令:

  1. 把 pc 寄存器的页地址存入 x3。注意这儿用的是 ADRP,不是 LDR,所以实际上没有取出 pc 存的地址,而是只取出了页地址,这样能够确保这个值是相对不简单变化的。
  2. 这儿往 x4 写入了 x3 地址加上 #SSP,这样往 x4 里存入的便是一个实际的地址。
  3. 然后把 x4 存到 sp + 0x38 的方位,也便是之前多分配一部分的空间。

然后在函数回来之前:

  1. 把 x3 + #SSP 的地址写入 x1,然后把 sp + 0x38 的地址写入 x2,对比他们的值。
  2. 假如发生了栈溢出进犯,sp + 0x38 的方位会先被写入替换成别的值。
  3. 这样假如发现 x1 和 x2 不同,就能够阐明发生了栈溢出。

ARMv8.3 的优化

ARMv8.3 引进了一个指针验证机制(Pointer Authentication mechanism),用于维护要害方位的指针不被修正,防止经过修正回来方位的指针来控制程序的履行流程。比较于惯例的栈维护,这个机制把汇编代码缩减为两行,分别是函数开始时刺进的指令和函数结束时刺进的指令,简化了整个流程。

arm64e 根据的是 ARMv8.3,它和 arm64 最大的区别便是多了指针认证。

弥补:在 arm64 到 arm64e 后,多了这个 e 之后, stack chk failed 就没有意义了,相当于 CPU 内置支持 check stack failed. 比方现在arm64e体系(设备大于 iPhoneX 都是 arm64e)内的所有函数基本都默许敞开 PAC 校验。可是由于咱们 App,上传到 App Store 只支持 arm64 格式的,所以现在仍是在用 stack chk failed。

指针认证

指针认证背面的基本思想是,64位架构中实际的运用的地址小于64位。指针值中有未运用的位,咱们能够运用这些位来放置指针认证码(PAC)。咱们能够在将指针写入内存之前将PAC刺进到每个要维护的指针中,并在运用之前验证其完整性。进犯者想要修正受维护的指针,有必要找到/猜测正确的PAC才干控制程序流。

相关指令

关于指针认证,需求两个首要操作:核算并增加 PAC,以及验证 PAC 并康复指针值。这些分别由 PAC *和 AUT *一组指令处理。假如在 AUT 指令期间验证失利,则处理器将 PAC 替换为特定形式,使得指针值成为不合法地址。当引证无效指针时,经过不合法地址反常进行实际过错检测。这种规划将过错处理与指令别离,并消除了运用其他指令进行过错处理的需求。经过查看 AUT 指令宣布过错信号的形式,反常处理程序能够区别「不合法地址反常」和「身份验证失利」。

生成 PAC 运用的是 QARMA 算法,它需求两个参数:指针和上下文。输出的是一个裁剪过的段以便放到指针中,一般 PACGA 指令会生成 32 位的 PAC,可是要放到指针中时会被缩短到 21 位。

虚拟内存地址一般存放在 32 位和 52 位之间,假如敞开了 tagged address,PAC 就会放在 3-23 位之间,关闭时会放在 11-31 位之间。

指针认证下刺进的汇编

iOS 中的栈内存保护: ____stack_chk_fail 和 ARMv8.3 的指针验证机制

实际上是把函数进口的指令和回来前的指令分别优化成一条了。

以下解释来自 ChatGPT,经过了一些修正使得它更通顺。


PACIASPAUTIASP 是 ARMv8.3-A 架构中引进的两条指令,它们是指针认证机制的一部分。

PACIASP 代表 “运用密钥 A 和 SP 对指令地址生成 PAC”。此指令核算并刺进一个 PAC,运用存储在链接寄存器 lr(x30)中的指令地址作为输入指针,运用栈指针(SP)作为上下文,并运用密钥 A 进行认证。

AUTIASP 代表 “运用密钥 A 和 SP 对指令地址进行认证”。此指令对存储在链接寄存器(x30)中的指令地址进行验证,运用栈指针(SP)作为上下文,并运用密钥 A 进行认证。假如认证失利,该指令会将 PAC 替换为特定的值,使指针值成为不合法地址。

这些指令用于防止根据回来的编程(ROP)进犯,确保存储在仓库上的回来地址未被进犯者修正。在从函数回来之前,运用 AUTIASP 指令对存储在仓库上的回来地址进行认证。假如认证失利,则表明回来地址已被修正,程序能够采纳适当措施,例如终止进程或调用仓库查看失利处理程序。

  1. SUB sp, sp, #0x40:这条指令从栈指针(sp)中减去0x40(64)。这一般用于为局部变量分配空间。
  2. STP x29, x30, [sp,#0x30]:这条指令将寄存器 x29 和 x30 的值存储到栈指针(sp)加上偏移量 0x30 的方位。这一般用于保存函数调用前的帧指针和回来地址。
  3. ADD x29, sp, #0x30:这条指令将栈指针(sp)和偏移量 0x30 相加的成果存储到寄存器 x29 中。这一般用于更新帧指针。
  4. LDP x29,x30,[sp,#0x30]:这条指令从栈指针(sp)加上偏移量0x30的方位加载值到寄存器 x29 和 x30。这一般用于康复函数调用前的帧指针和回来地址。
  5. ADD sp,sp,#0x40:这条指令将栈指针(sp)加上 0x40(64)。这一般用于开释局部变量的空间。

(这儿 STP 指的是 store,也便是将寄存器的值存到栈上,LDP 指的是 load,相应的从栈上取出对应的值。SUB 和 ADD 分别指减和加,能够用于分配和开释空间,也能够履行常见的加减核算。)


假如在AUTIASP验证时发现PAC过错,就会把地址替换成特定的形式,导致整个地址成为不合法地址。

总结

无论是 ____stack_chk_fail 仍是 PAC,都是对现有内存问题的一种维护。比较于前者刺进 canary 的方案,PAC 显着会更节省履行时间,指令条数从 7 条缩短到 2 条。不过日常开发中咱们更常常遇到的仍是前者,知道了这个过错代表什么之后,就更简单查找问题了。

关于 c 相关内存问题,栈内存维护能做到的也比较有限,首要维护的是要害指针不被修正。关于其他类型的内存问题,比方 double free、use after free 等,仍是需求 ASan 来帮助定位。

参阅文档

  1. Pointer Authentication on ARMv8.3
  2. www.sans.org/blog/stack-…
  3. developer.arm.com/documentati…
  4. developer.arm.com/documentati…
  5. WWDC 2018 what’s new in llvm PDF devstreaming-cdn.apple.com/videos/wwdc…
  6. WWDC 2018 what’s new in llvm 视频 wwdctogether.com/wwdc2018/40…
  7. 微云对上文中 WWDC session 的解析 iweiyun.github.io/2018/10/15/…
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。