我们专注漏洞检测方向:danenmao、arnoxia、皇天霸、lSHANG、KeyKernel、BugQueen、zyl、隐形人真忙、oxen(不分先后)

欢迎关注我们的微信公众号:EnsecTeam
作者:隐形人真忙 & KeyKernel

TL;DR

本文主要介绍通过Hook技术和漏洞Fuzz的方式来构建灰盒自动化漏洞挖掘系统的技术原理与细节,并给出企业级部署实践与检出效果。

1. 什么是灰盒扫描

传统扫描器的原理实际上是构造一些探测串,一般称之为载荷(payload),用这些构造出来的数据进行组包并发送给服务器,当服务器返回数据或者做出的反应符合某种判定规则时,就认为有漏洞。这个过程是一个黑盒探测的过程,并不涉及到服务的具体逻辑,而且受网络通信的制约较大——如进行延时SQL注入扫描时,网络延迟的大小直接影响扫描的准确性。此外,基于黑盒的传统扫描往往需要发送大量的数据包,如布尔型SQL注入漏洞扫描中,需要构造大量的闭合语法扫描payload,可能仅仅检测一条URL就要发送1000个以上的请求。
灰盒漏洞扫描正是为了弥补传统黑盒扫描的不足,通过HOOK需要检测的敏感函数,可以在web应用运行时获取到对应的危险参数,配合Fuzz技术,可以更加高效、准确地发现漏洞。

2. 灰盒扫描架构设计

本节以PHP语言为例,来讲解如何构建一个适用于PHP的动态灰盒扫描系统。灰盒扫描系统一般分为三部分:
(1) HOOK部分
主要实现对一些危险函数进行HOOK,确保在运行时环境中能够获取到传入函数的任意参数。
(2) Fuzz部分
主要生产一些精心构造的污染数据,比如包含一些特殊符号的字符串等。通过扫描发包的方式将脏数据插入到各个待检测的地方。
(3) 规则部分
主要负责对漏洞的识别。通过和Fuzz系统进行配合,检查HOOK到的参数是否符合判别规则,从而判断是否存在漏洞,如果存在则发起上报流程。
这三个模块如下图所示进行整合:

当然,基于fuzz的思路也有缺点,那就是非常依赖于待测web应用的URL输入源的质量。如果URL不够完整,很可能会漏掉某些代码片段,无法做到像静态代码扫描一样的全面检查。事实上,这也是静态分析技术和动态分析技术的区别,即动态分析技术的代码覆盖很难做到非常全面。

3. 构建灰盒扫描系统
3.1 构建HOOK层
3.1.1 HOOK原理
3.1.1.1 Zend和PHP

PHP是一个解释型语言,我们编写的PHP脚本文件需要解释器进行解析,生成中间代码然后执行。实现一个解释语言一般需要三个部分:
(1) 解释器部分:分析输入的代码,翻译该代码,然后执行代码。
(2) 功能部分:完成语言的一些特性和功能(如函数、类等等)
(3) 接口部分:提供外部统一交互接口(如与Web通信等)
Zend引擎是PHP实现的核心,它完成了解释器部分的实现,并且部分参与
PHP的功能部分实现,如一些PHP语言的实现、扩展机制、内存管理机制等。Zend引擎与PHP的关系图如下:

通常我们都选择使用实现一个扩展的形式来扩展PHP,在PHP扩展中,我们可以调用Zend API做很多定制化的功能,包括实现对PHP内置函数调用的HOOK操作。

3.1.1.2 PHP Opcode

