libdft: Practical Dynamic Data Flow Tracking for Commodity Systems

1.概述
动态数据流跟踪(DFT)处理在程序执行期间传播时标记和跟踪感兴趣的数据。 DFT已经被多种工具反复使用,用于多种目的,包括防止0day和跨站点脚本攻击,检测和防止信息泄漏,以及分析合法和恶意软件。 我们提出了libdft,一个动态DFT框架,它不像以前的工作那样快速,可重用,并且可以与商品软件和硬件一起使用.libdft提供了一个API,用于构建支持DFT的工具,这些工具可以在未修改的二进制文件上运行,在通用操作系统和硬件上运行 ,从而促进研究和快速原型制作。

我们探索了实现指令级数据跟踪的低级方面的不同方法,引入了更高效且具有64位功能的影子内存,并识别(并避免)导致先前研究的过度性能开销的常见缺陷。 我们使用具有大型代码库(如Apache和MySQL服务器)和Firefox Web浏览器的实际应用程序来评估libdft。 我们还使用一系列基准测试和实用程序来比较libdft与类似系统。 我们的结果表明它至少与以前的解决方案一样快,甚至更快,并且据我们所知,我们是第一个在这样深度评估快速动态DFT实现的性能开销的人。 最后,libdft可以免费作为开源软件使用。

一般术语设计,性能,安全性

关键词数据流跟踪,动态二进制检测,污点分析,信息泄漏检测,漏洞利用预防

1.简介
动态数据流跟踪(DFT),也称为信息流跟踪,是一种众所周知的技术,它处理在程序执行期间传播的“有趣”数据的标记和跟踪。 DFT有许多用途,例如分析恶意软件行为,加强软件抵御0day攻击(例如,缓冲区溢出,格式化字符串),检测和防止信息泄漏,甚至调试软件错误配置。 从体系结构的角度来看,它已经集成到完整的系统仿真器和虚拟机监视器中,使用动态二进制检测器对未修改的二进制文件进行了改进,并使用源到源代码转换添加到源代码库中。 还提出了在硬件中实现它的建议,但它们对硬件供应商几乎没有吸引力。

以前的研究利用DFT来研究该技术在特定感兴趣领域的适用性,产生他们自己的基于软件的DFT的特定问题和临时实现,所有这些都受到以下一个或多个问题的影响:高开销,可重用性很低(即 ,它们是特定于问题的),并且适用性有限(即,它们不适用于现有的商品软件)。 例如,LIFT [22]和Minemu [2]使用DFT来检测安全攻击。虽然速度很快,但它们不支持多线程应用程序(首先是设计)。 LIFT仅适用于64位二进制文件,而Minemu仅适用于32位二进制文件,其设计需要进行大量修改才能支持64位体系结构。更重要的是,它们专注于单个问题域,无法轻易修改以便在其他域中使用。

更精确和可定制的细粒度DFT实现也未能为研究团体提供实用且可重用的DFT框架。 例如,Dytan专注于呈现支持数据和控制流依赖性的可配置DFT工具。 不幸的是,它的多功能性价格很高,即使在运行仅具有数据流依赖性的小程序时(控制流依赖性会进一步影响性能)。 例如,Dytan报告使用gzip压缩时减速30倍,而LIFT报告不到10倍。 尽管实验可能无法直接比较,但性能的显着差异表明Dytan的设计不适合低开销。

本文认为,实际的动态DFT实现需要解决上面列出的所有三个问题,因此它应该同时快速,可重用并适用于商品硬件和软件。我们介绍了libdft,这是一个共享库形式的元工具,它使用英特尔的Pin动态二进制检测框架实现动态DFT [16]。 libdft的性能与以前的工作相当或更好,导致命令行实用程序的速度在1.14x和6.03x之间,同时它还可以运行Apache和MySQL等大型服务器应用程序,开销在1.25x和4.83x之间。此外,它还具有通用性和可重复使用性,可提供广泛的API,可用于实现DFT驱动的工具。最后,它运行在商品系统上。我们当前的实现与Linux上的x86二进制文件一起使用,而我们计划将其扩展为在64位体系结构和Windows操作系统(OS)上运行。 libdft引入了一个高效的,具有64位功能的影子内存,它代表了早期工作中最严重的限制之一,因为扁平影子内存结构在64位系统上施加了无法管理的内存空间开销,并且动态管理结构引入了高性能损失。更重要的是,libdft支持多进程和多线程应用程序,通过折衷内存来确保对抗竞争条件(参见第7节),并且它不需要修改程序或底层操作系统。

