为了清楚和简洁起见,下面引用的代码已精炼为最简单的形式。实际用于Fuzzing测试的完整版本可以在此处找到。
https://github.com/Rewzilla/domatophp
最近,我一直在对PHP解释器进行Fuzzing,我探索了许多工具和技术(AFL,LibFuzzer,甚至是自定义的Fuzzing引擎),但是最近我决定尝试Domato。对于那些不知道的人,Domato是基于语法的DOM Fuzzer,旨在从复杂的代码库中挖掘复杂的bug。它最初是为浏览器而设计的,但是我认为我可以将其用于Fuzzing PHP解释器。
https://github.com/googleprojectzero/domato
0x01 分析上下文语法
为了使用Domato,必须首先使用上下文无关的语法来描述语言,CFG只是一组定义语言构造方式的规则。例如,如果我们的语言由以下形式的句子组成:
[name]has[number][adjective][noun]s. [name]'s[noun]isvery[adjective]. Iwanttopurchase[number][adjective][noun]s.
这些变量中的每一个都可以采用几种形式,例如:
Names:alice,bob,eve Numbers:1,10,100 Adjectives:green,large,expensive Nouns:car,hat,laptop
那么上下文无关文法可能看起来像…
然后Domato使用上下文无关文法生成符合语言规则的随机组合。
evehas1expensivelaptops. alice'shatisverygreen. Iwanttopurchase100expensivecars. Iwanttopurchase10largelaptops. bobhas100expensivecars. evehas100greenlaptops. Iwanttopurchase100largelaptops. bobhas1largecars. Iwanttopurchase1largecars. Iwanttopurchase1largehats. bob'slaptopisveryexpensive.
可以想象,通过将每个规则分解为更多子规则,我们可以开始定义更复杂的语言,而不仅仅是简单的搜索/替换。实际上,Domato还提供了一些内置函数,用于限制递归并生成基本类型(int,char,string等)。
例如,以下Domato语法,该语法生成伪代码…
将其送入Domato会产生以下结果…
if(var0==var5){intvar5=915941154;}else{intvar3=1848395349;};if(var3==-121615885){intvar7=1962369640;;intvar1=196553597;;;intvar6=-263472135;;}else{intvar2==563276937;}; while(var9=var8){while(var0==-2029947247){intvar7=1879609559;}};charvar0='';; charvar2='/'; charvar3='P'; if(var8==var1){intvar7=-306701547;}else{while(var3==868601407){while(var0==-1328592927){charvar10='^';};charvar8='L';;;intvar9=-1345514425;;charvar5='b';;;}} intvar8=882574440; if(var8==var9){intvar7=1369926086;}else{if(var9!=-442302103){if(var3!=386704757){while(var4!=-264413007){charvar6='C';}}else{intvar8=289431268;}}else{charvar10='~';}} charvar5='+'; if(var9==1521038703){charvar2='&';}else{intvar7=-215672117;} while(var9==var0){charvar9='X';;intvar7=-1463788903;;};if(var8==var7){intvar10=1664850687;;charvar6='f';;}else{while(var5==-187795546){intvar3=-1287471401;}};
这非常适合Fuzzing解释器,因为每个样本都是不同的,并且仍然保证其在语法上是有效的!
0x02 列举Attack Surface
然后,下一步就是将PHP语言描述为CFG。如果有兴趣查看完整的CFG,请下载PHP源代码,然后查看Zend/zend_language_parser.y。
但是,我对Fuzzing特定的代码模式更感兴趣。因此,我实现了CFG,使其仅使用“Fuzzing”参数生成对内置函数和类方法的调用。为此,我们需要一个函数,方法及其参数的列表。
有两种获取此数据的方法。最简单的方法是使用PHP的内置Reflection类来遍历所有已定义的函数和类,从而构建一个列表。
以下代码对所有内部PHP函数进行了演示…
这会产生类似如下代码:
andrew@thinkpad/tmp%phplang.php zend_version(); func_num_args(); func_get_arg(arg_num); func_get_args(); strlen(str); strcmp(str1,str2); strncmp(str1,str2,len); strcasecmp(str1,str2); strncasecmp(str1,str2,len); each(arr); error_reporting(new_error_level); define(constant_name,value,case_insensitive); defined(constant_name); get_class(object); ...etc...
但是,此问题在于此列表不包含类型信息。ReflectionParameter类包含一个getType方法,但是对于大多数函数而言,它目前似乎不起作用。:(也许这是一个bug?很难说。无论如何,拥有类型信息将使我们的Fuzzing工作变得更加有效,因此值得花时间去寻找另一种获取该数据的方法。
https://www.php.net/manual/en/reflectionparameter.gettype.php
为了解析出我们需要的东西,PHP的文档通常相当不错,可以在此处将其作为单个压缩的HTML文档下载。经过数小时的辛苦编写正则表达式后,我能够将其解析为可用的函数,方法和参数类型列表。我将其留给读者练习,但是最终产品(以CFG形式)看起来像这样……
https://www.php.net/distributions/manual/php_manual_en.html.gz
0x03 设置Domato
为了使Domato使用我们的语法,我们还需要定义一些基本组件,例如:
经过大量的调整和调整后,我的配置最终看起来像这样……
我们还需要定义一个语法将被应用到的模板。该模板将设置环境,实例化以后可能使用的所有对象,然后运行每条线程。我的模板看起来像这样…
最后一步是复制和修改Domato的generator.py文件。我发现只需进行以下更改就足够了…
·第55和62行:将根元素更改为“
·第78行:引用我自己的“ template.php”
·第83行:在“ php.txt”中引用我自己的语法
·第134行:将输出名称和扩展名更改为“
然后,应该能够生成有效的Fuzzing输入!
andrew@thinkpad~/domato/php%pythongenerator.py/dev/stdout Writingasampleto/dev/stdout <?php $vars=array( "stdClass"=>newstdClass(), "Exception"=>newException(), "ErrorException"=>newErrorException(), "Error"=>newError(), "CompileError"=>newCompileError(), "ParseError"=>newParseError(), "TypeError"=>newTypeError(), ...etc... ); try{try{$vars["SplPriorityQueue"]->insert(false,array("a"=>1,"b"=>"2","c"=>3.0));}catch(Exception$e){}}catch(Error$e){} try{try{filter_has_var(1000,str_repeat("%s%x%n",0x100));}catch(Exception$e){}}catch(Error$e){} try{try{posix_access(implode(array_map(function($c){return"\\x".str_pad(dechex($c),2,"0");},range(0,255))),-1);}catch(Exception$e){}}catch(Error$e){} try{try{rand(0,0);}catch(Exception$e){}}catch(Error$e){} try{try{fputcsv(fopen("/dev/null","r"),array("a"=>1,"b"=>"2","c"=>3.0),str_repeat(chr(135),65),str_repeat(chr(193),17)+str_repeat(chr(21),65537),str_repeat("A",0x100));}catch(Exception$e){}}catch(Error$e){} try{try{$vars["ReflectionMethod"]->isAbstract();}catch(Exception$e){}}catch(Error$e){} try{try{$vars["DOMProcessingInstruction"]->__construct(str_repeat(chr(122),17)+str_repeat(chr(49),65537)+str_repeat(chr(235),257),str_repeat(chr(138),65)+str_repeat(chr(45),4097)+str_repeat(chr(135),65));}catch(Exception$e){}}catch(Error$e){} try{try{utf8_encode(str_repeat("A",0x100));}catch(Exception$e){}}catch(Error$e){} try{try{$vars["MultipleIterator"]->current();}catch(Exception$e){}}catch(Error$e){} try{try{dl(str_repeat("A",0x100));}catch(Exception$e){}}catch(Error$e){} try{try{ignore_user_abort(true);}catch(Exception$e){}}catch(Error$e){}
0x04 开始Fuzz
现在我们要处理的数据非常多,我们需要以一种最大化检测任何类型的内存损坏的机会的方式构建PHP。为此,我强烈建议使用LLVM Address Sanitizer(ASAN),它将检测任何无效的内存访问,即使它不会立即导致崩溃。
https://github.com/google/sanitizers/wiki/AddressSanitizer
用ASAN编译PHP,下载最新版本的源代码在这里,并运行以下命令…
https://www.php.net/downloads
./configureCFLAGS="-fsanitize=address-ggdb"CXXFLAGS="-fsanitize=address-ggdb"LDFLAGS="-fsanitize=address" make makeinstall
在Fuzzer运行之前,尝试消除不必要地阻碍该过程的任何条件也是一个好主意。例如,像大多数语言一样,PHP具有一个sleep()函数,该函数接受一个整数参数,并仅等待几秒后才能继续。用较大的值(例如INT_MAX)调用此函数将迅速占用较大的簇。
还有一些函数可能会导致进程合法地“崩溃”,例如posix_kill()或posix_setrlimit()。我们可能希望从测试语料库中删除这些内容,以减少误报的数量。
最后,由于PHP文档中列出的许多函数和类实际上在核心安装中不可用(而是从扩展中提供),因此我们不妨从资料集中删除其中的一些函数和类,以避免浪费时间调用不存在的代码。
最后,经过一番试验,我确定了以下清单…
$class_blacklist=array( //Can'tactuallyinstantiate "Closure", "Generator", "HashContext", "RecursiveIteratorIterator", "IteratorIterator", "FilterIterator", "RecursiveFilterIterator", "CallbackFilterIterator", "RecursiveCallbackFilterIterator", "ParentIterator", "LimitIterator", "CachingIterator", "RecursiveCachingIterator", "NoRewindIterator", "AppendIterator", "InfiniteIterator", "RegexIterator", "RecursiveRegexIterator", "EmptyIterator", "RecursiveTreeIterator", "ArrayObject", "ArrayIterator", "RecursiveArrayIterator", "SplFileInfo", "DirectoryIterator", "FilesystemIterator", "RecursiveDirectoryIterator", "GlobIterator", ); $function_blacklist=array( "exit",//falsepositives "readline",//pauses "readline_callback_handler_install",//pauses "syslog",//spamssyslog "sleep",//pauses "usleep",//pauses "time_sleep_until",//pauses "time_nanosleep",//pauses "pcntl_wait",//pauses "pcntl_waitstatus",//pauses "pcntl_waitpid",//pauses "pcntl_sigwaitinfo",//pauses "pcntl_sigtimedwait",//pauses "stream_socket_recvfrom",//pauses "posix_kill",//endsownprocess "ereg",//cpudos "eregi",//cpudos "eregi_replace",//cpudos "ereg_replace",//cpudos "similar_text",//cpudos "snmpwalk",//cpudos "snmpwalkoid",//cpudos "snmpget",//cpudos "split",//cpudos "spliti",//cpudos "snmpgetnext",//cpudos "mcrypt_create_iv",//cpudos "gmp_fact",//cpudos "posix_setrlimit" );
尽管一台机器既可以单独生成样本,但我还是选择了一小组来加快处理速度。我使用了在Intel NUC上运行的 Proxmox 和10个 Debian VM,其工作如下:
·节点0:样本生成,托管NFS共享。
·节点1-8:Fuzzing节点,从NFS共享中提取样本进行测试。
·节点9:“分类”节点:根据崩溃指标对崩溃样本进行分类。
我创建了简单的原始shell脚本以在每个脚本上运行以执行这些职责,这些脚本可以在上面链接的github repo中找到。
0x05 分析Crashs
几分钟内,该Fuzzer就生成了多个崩溃样本,一夜之间就生成了2,000多个。
通过根据崩溃的指令地址对崩溃进行分类,我能够确定所有2,000个崩溃都是3个错误造成的。其中,有2个显然无法利用(两个都是由于堆栈耗尽导致的OOM错误),但是最后一个似乎是UAF!这是最小化的崩溃示例…
此错误已在bug#79029中得到修复,应该包含在下一个版本中。在接下来的几篇文章中,我将讨论将其根本原因,实现任意代码执行的过程,以及在此过程中发现的一个巧妙的shellcode技巧。
https://bugs.php.net/bug.php?id=79029
本文翻
转载请注明:IT运维空间 » 安全防护 » 用Domato通过Fuzzing对PHP进行漏洞挖掘研究
发表评论