PHP是一种解释型语言,运行一段PHP代码通常包含两个阶段:编译和执行,其中编译部分包含语义分析、语法分析等阶段,这些功能由Zend引擎负责提供。一段PHP脚本文件经过Zend内核的处理,会变为一系列的操作码,称为opcode。这是一种Zend内核可以理解的中间语言,在执行阶段就是去执行这些opcode完成操作。
我们可以使用vld扩展(http://pecl.php.net/package/vld)来查看某段PHP脚本代码的opcode,安装方法如下:

[root@localhost]# wget http://pecl.php.net/get/vld-0.14.0.tgz
[root@localhost]# tar zxvf vld-0.14.0.tgz
[root@localhost]# cd vld-0.14.0/
[root@localhost]# phpize
[root@localhost]# ./configure - -with-php-config=[your path of php-config] - -enable-vld
[root@localhost]# make && make install

安装完成后,编辑一段PHP代码如下:

<?php
echo “hello world” ;
?>

执行下面的命令输出opcode:

[root@localhost]# php –dvld.active=1 test.php


可以清晰地看到在Zend内核中,针对echo是有一个名为“ZEND_ECHO”的opcode对应的。通过vld工具,我们可以看到函数调用过程中,Zend是分配了一个名为”ZEND_DO_FACALL”的opcode进行处理。

Opcode对应的结构体定义在{$PHP_HOME}/Zend/zend_compile.h,{$PHP_HOME}代指本地的PHP源码文件所在路径,下文的描述中统一使用这个替代符。
这里以PHP 5.4为例,低版本的PHP如5.2中opcode的定义截然不同。

struct _zend_op {
    opcode_handler_t handler;  // opcode对应的处理函数
    znode_op op1;  //第一个操作数
    znode_op op2;  //第二个操作数
    znode_op result;   //该opcode执行结果
    ulong extended_value;  //附带信息
    uint lineno;   //对应的行号
    zend_uchar opcode;   //对应的op指令
    zend_uchar op1_type;  //op1的类型
    zend_uchar op2_type;   //op2的类型
    zend_uchar result_type;  //返回值类型
};

每个opcode对应一个opcode_handler_t类型的函数,该函数用于实现opcode对应的操作。Handler函数定义可以在{$PHP_HOME}/Zend/zend_vm_execute.h中找到,比如ZEND_ECHO指令的一个handler函数如下:

static int ZEND_FASTCALL ZEND_ECHO_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zval *z;
    SAVE_OPLINE();
    z = opline->op1.zv;  // 提取操作数
    if (IS_CONST == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
        INIT_PZVAL(z);
    }
    zend_print_variable(z);  // 执行打印操作
    CHECK_EXCEPTION();  // 检测异常
    ZEND_VM_NEXT_OPCODE();  // 移动至下一个opcode
}

PHP内核的知识非常之丰富,但毕竟不是本文的重点,编写健壮、可持续运行的HOOK机制还需要对Zend内核源码更加细致的研究才能实现,这里由于篇幅限制不进行赘述。

3.1.2 HOOK实现
对PHP内置函数进行HOOK以获取运行时的函数参数,一般来说有下面三种方式:
(1) 对特定的Opcode进行HOOK
(2) 对ZEND_DO_FCALL进行HOOK
(3) 对PHP_FUNCTION实现函数进行HOOK
下面分别介绍这三种方式如何实现函数的HOOK

3.1.2.1 HOOK FCALL
在PHP内核中,每一个OP操作都是由一个固定的op Handler函数去负责的,即_zend_op结构体的handler属性,表示该OP对应的op handler函数。
我们可以通过调用zend_set_user_opcode_handler,将对应的Zend Opcode的handler函数替换成自己定义的函数实现HOOK机制:

zend_set_user_opcode_handler(ZEND_ECHO, hook_echo_handler);

当PHP执行ZEND_ECHO(即调用echo输出字符串)的时候就会调用我们的handler函数,这样就实现了HOOK,完成我们自定义的操作后,将opcode的处理转给默认的Opcode handler函数即可。
很多PHP内置函数的调用底层都是ZEND_FCALL进行实现的。

因此,我们需要HOOK两个opcode,ZEND_DO_FCALL和ZEND_DO_FCALL_BY_NAME,后者是通过动态参数调用的opcode。
而在我们自己的handler中,可以通过获取这两个opcode函数名称来对不同的函数进行hook操作。在PHP 5.4版本中,HOOK思路如下:
(1)从EG(current_execute_data)->function_state.function中获取原有的函数。
(2)然后对函数调用和类方法调用分别来执行HOOK操作
(3)HOOK完成后,恢复原有函数,将function_state.function重新赋值
注意,对于不同的PHP版本,所使用的结构体是不一样的。
在执行函数调用时,函数参数是保存在一个Zend虚拟机的栈结构中,因此我们需要移动指针来获取函数中的所有参数。
在FCALL的自定义handler中,函数参数分布图如下:

参数放入了EG(argument_stack)结构,因此首先获取一个栈顶指针:

void** p = EG(argument_stack)->top ;

获取函数参数个数的方法如下:

int arg_count = opline->extended_value;

拿到参数个数之后,我们就可以操作栈顶指针获取函数的每个参数了:

zval *arg1 =*((zval**)(p - arg_count)) ;   //参数1
zval *arg2 =*((zval**)(p - (arg_count - 1))) ;  //参数2
zval *arg3 =*((zval**)(p - (arg_count - 2))) ;  //参数3

获取到所有的参数之后,我们就可以结合Fuzz规则进行漏洞识别了。

3.1.2.2 HOOK Opcode
PHP中的一些“函数”的实现比较特殊,比如echo、include、eval等,这些函数在PHP内核中是直接调用特定的Opcode来执行的,如果只对FCALL指令进行HOOK,显然是不完整的。因此我们需要额外处理下列的Opcode:

ZEND_ECHO
ZEND_PRINT
ZEND_EXIT
ZEND_INCLUDE_OR_EVAL

具体实现形式和HOOK指令ZEND_DO_FCALL一致,唯一的区别在于获取参数的方式有所不同,在上一节中,我们通过一个Zend里的栈结构去获取到函数参数,而这次我们需要从Opcode的操作数中获取参数。

获取操作数的大致思路是,首先从zend_op结构中获取op1或者op2,然后根据op1_type或者op2_type分情况抽取参数值:
(1)IS_TMP_VAR
如果op的类型为临时变量,则调用get_zval_ptr_tmp获取参数值。
(2)IS_VAR
如果是变量类型,则直接从opline->var.ptr里获取
(3)IS_CV
如果是编译变量参考ZEND_ECHO_SPEC_CV_HANDLER中的处理方式,是直接从EG(active_symbol_table)中寻找。
(4)IS_CONST
如果op类型是常量,则直接获取opline->op1.zv即可。
上述方法都是从PHP源码中选取的,比如一个ZEND_ECHO指令的Handler会有多个,分别处理不同类型的op,这里有:

ZEND_ECHO_SPEC_VAR_HANDLER
ZEND_ECHO_SPEC_TMP_HANDLER
ZEND_ECHO_SPEC_CV_HANDLER
ZEND_ECHO_SPEC_CONST_HANDLER

比如我们可以通过阅读ZEND_ECHO_SPEC_TMP_HANDLER这个handler函数的源代码找到获取TMP类型操作数的方法:

static int ZEND_FASTCALL  ZEND_ECHO_SPEC_TMP_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
    USE_OPLINE
    zend_free_op free_op1;
    zval *z;
    SAVE_OPLINE();
    // 获取操作数
    z = _get_zval_ptr_tmp(opline->op1.var, EX_Ts(), &free_op1 TSRMLS_CC);
    if (IS_TMP_VAR == IS_TMP_VAR && Z_TYPE_P(z) == IS_OBJECT) {
        INIT_PZVAL(z);
    }
    zend_print_variable(z);
    zval_dtor(free_op1.var);
    CHECK_EXCEPTION();
    ZEND_VM_NEXT_OPCODE();
}

通过调用_get_zval_ptr_tmp这个API即可,类似的,其他类型的操作数获取也可以按照这个思路来实现。

3.1.2.3 HOOK PHP_FUNCTION
PHP中大多数内置函数是通过实现内部扩展的形式完成的,比如内置的
mysql_get_client_info函数就是通过扩展的形式实现的,其内部源码如下:

PHP_FUNCTION(mysql_get_client_info)
{
    if (zend_parse_parameters_none() == FAILURE) {
        return;
    }
    RETURN_STRING((char *)mysql_get_client_info(),1);
}

针对PHP_FUNCTION的HOOK流程大致如下,以HOOK system函数为例:
(1) 将system函数的别名改为我们自己的PHP扩展函数hook_system;
(2) 从当前function_table结构中找到system函数指针;
(3) 将system重命名为hook_system,用这个名称保存步骤1获取到的函数指针,保存在function_table结构中;
(4) 将system函数从function_table结构中删除;
(5) 当外界调用system函数时,会调用hook_system函数,在该函数中完成参数提取和漏洞判定操作;
(6) 恢复之前的函数指针,保证system函数正常执行
通过这种方式进行HOOK,可以很方便地使用zend_parse_parameters获取到
函数的参数,缺点是每个需要HOOK的函数都需要编写一个额外替换的扩展函数,维护起来比较繁琐。

3.2 构建Fuzz层

Fuzz系统主要负责构造一些特殊字符串,利用黑盒扫描的形式将其发送至安装了HOOK层的Web服务器,目的是与HOOK层的漏洞判断逻辑进行配合,比如传入一些特殊字符,如果这些特殊字符串进入危险函数的参数中没有发生有效的变形,则认为漏洞产生。
由于每次调用敏感函数,都会执行一次漏洞判定,因此为了性能考虑,Fuzz规则尽量做成较为简单且有效的形式。主要有三种设计思路:

3.2.1 基于特殊字符
很多Web漏洞,如SQL注入漏洞,在攻击时都需要引入特殊字符(如单引号、双引号、括号、分号等)进行语法闭合,因此很多过滤程序会对这些特殊字符进行净化或者转义处理。

