Android的FORTIFY

原标题:FORTIFY in Android
链接:https://android-developers.googleblog.com/2017/04/fortify-in-android.html
作者:George Burgess(软件工程师)
翻译:arjinmc

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社区对我们提出的调整和添加是非常有帮助和接受的,例如:

我们最近推出了AOSP,并从Android O开始,Android平台将受到clang FORTIFY的保护。我们仍然对NDK进行一些整理,所以开发人员应该期望在不久的将来看到我们升级的FORTIFY实施。另外,正如我们上面提到的那样,Chrome操作系统现在也有类似的FORTIFY实现,我们希望在未来几个月与开放源代码社区合作,以获得类似的实现*到glibc,即GNU C库。

*对于有兴趣的人来说,这与Chrome操作系统补丁的外观会有很大的不同。clang最近获得了称为属性diagnose_if,这最终允许对多比我们原来的glibc的方法清洁强化执行,并产生比我们目前可以更漂亮的错误/警告。我们期望在更高版本的Android中有类似的诊断功能实现。

results matching ""

    No results matching ""