本文的贡献可归纳如下:

  • 我们讨论了商用软件的快速且可重复使用的共享DFT库的设计和实现。具体来说,我们调查并确定导致先前DFT工具引起的性能下降的潜在原因,并提出最小化它的设计。我们从系统角度处理问题并尝试回答以下问题:这种DFT工具的性能界限是什么?从业者和系统实现者需要避免哪些做法?开销的来源是什么?我们还提出了一系列新颖的优化,以进一步提高DFT的性能。
  • 我们使用包括复杂和大型软件(如Apache和MySQL服务器以及Firefox Web浏览器)的实际应用程序来评估libdft的性能。 libdft实现了与以前的工作相似或更好的性能,同时适用于更广泛的软件集。此外,我们的广泛评估确立了关于DFT性能的一系列界限。据我们所知,我们是第一个对DFT框架进行如此广泛评估的人。
  • 我们展示了libdft驱动的工具(即libdft-DTA)的开发,以展示libdft的可重用性及其功能。 libdft-DTA执行动态污点分析(DTA)以检测与TaintCheck,Eudaemon和LIFT类似的0day攻击。我们展示了我们的多功能API可用于在大约450行C ++代码中开发一个复杂的工具。
  • 我们的实现可作为共享库免费提供,并可用于开发透明(即,无需对应用程序或底层操作系统进行任何更改)的工具来使用DFT服务。开发人员可以使用提供的API轻松定义感兴趣的数据,然后在任意点捕获它们的使用。通过这种方式,libdft通过允许潜在用户专注于解决特定问题(例如,检测信息泄漏或应用程序错误配置)而不是处理信息流跟踪框架的精细细节来促进研究和快速原型设计。

本文的其余部分组织如下:第2节介绍DFT并讨论动态和静态DFT方法之间的差异。我们在第3节介绍了libdft,并在第4节详细介绍了它的实现。第5节讨论了libdft在各种工具中的使用,并通过创建DTA工具展示了它的API,我们在第6节中与libdft一起评估。第7节检查当前实施的局限性以及未来的考虑因素。相关工作见第8节,结论见第9节。

2.数据流跟踪
DFT一直是一个受欢迎的研究主题,主要用于实施安全信息流和识别非法数据使用。 在过去的工作中,它经常被称为信息流跟踪(IFT)[24]或污点分析。 这项工作将DFT定义为:“在整个程序或系统的执行过程中准确跟踪所选数据流的过程”。 这个过程的特点是三个方面,我们将尝试借助图1中所示的代码片段来澄清。
数据源:数据源是程序或内存位置,其中感兴趣的数据通常在执行函数或系统调用之后进入系统。来自这些来源的数据被标记和跟踪。例如,如果我们将文件定义为源,则图1中的读取调用将导致标记数据并传递。
数据跟踪:在程序执行期间,标记数据在被程序指令复制和更改时被跟踪。考虑图1中的代码片段(a),其中数据已在第3行中标记。后面的while循环计算简单校验和(对数据中的所有字节进行异或)并将结果存储在csum中。在这种情况下,csum和数据之间存在数据流依赖性,因为前者直接依赖于后者。
另一方面,(b)中的授权间接受到phash值的影响,而phash的值又取决于pass。这通常被称为控制流依赖性,在这项工作中,我们不考虑隐含数据流的情况,这些情况与先前关于该主题的工作一致[18,24]。 Dytan为有条件地处理这种控制流依赖性做了规定,但得出的结论是,虽然它们在某些领域很有用,但它们经常会导致标记数据量的增加和不正确的数据依赖性[6]。正在进行的工作旨在解决这些问题[15]。
数据接收器:数据接收器也是程序或内存位置,可以检查是否存在标记数据,通常用于检查或强制执行数据流。例如,某些内存区域和函数参数可能不允许使用标记数据。再次考虑图1中的代码片段(a),其中第7行将csum写入文件。如果将文件定义为数据接收器,则使用write写csum可以触发用户定义的操作

