Android 的 FORTIFY
FORTIFY 是一个重要的安全功能,自 2012 年中以来在 Android 中可用。早在去年,从 GCC 迁移到 clang 作为默认的 C / C ++编译器,我们投入了大量的时间和精力,以确保巩固 FORTIFY 在 clang 上相媲美的品质。为了实现这一点,我们重新设计了一些关键的 FORTIFY 功能的工作原理,我们将在下面讨论。
在我们深入了解我们新的 FORTIFY 的一些细节之前,让我们简单介绍一下 FORTIFY 的作用及其使用方法。
什么是 FORTIFY?
FORTIFY 是 C 标准库的扩展,它试图捕获标准函数的错误使用,如 memset,sprintf,open 等。它有三个主要特点:
- 如果 FORTIFY 在编译时检测到对标准库函数的错误调用,则在修复错误之前,不会允许你的代码进行编译。
- 如果 FORTIFY 没有足够的信息,或者如果代码绝对是安全的,则 FORTIFY 将无法编译成任何内容。这意味着 FORTIFY 在无法找到错误的上下文中使用时,具有 0 个运行时开销。
- 其他方面,FORTIFY 添加检查以动态确定可疑代码是否有错误。如果它检测到错误,FORTIFY 将打印出一些调试信息并中止程序。
考虑以下示例,这是 FORTIFY 在真实世界代码中捕获的错误:
struct Foo {
int val;
struct Foo *next;
};
void initFoo(struct Foo *f) {
memset(&f, 0, sizeof(struct Foo));
}
FORTIFY 发现我们错误地将 f 作为 memset 的第一个参数,而不是 f。通常,这种错误可能很难跟踪:它表现为潜在地将 8 个额外的 0 个字节写入堆栈的随机部分,而不是对* f 做任何事情。因此,根据你的编译器优化设置,initFoo 的使用方式以及项目的测试标准,这可能会被忽略不了一段时间。使用 FORTIFY,你会收到一个编译时错误,如下所示:
/path/to/file.c: call to unavailable function 'memset': memset called with size bigger than buffer
memset(&f, 0, sizeof(struct Foo));
^~~~~~
有关运行时检查如何工作的示例,请考虑以下功能:
// 2147483648 == pow(2, 31). Use sizeof so we get the nul terminator,
// as well.
#define MAX_INT_STR_SIZE sizeof("2147483648")
struct IntAsStr {
char asStr[MAX_INT_STR_SIZE];
int num;
};
void initAsStr(struct IntAsStr *ias) {
sprintf(ias->asStr, "%d", ias->num);
}
该代码对所有正数都适用。但是,当你传入具有 num <= -1000000 的 IntAsStr 时,sprintf 将向 ias-> asStr 写入 MAX_INT_STR_SIZE + 1 个字节。没有 FORTIFY,这个逐个错误(最终清除 num 中的一个字节)可能会静默地忽略。使用它,程序打印出一个堆栈跟踪,一个内存映射,并将中止一个核心转储。
FORTIFY 还执行一些其他检查,例如确保 打开 的调用具有正确的参数,但它主要用于捕获与上述内存相关的错误。
但是,FORTIFY 无法捕获存在的每个与内存有关的错误。例如,考虑以下代码:
__attribute__((noinline)) // Tell the compiler to never inline this function.
inline void intToStr(int i, char *asStr) { sprintf(asStr, “%d”, num); }
char *intToDupedStr(int i) {
const int MAX_INT_STR_SIZE = sizeof(“2147483648”);
char buf[MAX_INT_STR_SIZE];
intToStr(i, buf);
return strdup(buf);
}
因为 FORTIFY 根据缓冲区的类型确定缓冲区的大小,如果可见,则它的分配站点不能捕获此错误。在这种情况下,FORTIFY 放弃,因为:
- 指针不是一个具有指针大小的类型,我们可以放心地确定,因为 char *可以指向一个可变的字节数
- FORTIFY 看不到指针分配的位置,因为 asStr 可以指向任何东西。
如果你想知道为什么我们有一个 noinline 属性,那是因为如果 intToStr 嵌入到 intToDupedStr 中,那么 FORTIFY 可能能够捕获这个错误。这是因为它会让编译器看到 asStr 指向与 buf 相同的内存,buf 是一个 sizeof(buf)字节的内存区域。
FORTIFY 工作原理
FORTIFY 通过在编译时拦截对标准库函数的所有直接调用,并将这些调用重定向到特殊的 FORTIFY 版本的库函数。每个库函数都由发出运行时诊断的部件组成,如果适用的话,则发出编译时诊断的部分。以下是 FORTIFY'ed memset 的运行时部分的简化示例(取自 string.h)。实际的 FORTIFY 实施可能包括一些额外的优化或检查。
_FORTIFY_FUNCTION
inline void *memset(void *dest, int ch, size_t count) {
size_t dest_size = __builtin_object_size(dest);
if (dest_size == (size_t)-1)
return __memset_real(dest, ch, count);
return __memset_chk(dest, ch, count, dest_size);
}
在这个例子中:
- _FORTIFY_FUNCTION 扩展到一些编译器特定的属性,以使所有直接调用 memset 调用这个特殊的包装器。
- __memset_real 用于绕过 FORTIFY 调用“常规”memset 函数。
- memset_chk 是特殊的 FORTIFY'ed memset。如果 count> dest_size, memset_chk 中止程序。否则,它只需调用__memset_real。
- __builtin_object_size 是魔术发生的地方:它和大小 sizeof 很相似,而不是告诉你一个类型的大小,它试图找出编译过程中给定指针有多少个字节。如果它失败,它回传(size_t)-1。
builtin_object_size 可能看起来很粗略。毕竟,编译器如何知道一个未知指针有多少个字节?嗯...不能。:) 这就是为什么_FORTIFY_FUNCTION 需要所有这些函数的内联:内联 memset 调用可能会使指针指向的分配(例如,局部变量,调用 malloc,...的结果)可见。如果是这样,我们可以经常为 builtin_object_size 确定准确的结果。
编译时诊断位也以__builtin_object_size 为中心。基本上,如果你的编译器有方法发出诊断,如果一个表达式可以证明是真的,那么你可以将它添加到包装器。这可以在 GCC 和具有编译器特定属性的 clang 上进行,因此添加诊断程序与对正确属性的处理一样简单。
为什么不净化?
如果你熟悉 C / C ++内存检查工具,你可能会想知道为什么在存在诸如 clang 的 AddressSanitizer 之类的时候,FORTIFY 很有用。净化器(sanitizers) 非常适合捕捉和跟踪与内存相关的错误,并且可以捕获到 FORTIFY 无法解决的许多问题,但是我们建议 FORTIFY 有两个原因:
- 除了在运行错误的情况下检查代码之外,FORTIFY 可能会发出明显不正确的代码的编译时错误,而当发生问题时,净化器只会中止你的程序。由于普遍接受的是尽可能早地捕捉问题,所以我们可以在编译时出现错误。
- FORTIFY 足够轻便,可以在生产中实现。在我们自己的代码部分使用它可以显示最大的 CPU 性能下降约 1.5%(平均为 0.1%),实际上没有内存开销,二进制大小的增加非常小。另一方面,净化器可以将代码减少 2 倍以上,经常会占用大量内存和存储空间。
因此,我们在 Android 的生产版本中启用 FORTIFY,以减轻一些错误可能造成的损害。特别是,FORTIFY 可以将潜在的远程代码执行错误转变为简单地中断应用程序的错误。再次,净化器能够检测到比 FORTIFY 更多的错误,所以我们绝对鼓励他们在开发/调试版本中使用。但是,运行给用户的二进制文件的运行成本只是太高,无法将其用于生产构建。
FORTIFY 重新设计
FORTIFY 的初始实施使用了世界 C89 的一些技巧,其中散布了几个 GCC 特定的属性和语言扩展。由于 Clang 不能模拟 GCC 如何完全支持原始的 FORTIFY 实现,因此我们重新设计了大部分它尽可能有效地在 clang 上运行。特别是,我们的 clang 风格的 FORTIFY 实现使用了 clang 特定的属性和语言扩展,以及一些函数重载(如果你使用其 overloadable 属性,clang 将乐意将 C ++重载规则应用于 C 函数)。
我们使用这种新的 FORTIFY 测试了数亿行代码,其中包括所有的 Android,所有的 Chrome 操作系统(需要自己重新实现 FORTIFY),我们的内部代码库和许多流行的开源项目。
这个测试揭露我们在不同的方式接近使已存在的代码崩溃。像:
template <typename OpenFunc>
bool writeOutputFile(OpenFunc &&openFile, const char *data, size_t len) {}
bool writeOutputFile(const char *data, int len) {
// Error: Can’t deduce type for the newly-overloaded `open` function.
return writeOutputFile(&::open, data, len);
}
和
struct Foo { void *(*fn)(void *, const void *, size_t); }
void runFoo(struct Foo f) {
// Error: Which overload of memcpy do we want to take the address of?
if (f.fn == memcpy) {
return;
}
// [snip]
}
还有一个开源项目,试图解析系统头像 stdio.h,以确定它有什么功能。添加 clang FORTIFY bits 极大地混淆了解析器,导致其构建失败。
尽管发生了巨大变化,但是我们看到的破损程度相当低。例如,编译 Chrome 操作系统时,我们的软件包中只有不到 2%看到编译时错误,这些都是几个文件中的微不足道的修复。虽然这可能是“够好”,但并不理想,所以我们改进了进一步减少不兼容性的方法。这些迭代中的一些甚至需要改变 clang 如何工作,但是 clang + LLVM 社区对我们提出的调整和添加是非常有帮助和接受的,例如:
- 添加 pass_object_size 到 clang ,
- 将 alloc_size 添加到 clang (以及其在 LLVM 中的对应项 ),以及
- 各种其他增强/调整,例如 在 C 的重载分辨率中允许不兼容的指针转换 。
我们最近推出了 AOSP,并从 Android O 开始,Android 平台将受到 clang FORTIFY 的保护。我们仍然对 NDK 进行一些整理,所以开发人员应该期望在不久的将来看到我们升级的 FORTIFY 实施。另外,正如我们上面提到的那样,Chrome 操作系统现在也有类似的 FORTIFY 实现,我们希望在未来几个月与开放源代码社区合作,以获得类似的实现*到 glibc ,即 GNU C 库。
*对于有兴趣的人来说,这与 Chrome 操作系统补丁的外观会有很大的不同。clang 最近获得了称为属性 diagnose_if ,这最终允许对多比我们原来的 glibc 的方法清洁强化执行,并产生比我们目前可以更漂亮的错误/警告。我们期望在更高版本的 Android 中有类似的诊断功能实现。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论