谁家好人还要写个PHP扩展来解混淆啊!来日方长,后会有期。 没想到吧,我与PHP混淆法XEND的爱恨纠葛,还在延续。在上集XEND解密(一)里,我用动态调试法解开了XEND最外层的混淆。一般的PHP混淆法,解完第一层混淆后,底下的明文就显露出来了,但XEND第一层混淆解开后,还有轻度的混淆,没有隐藏各种调用入口和函数名的弯弯绕绕了,可就是恶心:str_rot13、eval(base64_decode())和strrev乱飞的一个大几百行PHP文件。 第二层的混淆不复杂,混淆的手段就这三种,但动态调试或者手工还原会非常繁琐。但我当时懒(bushi),没有继续解第二层的混淆。结果,前段时间我收到了网友的交流邮件,这才下定决心解开第二层的混淆。 不求甚解才是进步的最大敌人。 一、HOOK EVAL 大法好! 在第二层的混淆上,既然动态调试和手工解密变得事倍功半了,那么有没有相对高效的第三种方法?讲到这里,我们不得不细细回想PHP的混淆法都有哪些比较泛化的特点。除了各种基于古典密码的字符串移位变形函数,如str_rot13,异或、ord之流,最为人熟悉的应该是可以执行任意代码的高危表达式eval,因为绝大部分的webshell都会把混淆后的代码交给eval执行。 那么,不管是变形到多么面糊模糊的代码,交给eval执行,eval也得把它还原成明文才能执行,这样一想,找个办法把eval的参数打印出来,不就好了吗? 在PHP中,eval这些语言结构,在ZEND里最终会调用zend_compile_string,而如果你到PHP源码里查找这个函数,会在zend.c里找到这句: - zend_compile_string = compile_string;
复制代码并在zend_compile.h里找到如下声明:
- extern ZEND_API zend_op_array *(*zend_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position);
复制代码不难看出,zend_compile_string就是函数compile_string的函数指针。这个指针是PHP安全研究员、PHP核心开发者Stefan Esser于2006年率先提出的,以便在调用compile_string时执行某些操作,也是这位大佬,在2010年率先提出了通过编写扩展的方式,在zend_compile_string上挂钩子,打印它的参数source_string来获取还原好的明文,还贴心地提供了对应的PHP扩展。 二、半吊子PHP扩展开发:更适合PHP8宝宝体质的evalhook PHP的底层是C,我之前从来没有写过C,也没有接触过PHP内核和ZEND ENGINE,于是抱着学习的心态,开始了跌跌撞撞的PHP内核学习之旅。因为有其他编程语言的底子,看懂C代码并不难;想要参与PHP扩展开发,对新手来说,一开始的难点主要在于理解PHP扩展结构,特别是用于管理PHP扩展生命周期的几个宏,比如:PHP_MINIT,PHP_RINIT,PHP_MSHUTDOWN,和PHP_RSHUDOWN。 我在网上搜了一圈这个扩展,编译好的版本都是5.6的,扩展的源码也是基于PHP5.6。我寻思这PHP版本都进入8时代了,不如就把它按照PHP8的规范改写,顺便也让自己过一遍PHP扩展开发。说干就干! 首先是搭建PHP扩展开发环境,那就得编译安装PHP,并安装apache2,配置apache和PHP通信,以及PHP、PHP-FPM等服务的环境变量。这个网上可以找到教程,就不赘述(踩了蛮多坑的,但如果有人有需要,日后可以写一篇配环境的文章)。 原先的插件源码,也就是evalhook.c,要改动的地方不多,PHP_MINIT_FUNCTION和PHP_MSHUTDOWN_FUNCTION中的控制流程无需变动。这里我不得不说佬就是佬,斯特凡大佬很聪明地定义了一个布尔值evalhook_hooked用于流程控制,使得代码结构很简洁。 主要的改动在zend_compile_string这个指针指向的函数compile_string上。PHP8.2及其之后的8.3版本中,compile_string的参数由2个变为3个,多了一个参数position。同时,斯特凡大佬的插件原先有一个控制台交互功能,读取用户控制台输入Y/N来决定是否执行eval或终止进程,同时他打印输出也是打印在控制台。不过我们的目的是解webshell混淆,而很多webshell呢,内置了检查USER AGENT之类的反调试手段,因此这个打印输出的方式也要改一下,方便我们在WEB环境里查看(这里可以用curl和php内置server在命令行模拟web环境,避开webshell的UA检测,但这又是另一个故事了)。 更改的代码如下: - static zend_op_array *(*orig_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position);
- static zend_bool evalhook_hooked = 0;
- static zend_op_array *evalhook_compile_string(zend_string *source_string, const char *filename, zend_compile_position position)
- {
- int c, len;
- char *copy;
-
- /* Ignore non string eval() */
- if (ZSTR_LEN(source_string) == 0) {
- return orig_compile_string(source_string, filename, position);
- }
-
- len = ZSTR_LEN(source_string);
- copy = estrndup(ZSTR_VAL(source_string), len);
- if (len > strlen(copy)) {
- for (c=0; c<len; c++) if (copy[c] == 0) copy[c] == '?';
- }
-
- php_printf("\n--------- start decoding ------------\n");
- php_printf("%s\n", copy);
- php_printf("--------- end decoding ------------\n");
-
- return orig_compile_string(source_string, filename, position);
- }
复制代码在web环境下打开ote.php,点击view source,即可看到解密效果,第二层的混淆也被解开了:
查看页面源码已经能看到解密后的明文代码
原先第二层依然做了rot13等轻度混淆
参考资料:
|