动态与静态DFT:执行DFT需要额外的数据标记内存。此外,程序本身需要分别使用标签传播逻辑以及源和接收器处的数据标记和检查逻辑进行扩展。其附加代码通常称为检测代码,可以静态注入(例如,在源代码开发期间和编译/加载时),或动态地使用虚拟化或动态二进制检测(DBI)。静态系统通过使用修改的编译器[25]或源到源转换引擎[28]重新编译软件来应用DFT。相反,动态的二进制文件可以直接应用于未修改的二进制文件,包括商业现成的软件[22,27,31]。在这两种情况下,软件都需要进行广泛的检测,以便将数据与某种标签相关联,并注入逻辑,这些逻辑在源处断言标签,根据程序语义定义的数据依赖性传播它们,最后检查接收器是否存在标记数据动态解决方案虽然比静态解决方案慢得多,但具有立即且渐进地适用于已部署的软件的优点。

图2.在libdft下运行的二进制文件的过程映像。 突出显示的框描述了可与libdft一起使用的可能数据源和接收器。

3.设计
我们将libdft设计为与Pin DBI框架一起使用,以便于创建采用动态DFT的Pintools。简而言之,Pin由一个虚拟机(VM)库和一个注入器组成,该注入器将VM连接到已经运行的进程或启动自身的新进程。 Pintools是共享库,它使用Pin广泛的API来检查和修改指令级的二进制文件。 libdft也是一个库,Pintools可以在Pin上运行的二进制文件上透明地应用细粒度DFT。更重要的是,它提供了自己的API(见第5节),使工具作者可以通过指定数据轻松自定义libdft源和接收器,甚至修改标签传播策略。

当用户连接到已经运行的进程,或者使用支持libdft的Pintool启动新进程时,注入器首先注入Pin的运行时,然后将控制传递给工具。启用libdft的工具可以使用三种类型的位置作为数据源或接收器:程序指令,函数调用和系统调用。它可以通过安装在遇到某个指令时调用的回调,或者在进行某个函数或系统调用时“点击”这些位置。这些用户定义的回调通过标记或取消标记数据以及监视或强制执行数据流来驱动DFT过程。图2描绘了在支持libdft的Pintool下运行的进程的内存映像。突出显示的框标记工具作者可以安装回调的位置。例如,用户可以标记由读取系统调用返回的缓冲区的内容(如图1所示的示例),并检查间接调用指令的操作数是否被标记(例如,图2中的eax寄存器)。 .

3.1.数据标签
libdft在tagmap中存储数据标记,tagmap包含一个进程范围的数据结构(影子内存),用于保存存储在内存中的数据的标记,以及一个特定于线程的结构,用于保存驻留在CPU寄存器中的数据的标记。存储在tagmap中的标记格式主要由两个因素决定:(a)标记的粒度,以及(b)标记的大小。标记粒度原则上,我们可以将数据单元标记为小到一个比特,或者作为更大的连续内存块。前者使我们能够执行非常细粒度和精确的DFT,而使用更大的粒度意味着数据跟踪将更粗糙并且更容易出错。例如,对于页面级粒度,将单个字节(已标记)移动到未标记的位置将导致标记包含目标的整个页面,从而“污染”相邻数据。然而,选择非常精细的标记需要很高的成本,因为存储标签需要更多的存储空间(例如,使用位级标记,单个字节需要8个标记而32位寄存器需要32个标记)。更重要的是,标签传播逻辑变得复杂,因为数据依赖性也更复杂(例如,考虑添加两个仅标记其部分位的32位数字)。 libdft使用字节级标记粒度,因为在大多数体系结构中,字节是最小的可寻址内存块。我们的选择使我们能够为大多数实际目的提供足够细粒度的跟踪,我们相信它在可用性和性能之间取得了平衡[21]。
标签大小正交地标记粒度,较大的标签更通用,因为它们允许不同类型的数据被唯一地标记(例如,每个字节可以使用唯一的32位数字来标记)。不幸的是,较大的标签需要复杂的传播逻辑和更多的存储空libdft提供两种不同的标签大小:(a)用于将最多8个不同值或颜色与每个标记字节相关联的字节标记(每个位代表不同的标记类),以及(b)单位标记(即数据被标记)或不)。第一个允许更复杂的跟踪和分析工具,而第二个允许只需要二进制标记来保存内存的工具。

