对于所有类型环境中的开发人员来说,安全性正成为一个越来越重要的主题,即便过去一直认为安全性不成问题的嵌入式系统也是如此。本文将介绍几种类型的编码漏洞,指出漏洞是什么、如何降低代码被攻击的风险、如何更好地找出代码中的此类缺陷。
注入攻击
通过将信息注入正在运行的流程,攻击者可以危害进程的运行状态,以反射到开发人员无法保护的某种最终目标。例如,攻击者可能会通过堆栈溢出(stack corruption)将代码注入进程,从而执行攻击者选定的代码。此外,攻击者也可能尝试将数据注入数据库,供将来使用;或将未受保护的字符串注入数据库查询,获取比开发人员更多的信息。无论出于怎样的目的,注入总是一件坏事,总是需要谨慎对待的。
最恶劣的注入攻击形式也许是代码注入——将新代码置入正在运行的进程的内存空间,随后指示正在运行的进程执行这些代码。此类攻击如果成功,则几乎可以进行任何操作,因为正在运行的进程完全被劫持,可执行攻击者希望执行的任何代码。
此类攻击最著名的示例之一就是 Windows 动画光标攻击,这正是本文要讨论的模式。攻击者利用一个简单的 Web 页面将形式不当的动画光标文件下载到查看者的 PC 中,导致浏览器调用此动画光标,动画光标调用时可能发生任意代码的注入。实际上,这是一个完美的攻击载体:因为它不要求对被攻击机器的任何实际访问、最终用户根本意识不到任何可能发生的麻烦;此外,如果攻击效果的恶意也是适度的,则对最终用户的外部影响几乎是零。
考虑示例 1(a),当然,这改写自 Windows 攻击,它构成了此类攻击载体的基础。这里的开发人员对于传入流的可靠性做出了基本的假设。信任流和并相信一切都没问题。使用基于堆栈的将被非串形化(deserialized)的类型调用函数,未知数据流和代码注入肯定会在某个时间点出现。
(a) void LoadTypeFromStream(unsigned char* stream, SOMETYPE* typtr) { int len; // Get the size of our type's serialized form memcpy(&len, stream, sizeof(int)); // De-serialize the type memcpy(typtr, stream + sizeof(int), len); } (b) void foo(unsigned char* stream) { SOMETYPE ty; LoadTypeFromStream(stream, &ty); } (c) void LoadTypeFromStream (unsigned char* stream, SOMETYPE* typtr) { int len; // Get the size of our type's serialized form memcpy(&len, stream, sizeof(int)); // GUARD if( len < 0 || len > sizeof(SOMETYPE) ) throw TaintedDataException(); // De-serialize the type memcpy(typtr, stream + sizeof(int), len); } |
示例1 注入攻击。
这是怎样发生的?假设您调用示例 1(b)中的函数。我们就得到了一个易于利用的攻击载体。这里的问题在于 SOMETYPE 在编译时的大小是固定的。假设此类型在内存中使用 128 个字节表示。再假设您构建传入流时,使前 4 个字节(要非串形化的内容的长度)的读数为 256.现在,您没有检查正在处理的内容的有效性,而是将 256 个字节复制到了仅为 128 个字节的保留堆栈空间内。
考虑到发布模式堆栈的典型布局,您显然遇到了麻烦。查看堆栈,了解原因所在。每个被调用的函数都会将其本地数据布设到堆栈的一个帧内,通常是通过在输入时从堆栈指针减去本地数据的已知大小(加上处理调用链本身所需的任何管理数据)实现的。编译器发出的理想函数 prolog(伪代码)如下所示:
.foo sub sp, 128 ; sizeof SOMETYPE |
push sp ; push the SOMETYPE local variable push ap ; push the stream pointer (comes from 1st argument) call LoadTypeFromStream ret |
在调用 foo() 时,调用方将流地址以及返回地址(作为使用调用指令或平台上可用的同等部分的隐式效果)压入堆栈,使堆栈内容中有 128 个字节是为我们的类型保留的,且紧邻返回给 foo() 调用方的返回地址,参见图 1.
现在,LoadTypeFromStream 执行,并将 256 个字节写入所提供的地址,也就是在我们调用函数之前堆栈指针(SP)的值。这会覆盖应该使用的 128 个字节(本例中位于地址 0x1000 处),加上随后的 128 个字节,包括传入的参数指针、返回地址以及堆栈中随后 128 个字节内存储的其他任何信息。
那么攻击者怎样利用这样的漏洞呢?并不简单,需要经过反复的试错。实际上,攻击者要安排攻击,使覆盖的返回地址将控制权移交给攻击者,而非预期调用方函数。因而,攻击者需要准确了解要利用哪些数据结构,这样的数据结构在要攻击的任意版本的操作系统或应用程序上有多大、周边有哪些内容(以便正确设定伪造的返回地址)、如何有意义地插入足够的信息以使返回地址和其他效果能够实现某种恶意操作。
这一切做起来并不简单,但多种多样的攻击表明,总是有人有太多的空闲时间。
应如何防范此类攻击?这是一次攻击还是多重攻击?所写入的代码是否真的像这里所显示的这样笨拙?现代编译器是否会对堆栈帧布局做一些特殊处理,以避免此类问题?
总而言之,模糊处理就等于没有防御。我们都认识到,程序员将攻击预想得越简单,攻击出现的可能性就越高。然而,即便是复杂的代码,若未进行合理防御,也迟早会受到攻击。这种利用被污染的数据流和非常基本的缓冲溢出漏洞的攻击,多年以来这一直是热门的研究课题,但每年仍然会出现大量此类攻击。
防范此类攻击的效果甚微,因为攻击形式复杂——注意您的数据假设。只要在示例1(a)中添加一行简单的代码,就会使其更加安全,参见示例1(c)。显然,随着流交互变得更加复杂,保护的要求也随之复杂化,但基本上说代码注入是编码中“不可饶恕”的过失,因为防范它的方法是那样普及和简单。