内容简介:到目前为止,我们已经讨论了针对使用单个结构化输入的API的模糊测试方法。不过,有些API可能与前面介绍的API差距甚大。例如,有些API不会直接使用数据,相反,它们是由许多函数组成的,并且仅在API处于特定状态时,这些函数才起作用。这种有状态API对于网络软件来说是非常常见的。对于这种API来说,使用protobuf进行模糊测试也很有效。这时,我们只需定义一个描述API调用序列(或跟踪)的protobuf消息,并实现一个函数来执行跟踪。虽然gRPC的API Fuzzer并没有使用libFuzzer的自定义m
针对状态API的模糊测试
到目前为止,我们已经讨论了针对使用单个结构化输入的API的模糊测试方法。不过,有些API可能与前面介绍的API差距甚大。例如,有些API不会直接使用数据,相反,它们是由许多函数组成的,并且仅在API处于特定状态时,这些函数才起作用。这种有状态API对于网络软件来说是非常常见的。对于这种API来说,使用protobuf进行模糊测试也很有效。这时,我们只需定义一个描述API调用序列(或跟踪)的protobuf消息,并实现一个函数来执行跟踪。
示例:gRPC API Fuzzer
虽然gRPC的API Fuzzer并没有使用libFuzzer的自定义mutator或protobuf,但是,它仍然是针对有状态API的模糊测试的一个很好的简单示例。在这里,Fuzzer会使用字节数组,每个单独的字节都被解释为针对特定API函数的单个调用(在某些情况下,后面的字节用作参数)。
switch (grpc_fuzzer_get_next_byte(&inp)) { default: // terminate on bad bytes case 0: { grpc_event ev = grpc_completion_queue_next(... case 1: { g_now = gpr_time_add(...
这个模糊测试目标适用于突变型的模糊测试引擎,并产生了80多份安全漏洞报告,其中一些是用libFuzzer发现的,另一些是用AFL发现的。
然而,这种方法有一个缺点,即fuzzer生成的输入对于模糊测试目标本身之外的代码来说是没有任何意义的,并且,这些输入会因目标代码的微小变动而失效。同时,这些输入也不是人类可读的,使得对由此发现的安全漏洞的分析更加复杂。
示例:Envoy头部到Fuzzer的映射
实际上,有一个Envoy模糊测试目标采用了一种不同的方法来对有状态API进行测试:它使用定制的protobuf消息类型对动作序列进行编码,并实现了针对这些类型的播放器。
DEFINE_PROTO_FUZZER(const test::common::http::HeaderMapImplFuzzTestCase& input) { ... for (int i = 0; i < input.actions().size(); ++i) { ... const auto& action = input.actions(i); ... switch (action.action_selector_case()) { ...
这个模糊测试目标已经至少发现了一个security regression:漏洞,修复补丁。该漏洞的Reproducer输入是一个带有消息文本的可读文件。
与前面将测试动作编码为字节序列的方法相比,使用protos对有状态API进行模糊测试时,不仅速度要慢一些,而且,测试过程也更复杂一些。不过,这种方法也有其优点:这种方法更加灵活,同时,也更加易于维护,因为protobuf类型比自定义字节编码更容易理解,也更易于扩展。
示例:Chrome IPC Fuzzer
由于Chrome含有许多有状态的API,因此,在代码审查期间,人工审查这些API的话,难度非常大,这使得针对这些API的模糊测试技术,很容易找到有价值的漏洞。在这方面,一个很好的例子就是AppCache子系统。这是一种针对HTTP的缓存机制,旨在使某些应用程序可脱机使用。
在Chrome中,该机制是在沙盒渲染器进程和特权浏览器进程之间的接口中实现的,它们运行于unsandboxed状态。下面是一个可用于渲染器进程的API:
// AppCache messages sent from the child process to the browser. interface AppCacheBackend { RegisterHost(int32 host_id); UnregisterHost(int32 host_id); SetSpawningHostId(int32 host_id, int32 spawning_host_id); SelectCache(int32 host_id, url.mojom.Url document_url, int64 appcache_document_was_loaded_from, url.mojom.Url opt_manifest_url); SelectCacheForSharedWorker(int32 host_id, int64 appcache_id); MarkAsForeignEntry(int32 host_id, url.mojom.Url document_url, int64 appcache_document_was_loaded_from); [Sync] GetStatus(int32 host_id) => (AppCacheStatus status); [Sync] StartUpdate(int32 host_id) => (bool success); [Sync] SwapCache(int32 host_id) => (bool success); [Sync] GetResourceList(int32 host_id) => (array<AppCacheResourceInfo> resources); };
对于单个AppCacheBackend来说,实际上就是一个有状态的对象,它们在浏览器进程中运行,负责处理所有这些消息。作为其正常操作的一部分,后端也可以发出HTTP请求。而针对这些请求的响应内容,实际上会影响控制流。因为它们是外部可用的,所以它们也是攻击面的一部分。
了解这些后,我们可以编写一个protobuf规范,让我们可以对后端执行一系列API调用,并处理它发出的所有请求。由于Chrome的代码允许我们覆盖本地模糊测试输入的网络数据源,从而简化了测试过程。
以下是我们的fuzzer protobuf规范的片段:
message Session { repeated Command commands = 1; } // Based on blink::mojom::AppCacheBackend interface // See third_party/blink/public/mojom/appcache/appcache.mojom message Command { oneof command { RegisterHost register_host = 1; UnregisterHost unregister_host = 2; SelectCache select_cache = 3; SetSpawningHostId set_spawning_host_id = 4; SelectCacheForSharedWorker select_cache_for_shared_worker = 5; MarkAsForeignEntry mark_as_foreign_entry = 6; GetStatus get_status = 7; StartUpdate start_update = 8; SwapCache swap_cache = 9; GetResourceList get_resource_list = 10; DoRequest do_request = 11; RunUntilIdle run_until_idle = 12; } } // We only need a few hosts to encapsulate all the logic enum HostId { HOST_N2 = -2; HOST_N1 = -1; HOST_0 = 0; HOST_1 = 1; HOST_2 = 2; } message RegisterHost { required HostId host_id = 1; }
这样,我们就建立了对表示API调用的"命令"序列进行模糊测试的protobuf。请注意,虽然RegisterHost API使用uint32_t作为输入,但我们仅提供一些可能的值,这些值在实际实现中会触发我们感兴趣的控制流(即正常情况和小负数,表示“预分配”主机)。
现在,让我们看看如何处理HTTP请求:
enum HttpCode { RESPONSE_100 = 100; RESPONSE_200 = 200; RESPONSE_206 = 206; RESPONSE_301 = 301; RESPONSE_302 = 302; RESPONSE_303 = 303; RESPONSE_304 = 304; RESPONSE_307 = 307; RESPONSE_308 = 308; RESPONSE_401 = 401; RESPONSE_403 = 403; RESPONSE_404 = 404; RESPONSE_500 = 500; RESPONSE_501 = 501; } message ManifestResponse { repeated Url urls = 1; } // Make sure to test logic when fetching more than the max concurrent allowed. enum UrlTestCaseIndex { EMPTY = 0; PATH_1 = 1; PATH_2 = 2; PATH_3 = 3; PATH_4 = 4; PATH_5 = 5; } message Url { required UrlTestCaseIndex url_test_case_idx = 1; } message DoRequest { required HttpCode http_code = 1; required bool do_not_cache = 2; required ManifestResponse manifest_response = 3; required Url url = 4; }
为了设计DoRequest消息,需要人工审阅AppCache的相关代码。影响AppCache后端控制流的内容包括HTTP相关代码、指示是否缓存响应的头部、用于manifest请求的manifest内容,以及请求的URL。这里,我们将该URL作为消息的一部分来进行存储,以应对两种可能出现的情况:预先准备一个响应,以便在请求到来时立即就绪,或者响应一个挂起的请求。第二种情况是对Chrome沙盒逸出漏洞进行模糊测试时所需要的,这就是我们在这里对其建模的原因。
相应的C++代码如下所示。请注意,这里使用Session消息作为基本的模糊测试消息类型:
DEFINE_BINARY_PROTO_FUZZER(const fuzzing::proto::Session& session) { network::TestURLLoaderFactory mock_url_loader_factory; SingletonEnv().InitializeAppCacheService(&mock_url_loader_factory); // Create a context for mojo::ReportBadMessage. mojo::Message message; auto dispatch_context = std::make_unique<mojo::internal::MessageDispatchContext>(&message); blink::mojom::AppCacheBackendPtr host; AppCacheDispatcherHost::Create(SingletonEnv().appcache_service.get(), /*process_id=*/1, mojo::MakeRequest(&host)); for (const fuzzing::proto::Command& command : session.commands()) { switch (command.command_case()) { case fuzzing::proto::Command::kRegisterHost: { int32_t host_id = command.register_host().host_id(); host->RegisterHost(host_id); break; } // ... case fuzzing::proto::Command::kDoRequest: { uint32_t code = command.do_request().http_code(); bool do_not_cache = command.do_request().do_not_cache(); const fuzzing::proto::ManifestResponse& manifest_response = command.do_request().manifest_response(); DoRequest(&mock_url_loader_factory, command.do_request().url(), code, do_not_cache, manifest_response); break; }
我们只需建立一个AppCache后端的实例,并让它跟我们在创建该实例时提供的模拟URL加载器工厂进行通信。我们将DoRequest实现为一个助手函数,来处理预先准备好的请求,或响应挂起的请求。
要想查看protobuf组件的完整源代码,请参考这里;关于C++组件的完整源代码,请参阅这里。
更多详情请参考:
Attacking Chrome IPC: Reliably finding bugs to escape the Chrome sandbox
小结
结构敏感型模糊测试方法是程序状态调查和漏洞挖掘领域中的“下一个重大里程碑”。该技术的重要性,可以与本世纪初以来的覆盖率导向的模糊测试技术相媲美。不过,如本文所述,结构敏感型模糊测试方法需要对每种输入类型进行大量的手动操作。因此,如何进一步提高该方法的自动化程度将是下一步的研究热点。
相关链接
· libprotobuf-mutator ——用于处理protobufs的Mutator。
· Getting Started with libprotobuf-mutator in Chromium.
· Adventures in Fuzzing Instruction Selection ——使用libFuzzer和LLVM IR的自定义mutator来查找LLVM优化过程中的安全漏洞。
· AFLSmart ——通过接受种子文件的高级结构表示,使AFL输入结构变得更加清晰。这能避免AFL中的随机位翻转突变,从而使得基于覆盖率的灰盒模糊测试在测试那些处理结构化文件格式(例如PDF、PNG WAV等)的应用程序时变得非常高效。
· syzkaller – kernel fuzzer.
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 通过结构化异常处理绕过CFG
- Golang 通过反射的方式调用结构体方法
- 数据结构是噩梦?想要通过面试,你必须掌握它
- 通过libFuzzer实现结构敏感型的模糊测试技术(上)
- Python cookbook(数据结构与算法)通过公共键对字典列表排序算法示例
- 无需理解数据结构也不需编程技能 Tableau如何通过数据问答再降低数据使用门槛
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。