365bet如何设置中文,使用GPU创建高性能的模糊器

通过云计算对嵌入式软件进行模糊处理时,我们可以使用GPU来使性能/价格提高10倍吗?根据我们以前的工作经验,我们认为答案是肯定的!
模糊测试是一种软件测试技术,通过将大量随机输入暴露给程序,该技术可能导致意外的软件行为。这是一项重要的行业标准技术,通常用于消除安全漏洞。但是,模糊测试并不总是那么费时,而且在模糊嵌入式软件时还必须面对各种其他挑战。
我们知道仪器技术通常用于模糊测试,并且设备必须具有高吞吐量计算功能,但是这些方面缺少嵌入式平台。这是因为当无法访问源代码时,将需要通过仿真器(速度非常慢)或许多物理设备来进行这些平台的实际模糊测试。这些要求通常是不切实际的。
尽管大多数模糊测试方法都使用传统的CPU架构或仿真器,但我们还是决定使用其他商用硬件专门解决此问题,此处使用的硬件是GPU。最近,随着机器学习的飞速发展,不仅GPU的价格变得人们可以负担得起,而且所有主要的云提供商都可以随时拥有大量GPU,从而提供计算能力。众所周知,GPU可以很好地并行执行任务,并且模糊化很容易并行化。
在本文中,我将为读者详细介绍基于GPU的大规模并行模糊器的设计和实现方法,到目前为止,我们已经实现了一个执行引擎,该引擎的性能(执行时间/秒/美元)是libFuzzer的五倍。更重要的是,我们仍有大量的优化空间。
使用GPU进行模糊处理
模糊测试的目的是为程序生成“意外”输入,从而导致不良行为(例如崩溃或内存错误)。当前,最常用的模糊器是面向覆盖的,他的工作重点是寻找可以改善代码覆盖率的输入(例如执行以前未执行的功能),以调查可能导致程序崩溃的极端情况。
为了实现这一目标,Fuzzer将为目标程序提供大量的随机输入。因此,此任务易于并行化,因为每个输入都可以独立于其他输入执行。
目前,GPU的价格相当便宜,Google Cloud上预防性TeslaT4的价格仅为每小时$ 0.11。另外,GPU确实擅长于并行执行大量工作:TeslaT4可以在并行运行2560个线程的同时在40,000个线程之间进行oneContext切换,并且如前所述,模糊测试是非常合适的并行处理问题。理论上,使用数千个线程,我们可以同时测试数千个不同的输入。
为什么以前没有人这样做?
简而言之,GPU上的运行代码和CPU上的运行代码在几个关键方面有很大的不同。
首先,GPU无法直接执行x86 / aarch64 /体系结构的指令,因为GPU具有自己的指令集。我们的目标是在没有源代码的情况下测试嵌入式软件:由于我们只有二进制代码,因此很难生成和执行GPU汇编代码。
其次,GPU没有操作系统。传统的并行化模糊器必须启动多个进程,并要求可以分别处理不同的输入而不会干扰其他进程,因此,如果任何输入导致某个进程崩溃,则其他进程一定不会受到影响,但是由于GPU缺少进程和地址空间隔离会导致整个内存模糊器崩溃,因此我们需要找到一种方法来隔离需要模糊处理的程序并行实例。
如果没有操作系统,它将无法响应系统调用,也就是说,程序将无法执行任何操作,例如打开文件和使用网络,因此我们需要模拟系统调用或传递最后,GPU内存管理也是一个非常棘手的问题。由于GPU的内存层次结构非常复杂,因此存在许多不同的内存类型,并且每种内存类型具有不同的可用性和性能,并且GPU的性能高度依赖于内存访问模式,例如,在访问和访问内存时,这种性能影响至关重要。此外,GPU没有大量的内存,这使得难以正确管理内存布局和访问模式拥有.16 GB的设备内存听起来可能令人印象深刻,但是当在40,000个执行线程中分配时,每个线程仅具有微小的419 KB空间。
我们可以构建GPU模糊器吗?我们可以!虽然创建有效的GPU模糊测试器有很多障碍,但没有障碍是无法克服的。
通过翻译二进制文件执行代码
首先,让我们看看arch64体系结构二进制文件是否可以在GPU上运行。
如前所述,我们要在GPU上测试嵌入式二进制代码(如ARMv7,arch64等)。由于NVIDIA GPU使用另一种称为PTX(ParallelThreadeXecution,RTX)的指令集体系结构,因此我们可以使用我们想困惑的二进制文件,不要直接这样做。解决此问题的常用方法是模拟嵌入式CPU。但是,开发用于GPU的CPU仿真器不仅昂贵,而且性能也很差。另一个选择是将二进制代码转换为PTX代码,以便无需仿真处理即可直接在GPU上执行。
我们开发了一种名为Remill的二进制翻译工具,可以实现上述目标。Remill可以将二进制代码转换为LLVMIR(中间表示代码),然后将其重定向并编译为LLVM项目支持的任何体系结构碰巧的是LLVM以PTX代码的形式支持LLVMIR的输出,非常适合我们的目的。
假设我们有一个简单的示例函数,该函数非常简单:将w19设置为0,加5,然后返回结果。
主要:
movw19,#0 ???? //将数字0存储在寄存器w19中
addw19,w19,#5 // Add5
movw0,w19 ???? //返回结果
退回
我们可以将这些指令的字节传递给Remill,并让Remill生成适当的LLVMIR来模拟在ARM处理器上运行的原始程序:
//简洁起见:)
defineso_local%struct.Memory * @ _ Z5sliceP6MemorymPm(%struct.Memory * readnonereturned%0,i64%1,i64 * nocapture%2)local_unnamed_addr#0{
%4 = addi64%1.1
storei64%4,i64 *%2,align8,!tbaa!2
ret%struct.Memory *%0
然后,通过一些调整,我们可以使LLVM将上面的LLVMIR编译为PTX汇编代码:
ld.param.u64?%rd1,[sub_0_param_0];
ld.param.u64?%rd2,[sub_0_param_1];
mov.u64 ????%rd4.5;
st.u64 ????[%rd1 + 848],%rd4;
add.s64 ????%rd5,%rd2,12;
st.u64 ????[%rd1 + 1056],%rd5;
最后,我们可以将此PTX加载到GPU中并像访问源代码一样运行它:
如前所述,由于GPU没有操作系统来实现进程之间的隔离,因此我们必须隔离地址空间本身,以便要模糊的程序的多个实例可以访问同一组内存地址,而不会互相分配干扰。同时,我们还需要检测目标程序中的内存安全错误。
实际上,Remill通过调用函数read_memory和write_memory替换了原始程序中的所有内存访问。通过这些功能,我们可以实现软件内存管理单元,以补偿操作系统缺少的内存管理功能并相应地处理内存访问。
例如,考虑以下函数,该函数采用一个指针并增加它指向的整数:
添加一个:
ldrw8,[x0]?? //加载指针指向的值
addw8,w8,#1 //增加值
strw8,[x0]?? //写回新值
退回
Remill将上述汇编代码转换为IR代码,其中包含对read_memory函数的调用,添加指令和对write_memory函数的调用,如下所示:
define%struct.Memory*@slice(%struct.Memory *,i64%X8,i64 * nocapture%X0_output)local_unnamed_addr#2{
%2 = tailcalli32 @__ remill_read_memory_32(%struct.Memory *%0,i64undef)#3
%3 = addi32%2.1
%4 = tailcall%struct.Memory * @ __ remill_write_memory_32(%struct.Memory *%0,i64undef,i32%3)#3%5 = tailcall%struct.Memory * @ __ remill_function_return(%struct.State * nonnullundef,i6416,%struct.Memory *%4)#2,!Noalias!0
ret%struct.Memory *%5
使用__remill_read_memory_32和__remill_write_memory_32函数,我们可以为每个线程提供自己的虚拟地址空间。此外,我们还可以在非法访问使整个模糊器崩溃之前检查并拦截内存访问期间的错误情况。
但是请记住,当40,000个线程共享16GB的设备内存时,此空间并不多。为了节省空间,我们可以在MMU中使用写时复制策略对线程进行多线程共享,直到其中一个线程对内存执行写操作为止。此时,存储空间的内容已被复制;实际上,以这种方式存储存储是一种非常有效的策略。
初期表现
现在,我们找到了一种可能的方法:首先将二进制程序转换为LLVM,将其转换为PTX,将其混合为MMU,然后在数千个GPU线程上并行运行。
但是,我们的目标是构建性能提高10倍的模糊器(此处的比较对象是在CPU上运行的模糊器),能否实现这一目标?的确,评估模糊器的性能是一件非常困难的事情,研究人员发表了许多文章以有效地比较它们。此外,由于我们仍然缺少重要的模糊器组件(例如B),我们的模糊器过于粗糙且难以正确评估。生成程序新输入的变量。但是,如果仅测量执行器的性能,则可以检查模糊器执行目标程序中输入的速度(执行时间/秒)。通过标准化计算机硬件的成本(请注意,GPU通常比带有其他模糊器的CPU贵),我们可以比较执行时间/秒/美元。
我们应该在基准测试中困惑哪些代码?实际上,由于以下原因,libpcap的BPF数据包筛选器代码似乎是一个不错的选择:
·它实现了一个复杂的状态机,人类难以证明其合理性,使其成为一个很好的测试对象。
·BPF组件过去曾暴露于安全漏洞中,因此它们可用作我们的模糊测试的现实目标。
·没有系统调用(我们的微型模糊器尚不支持系统调用)。
接下来,我们编写一个测试应用程序,以从模糊器获取数据包,并在其上运行复杂的BPF过滤器程序:
dsthost1.2.3.4ortcporudporiporip6orarporrarp
Oratalkoraarpordecnetorisoorstporipx
这个测试程序没有做很多事情,但是它使用复杂的逻辑并且需要许多内存访问操作。
要评估我们的模糊器,我们可以将它与libFuzzer进行比较。LibFuzzer是一种快速且广泛使用的模糊器。当然,这不是一个很公平的比较。一方面,libFuzzer有一个更为宽松的问题:它可以使用测试程序的源代码进行模糊测试,但是我们的模糊器必须转换和处理针对不同体系结构编译的二进制代码进行安全性处理时,源代码通常不可用调查。另一方面,libFuzzer可以通过突变生成新的输入,而我们尚未做到。尽管此比较并不完美,但可以用来估计数量级。
本文使用GoogleComputeEngine 8核N1实例(在撰写本文时,非抢占式实例为每小时0.379998美元)和TeslaT4GPU(截至撰写本文时),每小时为0.35美元。
不幸的是,与libFuzzer相比,我们的fuzzer不能很好地工作。LibFuzzer的性能达到520万次/秒/美元,而我们的Fuzzer仅达到361,000次/秒/美元。
让我们看看是否还有改进的余地…
交叉储存
在开始性能调整之前,我们应该分析模糊器以更好地了解其性能。实际上,Nvidia的NsightCompute Profiler可用于解释硬件使用和性能瓶颈。
通过分析数据,我们可以看到GPU仅消耗3%的处理能力。在大多数情况下,GPU计算机硬件处于不活动状态,这意味着什么也不做,通常是因为内存延迟过高:GPU一直在等待内存完成读写操作,但在我们这种情况下,它并没有这样做因为我们的模糊器需要访问大量内存,所以根据性能分析数据,您会发现GPU仅使用了可用内存带宽的45%。相反,对我们来说,原因是内存访问的效率太低:每次内存访问都花费很长时间,并且不能为计算提供足够的数据。
要解决此问题,我们需要更好地了解GPU执行模型。
GPU线程每组32个称为扭曲的线程。warp中的所有线程在并行多处理器中一起运行,并在锁步模式下运行,这意味着它们同时执行相同的指令。
当线程读取或写入内存时,以128字节内存块为单位执行内存访问。如果warp中有32个线程尝试读取位于相同128字节块中的内存,则硬件仅需要从请求内存总线传输一个内存块(这称为内存事务),但是如果线程从中读取内存内容在不同的内存块中,硬件可能需要执行32个独立的内存事务,并且这些事务通常被序列化,从而导致我们的性能分析结果中所述的行为。也就是说,计算机硬件几乎总是处于非活动状态,因为它必须等待如此多的内存事务完成。内存带宽的使用似乎还不错,因为可以读取很多128字节的内存块,但是实际上每个内存块仅使用4或8字节的内容,因此很多Bis被广泛使用。浪费了。当前我们正在为每个线程分配单独的内存,因此,当一个线程访问内存时,它很少访问与另一个线程相同的128字节内存块。我们可以通过为一个warp(32个线程)分配一个内存块并在此warp中分配Nest线程的内存来改变这种情况。这样,当线程需要从内存访问值时,它们的值是连续的,GPU可以通过内存事务完成这些内存读取。
尝试之后,我们发现性能提高了一个数量级!显然,在编写GPU时,请注意内存访问模式非常重要。
减少数据传输和内核启动的次数
如果再次运行事件探查器,您可能会发现我们的计算机使用率已得到显着提高(从3%提高到33%),但距离充分利用还有很长的路要走。我们可以做得更好吗?
让我们继续检查内存使用模式并查看所使用的内存类型,NVidiaGPU在不同的物理位置提供了多种存储。最容易使用的类型称为“统一内存”。这意味着我们可以在不同的物理位置。我们之所以使用此方法,是因为我们不必过多地考虑字节的物理位置。但是,如果对字节的正确管理不当,则会由于物理位置之间的数据传输效率低而造成性能瓶颈。
由于我们当前的内存延迟仍然很高,因此让我们仔细看看这些传输是否存在任何问题。
我们简单的模糊器可以“逐轮”工作:如果GPU可以执行40,000个线程,我们将40,000个输入传递给GPU,并且每个线程在下一轮开始之前会模糊这些输入。测试。在这两个回合之间,我们设置了使用的内存(例如,覆盖率跟踪数据结构和模糊程序使用的内存)。但是,这会导致每轮之间GPU和CPU之间的大量数据传输,因为内存会重置为CPU,然后重置为GPU。发生这些传输时,GPU不会执行任何操作。当GPU等待CPU启动下一个操作周期时,会产生额外的延迟,我们可以通过一次启动GPU代码来避免CPU和GPU之间的同步来改善这种情况。由于不必将大量数据存储在统一内存中,因此我们可以为GPU分配全局内存以进行保存。然后,当我们需要发送有关模糊测试进度的信息时(例如,导致崩溃的输入是什么))执行异步机制,此数据将以线程方式发送到CPU,当它模糊测试一个输入时,重置内存并继续测试下一个输入,而无需传输数据或等待CPU。
这几乎使速度提高了另一个数量级!现在,我们的Fuzzer比libFuzzer快5倍。
从这个角度来看,这种方法仍然很有前途-尽管我们的模糊器仍然缺少变异机器,并且无法处理系统调用,但与libFuzzer的性能比较表明,GPU用于模糊处理,某些应用程序可能非常有用。
基于GPU的模糊测试的下一步是什么?
尽管我们已经很接近这个模糊测试项目的性能目标,但是这个项目还有很长的路要走,但是由于硬件利用率仍然很低,因此我们还有很多优化的余地。
此外,我们需要建立对处理系统调用的支持,这在对I / O密集型应用程序进行模糊处理时可能会对性能产生重大影响,但是要使这种模糊处理程序实用,我们还需要创建一个变异引擎,尽管这个问题很多比创建执行引擎更容易理解。
但是,我们很高兴在开发初期看到如此有希望的结果。我们期待在模糊嵌入式二进制文件方面提高一个数量级。
最后,我还要感谢ArtemDinaburg对该系统的第一个设计,以及他对实施该项目的热情指导。我还要感谢Peter Goodman的设计反馈和调试建议,谢谢您。
参考和来源:https://blog.trailofbits.com/2020/10/22/lets-build-a-high-performance-fuzzer-with-gpus/