3.2标签传播
标签传播是使用Pin的API完成的,用于检测和分析目标过程。在Pin的术语中,检测指的是检查程序的二进制指令以确定应该在哪里插入分析例程的任务。例如,libdft检查(松散声明)移动或组合数据以确定数据依赖性的每个程序指令。另一方面,分析指的是在原始代码之前,之后或代替原始代码执行时改进的实际例程或代码。在我们的例子中,我们根据在检测过程中观察到的数据依赖性,注入实现标签传播逻辑的分析代码。
原始代码和libdft的分析例程由Pin的实时(JIT)编译器转换,用于生成实际运行的代码。这在第一次执行代码序列之前立即发生,并且结果被放置在代码高速缓存中(也在图2中描绘),以避免对于相同的代码序列重复该过程。我们注入的代码在应用程序指令之前执行,在寄存器之间以及寄存器和存储器之间复制时跟踪数据,从而实现细粒度DFT。 Pin's VMensures目标进程完全从代码缓存中运行,通过解释所有无法安全执行的指令(例如,间接分支)。此外,还应用了一系列优化,例如跟踪链接和寄存器重新分配,以提高性能[16]。

最后,libdft允许工具修改默认标记传播策略,方法是通过其API注册自己的检测回调,以获取感兴趣的指令。 这样,工具作者可以根据他们的需要定制数据标记,并在某些情况下取消标记传播或跟踪其他未处理的指令。

3.3快速动态DFT的挑战
为了降低libdft的开销,我们仔细研究了DBI框架(如Pin)如何识别应该避免的开发实践。 Pin的开销主要取决于注入的分析代码的大小,但由于代码本身的结构,它通常可能高于预期。 具体来说,底层架构提供的寄存器将用于执行应用程序代码以及实现DFT逻辑的代码,从而迫使DBI框架溢出寄存器(即,将其内容保存到内存中并稍后恢复它们), 每当分析例程需要利用已经分配的寄存器时。因此,代码越复杂,寄存器就越多。

此外,由于某些副作用,必须避免使用某些类型的指令。例如,在处理周期方面将EFLAGS寄存器溢出到x86架构中是昂贵的,并且由专用指令(PUSHF,PUSHFD)执行。因此,应该谨慎地包括修改该寄存器的分析代码中的指令。更重要的是,必须完全避免测试和分支操作,因为它们会导致非内联代码。特别是,每当DFT代码中包含分支指令时,Pin的JIT引擎将发出对相应分析例程的函数调用,而不是内联代码的代码以及应用程序的指令。对任何动态DFT工具的实施施加这样的限制是一项挑战。我们的实现与Pin一起考虑了这些问题,以实现良好的性能。
libdft的设计为满足第1节中列出的所有三个属性的框架提供了基础。通过考虑上面讨论的限制,我们实现了低开销。此外,libdft的广泛API使其可重用,因为它使用户能够自定义它以用于各种领域,例如安全性,隐私,程序分析和调试。最后,通过使用成熟的,而不是实验性和功能有限的DBI平台来满足最后一个属性,以便为各种流行系统(例如,x86和x86-64 Linux和Windows操作系统)提供实现DFT的设备)。

图3. libdft的体系结构。 I / O接口和跟踪器的阴影组件说明了实现DFT逻
辑的仪器和分析代码,而tagmap上的x标记区域表示标记的字节。

 4.实施
我们使用Pin 2.9实现了libdft,目前可用于Linux和Windows操作系统。 我们的原型适用于在Linux上运行在32位x86 CPU上的未经修改的多进程和多线程应用程序,但可以通过适度的努力在x86-64体系结构和Windows操作系统上进行扩展(我们将在第7节讨论未来的端口)。 libdft的主要组件如图3所示。

