安卓Native层共享库fuzzing技术思路及实践

  • A+
所属分类:移动安全

一、前言

fuzzing技术在漏洞挖掘领域是一个无法绕开的话题,无恒实验室也一直在使用fuzzing技术发现产品的问题。虽然fuzzing不是万能的,但是没有它是万万不能的。说它不是万能的其实也是相对的说法,理想状态下,例如在可接受的时间范围内,计算资源足够丰富且系统复杂度足够低的情况下,fuzzing就能够给你任何想要的结果。这就好像是让一台计算机去随机的print一些文字,只要时间足够长,随机字符产生的效率足够快,那么早晚有一天会print出一部《三体》出来。

然而理论是丰满的,现实是骨感的,虽然人类社会的计算资源和效率在不断增长,但是软件系统的复杂度却以更快的幅度在增长着,以往通过近乎于dumb fuzz就能找到漏洞的情况几乎绝迹了。所以fuzzing技术必然要朝着更高覆盖率的样本生成、更高效的代码路径移动算法(变异)、更合理的计算资源分配调度等方向去发展。

不敢妄言未来fuzzing技术能发展到什么程度,这要靠学术界和工业界的共同努力,但是如果我们有一天见到了一个AI在面对大部分未知系统的时候都能自己solve出bug,那么毫无疑问,它一定用到了fuzzing!情怀的部分就到这里,下面来脚踏实地的实践一下安卓的一些native binary如何去fuzz。


二、技术背景:


fuzzing二进制目前有很多流派,但都大同小异,目的都是以最快的速度产生样本覆盖更多的code path,显然在这个过程中以code coverage作为整个fuzzer的驱动导向是最科学的,也就是覆盖率引导的灰盒模糊测试技术CGF(Coverage-based Greybox Fuzzing),这里有必要对这个最为核心的技术背景做些介绍。

统计coverage信息的方法通常有以下几种:

1、Compiler Instrumentation:如果存在源码的情况下,这种方式做代码覆盖率统计和fuzz是最可靠且高效的,例如利用LLVM、GCC等,但可惜很多情况下拿不到源码。


安卓Native层共享库fuzzing技术思路及实践


https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html

https://llvm.org/docs/CommandGuide/llvm-cov.html


2、Execute Simulation:例如基于QEMU、Unicorn模式的AFL就会在QEMU准备翻译执行基本块之前插入覆盖率统计的代码,缺点是执行效率有点低。


安卓Native层共享库fuzzing技术思路及实践


https://github.com/edgarigl/qemu-etrace

https://andreafioraldi.github.io/articles/2019/07/20/aflpp-qemu-compcov.html


3、Runtime Trace:这种形式的代码路径覆盖比较灵活,实现形式也比较多样化,也是本文所用到的主要方法。较为常见的方式例如使用frida动态插桩、调试启动、或者直接改造手机ROM等方式都可以实现,缺点是由于架构复杂稳定性很难保证,很多时候还没等target crash,fuzzer自己先crash了。文章后面会详细一些的介绍用到的frida stalker工具。


安卓Native层共享库fuzzing技术思路及实践


https://frida.re/docs/stalker/


4、Binary Rewrite:这种方法主要是针对二进制disassemble的每个基本语句块进行插桩,如果做的比较理想,效果可能仅次于Compiler Instrumentation,但缺点是它太难了,如果仅仅是个比较简单的binary问题倒还不大,但如果是个复杂度很高的系统实现起来就会困难重重,例如有些binary自带VM的情况、binary存在runtime rewrite自身的机制、binary使用了一些CPU的特殊Architecture Feature、各种binary重定位问题等等,这些处理不好都会让rewriten binary无法顺利执行。


安卓Native层共享库fuzzing技术思路及实践


https://github.com/GJDuck/e9patch

https://github.com/utds3lab/multiverse

https://github.com/talos-vulndev/afl-dyninst


5、Hardware Trace:这可能成为未来binary fuzzing的主流方向,硬件对软件天然就处在上帝视角中,这里要明确一下我所说的Hardware Trace这个范畴,并不是真的需要搞个专用于fuzzing的硬件,这里主要是说利用硬件与操作系统之间的那一层的能力去完成fuzz的目的,这对fuzz操作系统自身尤其有效,例如利用hypervisor、硬件调试器等的能力,当然也不排除有一天会有人搞出个FPGA甚至ASIC来跑fuzz,哈哈,那简直太硬核了!


安卓Native层共享库fuzzing技术思路及实践


https://github.com/gamozolabs/applepie

https://the-elves.github.io/Ajinkya-GSoC19-Submission/


三、目标选择


安卓Native层共享库fuzzing技术思路及实践


关于如何选择fuzzing目标这点,主要从安卓so库的安全风险角度分享一下我的经验和思路:


1、有攻击面:这是最先要确认的一个点,虽然理论上说任何程序都有攻击面,但是有大有小,有多有少,我们倾向于选择攻击面大的目标,这样才更有价值,例如有些so库可能会直接接收用户的外部数据进行处理,例如视频播放器、图片解析引擎、js解析引擎等等,这些so库如果出现漏洞,大概率上比较容易直接利用。