因此我们可以通过扫描的形式向web应用传入一些特殊字符,比如针对SQL注入,我们发送字符串;).,\“(\”(;‘,如果后端调用mysql_query,HOOK层获取到其参数中原样存在这个字符串,就可以初步判断为漏洞。

3.2.2 基于特征字符串
除了特殊字符,还可以提前预定Fuzz的命中模式。比如针对SSRF漏洞的检测,我们可以传入一个内网的URL,当HOOK层获取到某些敏感函数的参数时,当参数中原封不动的存在该URL,就可以认为程序内部实际上没有有效的校验,否则也不会执行到调用敏感函数发起网络请求的代码。因此,这种情况也可以认为是漏洞命中。

3.2.3 基于复杂逻辑判断
某些更加复杂的规则是无法简单地用特征串的形式,比如检测文件上传漏洞webshell等情况,需要获取一些额外的程序执行信息才能执行判断,这就需要做成类似于“扫描插件PoC”的形式。

4.灰盒扫描形态与部署
灰盒动态扫描本质上是为了弥补传统漏洞扫描的不足,从动态运行时的维度切入,来增强漏洞发现能力。同时,灰盒扫描不需要很复杂的判断逻辑就能获得较高的准确率和较低的误报率。
灰盒扫描实现中,可以将HOOK层和漏洞判断功能进行分离,这是因为HOOK层的变动往往需要重启fastcgi或者服务器,如果漏洞判别逻辑经常变动,会造成频繁重启。因此需要单独的agent进行实现,或者直接传递至云端进行分析处理。所以灰盒扫描系统的大致形态如下:

在甲方安全实践上,一般可以将其部署到线下测试环境,最理想的形态是和上线前阶段的漏洞扫描进行深度结合,与传统漏洞扫描进行相互补充,从而将大多数常规漏洞消灭在测试阶段。
对于线上环境,灰盒扫描往往会消耗服务器大量的资源,尤其是每次执行危险函数都会执行一次HOOK和漏洞判别操作,对性能的损耗很大。如果其损耗在业务方可接受的范围内,将灰盒动态扫描和公司内部的日常漏洞扫描进行结合也是一个不错的部署方案。

4.1.1 灰盒扫描HOOK选择

在灰盒扫描中,覆盖更多的函数意味着拥有更强的检测能力,但是同时也更多的影响了服务的运行。
通常,在测试环境中,我们使用HOOK opcode的方式,提升检测能力,并且同时也可以在测试环境中验证灰盒的检测规则以及能力。
HOOK opcode 执行时对性能损耗比较大,这时一般我们对我们不感兴趣的流量进行放行,对不感兴趣的函数进行放行。可以将性能损耗降低在10%以下。但是这种损耗,对于线上业务是不可接受的。
对于线上业务,采用定制HOOK特定函数的方式来实现,实测使用wordpress每次页面请求产生20次函数拦截的推送情况下只有会对php端产生3%以内的耗时影响,无推送时只有1%内的耗时影响。对整体业务的性能影响较低。

4.1.2 信息收集难点与解决方式

获取了恶意流量,得到了触发漏洞的场景,我们需要对该场景进行收集和上报,供安全工程师已经业务线查看。对于上报和收集在PHP场景下,比较困难。原因有:

  1. PHP页面开始结束的时间固定,页面结束后,无法做到优雅的延迟上报。
  2. 通过收集的场景信息,计算这个信息大小,如果每个页面超过几千kb,信息接收方也很难处理,同时对机器的CPU和内存都很有挑战。
  3. 可能需要开启其他的恶意信息上报,这个上报的信息同样很大。

受于上述原因,我们使用了Agent + so 的搭配,让Agent常驻系统中,开辟IPC通讯给so使用。so将收集到的数据传递到Agent。IPC通讯时,不建议使用有锁通讯。针对性能这块,只能慢慢尝试调优,将性能调整到预期的水平即可。

5.漏洞检出效果

事实上,对于一些注入类型的漏洞,比如SQL注入、命令注入、代码注入等,传统的扫描都需要触发内部的执行条件,比如最起码要成功闭合语法结构等。但是由于后端代码一般场景复杂,所以传统扫描会有很多由于场景缺失而造成的漏报和误报。
但是使用灰盒扫描就可以显著增加这些复杂场景的漏洞检出率,比如遇到insert、update、delete注入以及一些字符串变形的场景,也可以很准确地定位到漏洞:

目前自灰盒扫描上线以来,准确率达到99%以上,基本实现了零误报,大大降低了运营成本。同时也和传统漏洞扫描进行了互补,通过将灰盒扫描嵌入到上线前安全测试阶段,可以把大多数高危漏洞扼杀在上线前阶段。

点击收藏 | 0 关注 | 2
登录 后跟帖