4.1 Tagmap
tagmap的实现在libdft的整体性能中起着至关重要的作用,因为注入的DFT逻辑始终在数据标签上运行。
4.1.1注册标签
我们在vcpu结构中存储x86架构的所有8个通用寄存器(GPR)的标签,这是标签映射的一部分(参见图3)。请注意,我们仅标记和跟踪应用程序可直接使用的寄存器(如GPR)。指令指针(EIP),EFLAGS和段寄存器等寄存器不会被标记或跟踪。回想一下,根据我们对DFT的定义,我们只跟踪直接数据流依赖性,因此忽略EFLAGS是安全的。然而,基于EFLAGS的内容有条件地执行的指令被适当地处理(例如,CMOVcc,SETcc)。此外,为简单起见,当前忽略浮点寄存器(FPU)以及SSE寄存器(XMM,MMX)。为了在将来支持这些寄存器,我们只需要放大vcpu。
tagmap包含多个vcpu结构,每个执行线程一个。具体来说,libdft捕获应用程序的线程创建和线程终止事件,并动态管理vcpu结构的数量。我们使用其指定给每个线程的虚拟ID(即,从零开始的增量值)为每个线程找到适当的结构。对于位大小的标签,我们使用一个字节来保存每个32位GPR所需的四个1位标签,因此每个线程的vcpu空间开销为8个字节。类似地,在字节大小的标签的情况下,每个32位GPR需要4个字节,因此vcpu的空间开销变为每个线程32个字节。
4.1.2记忆标签
位大小的标记当libdft配置为使用位大小的标记时,它将内存标记存储在一个固定大小的平面结构中(参见图3中的mem位图),该结构为进程可寻址内存的每个字节保存一位。 x86系统中虚拟地址空间的总大小为4GB(232),但是OS为自己(即内核)保留了该空间的一部分。为内核保留的空间量取决于操作系统,Linux通常采用3G / 1G内存分割,为进程留下3GB的地址空间。在这种情况下,我们需要为tagmap连续保留384MB。
地址vaddr的内存标签可以通过以下方式获得:tval = mem_bitmap [vaddr >> 3]&(MASK <<(vaddr&0x7))。具体来说,我们使用vaddr的29个最高有效位(MSB)作为mem位图中的字节索引,用于选择包含vaddr标记的字节。然后,我们将vaddr的3个最低有效位视为先前获取的字节中的位偏移,并通过将MASK设置为0x1,我们获得单个字节的标记位。类似地,如果MASK是0x3或0xF,我们分别获得一个字或双字的标记位。 mem位图施加的地址空间开销为12.5%。使用固定大小的结构而不是动态管理的结构(例如,类似页面的表格)允许我们避免管理和访问它所涉及的惩罚。请注意,虽然在32位系统上,tagmap的大小是合理的,但在64位体系结构中,平面位图并不实用。例如,在x86-64中,平面位图需要32TB。

字节大小的标签当libdft使用字节大小的标签时,它会将它们存储在动态分配的标签映射段中(参见图3中的tseg)。每次应用程序通过执行图像加载(例如,加载动态共享对象时)隐式地获取新的内存块,或者通过调用mmap,mremap,brk和shmat等系统调用显式获取内存时,libdft会拦截事件并分配一个大小相等的连续内存区域。例如,如果应用程序使用mmap请求1MB的匿名映射,则libdft将使用1MB的tagmap段“遮蔽”分配的区域,以存储mmap-ed内存的字节标记。更重要的是,还共享对应于共享内存块的标记映射段。因此,在libdft下运行的两个进程可以有效地共享影子内存。据我们所知,我们是第一个实现这种标签共享方案的人。

我们通过proc伪文件系统(/ proc / <pid> / maps)获取有关在加载时或者libdft附加到应用程序之前映射的内存区域的信息。 这样我们就可以获取堆栈的位置和其他内核映射的内存对象,例如vDSO和vsyscall页面,并相应地分配相应的tagmap段。 为了处理堆栈的隐式扩展,libdft预先分配一个tagmap段来覆盖堆栈,好像它扩展到它的最大值,这可以通过getrlimit(RLIMIT STACK)获得。 但是,线程堆栈不需要相同,因为它们是使用mmap显式分配的。

发布了43 篇原创文章 · 获赞 23 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zhang14916/article/details/89765462