2、高频应用:更高频被用到的so库也是值得重点考量的,越是高频被用到,就越易于攻击利用,风险也就越大,例如有些工具util性质的so库,可能会被好多其他库调用,出问题的概率很大。


3、复杂度高:理论上说,漏洞的产生的概率与系统复杂度成正比,越是复杂就越是容易出问题。


4、消减措施缺失:某些so库在编译过程中可能没有考虑到安全性,没有开启安全编译选项,这就会导致其上的漏洞很容易被利用,风险也很大。


四、样本生成


安卓Native层共享库fuzzing技术思路及实践


确认了目标以后,就要开始考虑目标的业务逻辑了,越是能清晰的了解测试目标,就越是能准确的构造出好的样本,提升fuzz的效率。这个过程就好像导弹在击中目标之前的制导过程,需要明确击中目标所要经过的各个路径和需要绕过哪些障碍等。落实到业务上就是需要了解目标会接收什么样的输入数据、对数据处理的过程是怎样的、是否需要交互、是同步还是异步等等,越准确清晰越好。例如,现在要fuzz的目标是一个视频解码引擎的H265解码算法,那么就要考虑如何去基于H265的编码算法生成一些视频样本,如果样本使用其他一些MPEG、AVS、WMV等的编码,那可能fuzz到天荒地老也未必能找出一个H265解码器的问题。

“精确制导”之后我们也需要去做一些类似“火力覆盖”的事情,因为生成一个单一样本很难做到最大化的code coverage,所以我们需要在目标接收数据范围内做一些多样性的变化,产生一个样本集,通过大量多样性的样本达到一个比较满意的代码覆盖率。

本文使用ffmpeg库对H265的视频样本进行生成,示例部分代码逻辑。

对原始帧做一些随机变化:

C++
AVFrame *pFrame;//...pFrame->best_effort_timestamp = gen_fuzz_integer();pFrame->channel_layout = gen_fuzz_integer();pFrame->best_effort_timestamp = gen_fuzz_integer();pFrame->coded_picture_number = gen_fuzz_integer();pFrame->decode_error_flags = gen_fuzz_integer();pFrame->flags = gen_fuzz_integer();pFrame->height =gen_fuzz_integer();pFrame->width = gen_fuzz_integer();pFrame->nb_extended_buf = gen_fuzz_integer();pFrame->key_frame = gen_fuzz_integer();pFrame->nb_samples = gen_fuzz_integer();pFrame->palette_has_changed = gen_fuzz_integer();if(i%9 == 0) {pFrame->pkt_duration = gen_fuzz_integer();}pFrame->pkt_pos = gen_fuzz_integer();if(i%99 == 0){pFrame->pts = gen_fuzz_integer();}//...参数过多不一一列举


视频参数也进行些随机多样化的设置:


C++
AVCodecContext *pCodecCtx;//...pCodecCtx = video_st->codec;pCodecCtx->codec_id = AV_CODEC_ID_HEVC;//H265pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;pCodecCtx->width = abs(gen_fuzz_integer())%32768;pCodecCtx->height = abs(gen_fuzz_integer())%32768;pCodecCtx->bit_rate_tolerance = abs(gen_fuzz_integer());pCodecCtx->bit_rate = 1000 + abs(gen_fuzz_integer())%200000;pCodecCtx->gop_size = abs(gen_fuzz_integer())%10000;pCodecCtx->time_base.num = 1+abs(gen_fuzz_integer())%10;pCodecCtx->time_base.den = 25+abs(gen_fuzz_integer())%10000;pCodecCtx->qmin = abs(gen_fuzz_integer())%10;pCodecCtx->qmax = abs(gen_fuzz_integer())%1000;pCodecCtx->max_qdiff = gen_fuzz_integer()%128;pCodecCtx->qcompress = gen_fuzz_float();pCodecCtx->me_range = gen_fuzz_integer()%128;pCodecCtx->active_thread_type = gen_fuzz_integer()%X265_MAX_FRAME_THREADS;pCodecCtx->sample_rate = abs(gen_fuzz_integer())%400000;//...参数过多不一一列举


还可以选择性的在视频流封装前搞点事情:


C++
unsigned char *mutated_data = (unsigned char *) malloc_and_fillfuzzdata(mutated_size);memcpy((unsigned char *) pkt.data + mutated_offset, mutated_data, mutated_size);


这样生成出来的视频文件其实已经经过一定程度的变异,甚至可以直接fuzz出一些crash了。


五、覆盖率引导


样本集有了,我们需要进行一些裁剪工作,主要根据样本对被测目标的覆盖率进行筛选,选出能够最大化coverage的最小集合,然后再对这个最小样本集进行变异,这样可以避免生成一些重复性的测试cases。

技术背景中提到过的stalker是这个环节的核心,stalker是frida系列神器的代码tracer,可以做到函数级、基本块级、指令集的代码tracing。不过这个工具之前一直对arm支持的不够好,目前frida 14.0的版本也仅支持arm64。

stalker的主要原理是dynamic recompilation,这里我们简单介绍一些stalker中的专有名词概念:

Blocks:基本块,与编译原理中的概念相同,不再赘述。

Probes:Probes就是一个基本块的桩点,如果你用过interceptor的话,他们很类似。

Trust Threshold:这个概念稍微有点绕,它实际主要是为了优化编译执行效率,但是设置它却和一些带有self-modifying功能的程序有关,例如某些加过壳或者做了anti-disassembly的binary在runtime期间会对自身的代码进行rewrite,这个过程stalker就必须要对代码重新进行dynamic recompilation,这将会是个非常耗时且麻烦的过程。所以stalker为blocks设置了一个阈值N,当blocks执行过N次之后,这个block将会被标记为可信的,以后就不会再对它进行修改了。

了解了这些概念后,继续来看一下stalker在dynamic recompilation过程中的基本操作。stalker会申请一块内存,以基本块为单位写入instrumented后的block copy并插入Probes,重定位位置相关的指令例如ADR Address of label at a PC-relative offset.,对于函数调用,保存lr相关的上下文信息,建立这个块的索引并且进行count计数,达到Trust Threshold的设定阈值的就不再进行重新编译,接下来执行一个基本块然后继续开始下一个。这个过程限于篇幅只能说个大概,实际的处理过程比较复杂,感兴趣的同学可以自行去读一下stalker的代码加深理解。


安卓Native层共享库fuzzing技术思路及实践


通过stalker的能力,我们可以在trace target的过程中清晰的拿到coverage,后续通过coverage来优化样本集,引导变异过程等就都很轻松了。例如我们可以通过建立bitmap的方式记录覆盖状态,根据coverage的高低来对样本进行筛选,还可以通过coverage来确定每次样本变异后的的效果等。

最后截几小段关键代码示例一下:


JavaScript
// Always trust code.Stalker.trustThreshold = 0;var stalker_events = []
// attach to target and initial stalkersettarget: function(target) { debug("Target: " + target) target_function = ptr(target)
Interceptor.attach(target_function, { onEnter: function (args) { stalker_attached = true stalker_finished = false Stalker.queueCapacity = 100000000 Stalker.queueDrainInterval = 1000*1000 debug("follow @ " + target); Stalker.follow(Process.getCurrentThreadId(), { events: { call: false, ret: false, exec: false, block: false, compile: true }, onReceive: function (events) { stalker_events.push(events) debug("onReceive: len(stalker_events)=" + stalker_events.length) } }); }, onLeave: function (retval) { debug("unfollow @ " + target); Stalker.unfollow(Process.getCurrentThreadId()) Stalker.flush(); if(gc_cnt % 100 == 0){ Stalker.garbageCollect(); } gc_cnt++; stalker_finished = true } });},
// get the coveragegetcoverage: function(args) { debug("getcoverage: len(stalker_events)=" + stalker_events.length) if(stalker_events.length == 0) return undefined; var accumulated_events = [] for(var i = 0; i < stalker_events.length; i++) { var parsed = Stalker.parse(stalker_events[i], {stringify: false, annotate: false}) accumulated_events = accumulated_events.concat(parsed); } return accumulated_events;},
// log coveragelogcoverage: function() { debug("log coverage"); var cov = rpc.exports.getcoverage(); debug("getcoverage done"); rpc.exports.clearcoverage();
if(cov != undefined) { debug("covlen: " + cov.length); Memory.writeU32(prev_hit, 0); var hit_count = 0; for (var i = 0; i < cov.length; i += 1) { var start = cov[i][0]; var end = cov[i][1]; if((start >= trace_modules[0]["base"] && end <=trace_modules[0]["end"]) || (start >= trace_modules[1]["base"] && end <=trace_modules[1]["end"]) || (start >= trace_modules[2]["base"] && end <=trace_modules[2]["end"]) ) { BITMAP.cov_log(BITMAP.trace_bits, ptr(prev_hit), ptr(start)); } } debug("log coverage done.") return } else { return "no coverage" }},
//check coveragecheckcoverage: function() { debug("check coverage once"); var cnt = BITMAP.calc_bitmap_count2(BITMAP.trace_bits); BITMAP.map_rate = cnt * 100 / CONFIG.MAP_SIZE; return BITMAP.map_rate;},


六、结束语


fuzzing技术作为漏洞挖掘的经典手段,一直受到安全从业人员的喜爱,无恒实验室也一直致力于使用fuzzing技术发现产品的安全缺陷,提升产品质量。在这个过程中我们发现了大量安全性及稳定性问题,但是路漫漫其修远兮,未来无恒实验室会继续在fuzzing的智能化、高效化、精准化等方向持续投入研究,并且向业界贡献成果。


无恒实验室更多详情请点击阅读原文

本文始发于微信公众号(字节跳动安全中心):安卓Native层共享库fuzzing技术思路及实践

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: