# 基于PHP扩展的WAF实现

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2015-07-24T03:24:00Z
Canonical: https://wepostx.com/topics/920

> 乌云历史资料归档
>
> **原始作者：** lightless
> **原始编号：** superkieran-wooyundrops:658
> **原始发布时间：** 2015-07-24 11:24
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

# 0x00 前言

最近上(ri)网(zhan)上(ri)多了，各种狗啊盾啊看的好心烦，好多蜜汁shell都被杀了，搞的我自己也想开发这么一个斩马刀，顺便当作毕设来做了。
未知攻，焉知防。我们先来看看shell们都是怎么躲过查杀的：加密、变形、回调、隐藏关键字……总之就是一句话，让自己变得没有特征，这样就可以躲过狗和盾的查杀。但是万变不离其宗，无论怎么变形，最终都会回到类似这样的格式：

```text
#!php $_GET($_POST)
```
分为执行数据部分($_GET)和传递数据的部分($_POST)，也就是说，无论怎么变形，在执行的过程中都会变成这个样子，接下来还是去执行类似system(), exec(), eval()等等函数，那么我们就直接定位到这里，检测该脚本是否调用危险函数，或是在脚本调用这些函数的时候进行分析，判断该脚本是否为非法用户的shell，会取到很不错的防御效果。

# 0x01 如何获取先机

既然分析出来了问题的根结，那么下一步就是要控制住这些危险函数的入口点，即system()等函数的底层入口，就像Windows API中hook掉zw系列函数一样，我们也要hook掉system()这些函数。
这里我们需要用到PHP Extension， 即PHP扩展，位于PHP内核zend和PHP应用代码之间，很明显，扩展可以监控应用层代码的执行细节，也可以调用内核提供的ZEND API接口，包括禁用类、禁用函数等等。图为PHP结合其他必要组件的基本结构。

![原文图片](/media/2016/06/e0d12b87000cfdba61fb5de1fee617ea)

在ZEND中提供了接口供我们进行这样的操作，通过zend_set_user_opcode_handler就可以达到目的，根据某大牛所说，只需要hook三个OPCODE即可。

```text
#!c ZEND_INCLUDE_OR_EVAL 处理eval()、require等 ZEND_DO_FCALL 函数执行system()等 ZEND_DO_FCALL_BY_NAME 变量函数执行 $func = "system";$func();等
```
以ZEND_DO_FCALL为例，在MINIT中将ZEND_DO_FCALL替换掉：

```text
#!c zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL);
```
接着定义自己的处理函数，这里是LIGHT_DO_FCALL。

```text
#!c static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS) { /* 检测是否为合法调用 */ if (/*非法调用*/) { /*拦截掉，不执行。*/ } else { /*合法调用，继续执行*/ return ZEND_USER_OPCODE_DISPATHC; } }
```
这样一个骨架就完成了，其余的内容可以自行添加。这样每当调用system()等函数时，都会经过你自己的LIGHT_DO_FCALL，于是就可以对上层的代码执行进行检查了。

# 0x02 上面的方法太粗暴

上面的方法固然是好，但是误判会很多，甚至一个普通的函数调用都会直接被ban掉。这样的WAF放在业务中一定会被喷出翔的。我们需要换个思路。
之前已经说过了，无论如何变形、加密、隐藏关键字等，最终都需要调用到system()，eval()等函数，终究是逃不过这些的。那么我们把刚才的思路的变一下，不要直接hook那几个OPCODE，向上走一层。
eval()函数在底层是要调用zend_compile_string函数的，那么我们是不是可以在底层重载掉zend_compile_string函数，或是用自己的函数替换掉呢？答案肯定是可以的，我们只需要重写自己的zend_compile_string函数即可。
但是对于system来说有些不一样，上面我们hook掉了ZEND_DO_FCALL，但是太粗暴了，我们这里可以在function_table中删除掉system，并用我们自己的函数注册system函数，这样就可以做到了实时检查，如果为合法调用，可以调用旧的system函数并继续执行。
很明显这样做的话，确实减少了误杀，但是也增加了风险，因为除了system()之外还有很多可以用于执行命令的函数。除了执行命令，还有遍历目录、读敏感文件的问题，需要考虑很多的方面，偏僻的函数都要考虑在内，还有PHP SPL、DirectoryIterator等等各种方面。

# 0x03 说了这么多我们看看实际效果吧

我们来看看hook三个OPCODE的方式会产生什么样子的效果。首先我们在MINIT函数中将这三个OPCODE的处理函数换成我们自己的。

```text
#!c ZEND_MINIT_FUNCTION(lightWAF) { /* * hook掉ZEND_DO_FCALL * 处理system函数等 */ zend_set_user_opcode_handler(ZEND_DO_FCALL, LIGHT_DO_FCALL); /* * hook掉ZEND_DO_FCALL_BY_NAME * 处理$func='system';$func();等类型 */ zend_set_user_opcode_handler(ZEND_DO_FCALL_BY_NAME, LIGHT_DO_FCALL_BY_NAME); /* * hook掉ZEND_INCLUDE_OR_EVAL * 处理eval, require等 */ zend_set_user_opcode_handler(ZEND_INCLUDE_OR_EVAL, LIGHT_INCLUDE_OR_EVAL); return SUCCESS; }
```
LIGHT_DO_FCALL， LIGHT_DO_FCALL_BY_NAME，LIGHT_INCLUDE_OR_EVAL的内容类似，均为判断脚本文件是否在upload目录中，如果在的话就禁止危险函数的执行，否则放行。这里我们用LIGHT_DO_FCALL说明一下：

```text
#!c /* LIGHT_DO_FCALL */ static int LIGHT_DO_FCALL(ZEND_OPCODE_HANDLER_ARGS) { char *filePath; filePath = zend_get_executed_filename(TSRMLS_C); php_printf("[Debug] filePath: %s\n<br />", filePath); if (strstr(filePath, "/upload/")) { /* 非法调用，拦截 */ php_printf("[Warning] Execute command via system() etc.\n<br />"); return ZEND_USER_OPCODE_RETURN; } else /* Not Found */ { /* 合法调用，放行 */ return ZEND_USER_OPCODE_DISPATCH; } }
```
这里我们看到，一旦检测到在upload目录中，就返回ZEND_USER_OPCODE_RETURN，实际作用就是不继续执行危险函数了，如果不在upload目录中，就返回ZEND_USER_OPCODE_DISPATCH，实际作用就是继续执行该函数。
扩展编写完成后，编译即可得到so文件，如果对PHP扩展开发以及编译不熟悉的同学，请参考这个扩展开发教程的第五章：http://www.walu.cc/phpbook/ ，开发方面的知识超出了本文的内容，所以这里不再赘述了。
编译完成后，将得到的lightWAF.so文件复制到php的扩展目录下，我这里是/usr/local/php/lib/php/extensions/，根据自己的实际情况进行调整。接下来修改php.ini，让PHP自动加载我们的扩展，在其中加上一行：

```text
extension=/path/to/our/lightWAF.so
```
我这里是：

```text
extension=/usr/local/php/lib/php/extensions/lightWAF.so
```
根据之前so文件放的位置不同，请自行修改路径。

![原文图片](/media/2016/06/8b1ed2168c56a384ae54fed1c2cb752b)

最后我们重启PHP服务，让设置生效。PHP重启完成后，在终端中执行php -m，来验证扩展是否加载成功，如果在结果中看到了lightWAF，则说明加载成功。（请无视图中的错误信息，那个是我本地环境的问题，对lightWAF以及PHP没什么影响）

![原文图片](/media/2016/06/d899a4647238d36a6c047af0b1ea1f5a)

成功加载后，我们看一下实际效果，这里有一个上传文件的页面，可以上传任何类型的文件并将上传的文件保存在upload目录中。现在模拟一下getshell的过程。

![原文图片](/media/2016/06/c867d37f755ef0487529abb6a62a7649)

我们写个小马，内容如下：

```text
#!php <?php system($_GET["cmd"]); ?>
```
进行上传，并访问shell，结果如下：

![原文图片](/media/2016/06/c84a066ea475733108e3763cb925e938)

可以看到被成功拦截了，好吧，我们换只牛逼的马儿，试试反射型的。

```text
#!php <?php $func = new ReflectionFunction("system"); echo $func->invokeArgs(array("$_GET[c][/c]")); ?>
```
继续上传并访问，看看结果：

![原文图片](/media/2016/06/5e63f87ed3204c3768c15b692c17dc0d)

依然被拦截了，只不过这次触发的是ZEND_DO_FCALL_BY_NAME这个OPCODE。 接下来我们看看正常的文件会不会被拦截，在lightWAF目录下写入一个test.php，用于模拟正常的业务文件，内容如下：

```text
#!php <?php system('ls'); ?>
```
访问一下看看结果：

![原文图片](/media/2016/06/24bf8a3d3a0bb707667cca7c807f0767)

可以看到ls命令成功的执行了，也就是说我们的正常文件是不会被拦截的，而只有upload目录中的文件会被拦截，这样做又会引发另一个弊端，倘若攻击者通过某种方法将shell写入正常的文件中，或是与业务结合起来，那么这种防御手段就很难生效了。具体如何防御还要结合其他的特征进行检测，并不是没有办法了，实际应用中不能只依靠检测文件路径这一条规则，需要结合业务进行部署防御方案。
另一种方法与这个hook三个OPCODE的方法类似，无非就是麻烦一点，感兴趣的同学可以围观下面的参考文献：http://security.tencent.com/index.php/blog/msg/19 ，这里描述了hook函数的比较详细的思路。

# 0x04 不谈业务的安全都是耍流氓

总结一下，拦截的方法大概就是上面两种，但是拦截的依据还没有决定，如何判断一个调用system()的脚本是否为webshell呢？如果我们的WAF放到生产环境，啥都不管乱杀，很有可能造成正常的业务无法工作。我自己总结了几个方法，可能联合起来使用效果更好一些。

- 根据目录判断 通常情况下，上传的文件一般都有专门的目录进行存放，例如upload/等，正常的业务文件是不在这其中的，于是我们可以简单的只对这个目录进行处理，其他目录的文件一律放行。

- 根据文件权限判断 这需要网站的维护者对网站的权限进行严格的控制，例如，所有的web文件均是644，且为root:root所有。上传的文件为www:www所有，根据权限的不同进行查杀，对于默认的web文件进行放行，对属于非root:root的文件进行查杀。也可以根据w标志判断，通过未知方式getshell的文件很多是带有w标志的，所以可以根据这些特征进行查杀。

- 变形检测 如果发现一个文件调用了assert、system、preg_replace /e等等，但是在源文件中没有发现这些关键字，还等什么，这个文件很大的可能就是shell了。（zend_get_executed_filename可以获得文件的标志以及文件的路径，例如出现了assert，说明该脚本使用了assert执行代码。）

- 黑名单检测 就像传统的杀软一样，总会有那么一部分的特征病毒库，我们也可以建立一部分的webshell特征库，先依靠特征库杀掉一部分，再根据其他的情况进行判断。如果将动态检测和静态检测结合起来，查杀、拦截效果应当都会有显著的改善。

# 0x05 所以这WAF有啥用

安全需要和业务结合起来进行，不谈业务的安全都是耍流氓。因为是扩展级别的WAF，在部署的时候可能需要重新编译，修改配置文件等等。批量式部署可能显得不是那么方便，而且要根据业务需要进行各种细微的调整。如果仅仅是几台服务器，相信这种WAF还是十分棒的，调整起来也十分方便。
如果能开发出适合批量部署的基于扩展的WAF，那么可能会比较容易普及，毕竟在业务上部署WAF不是一个简单的事情。

- 参考资料：
http://security.tencent.com/index.php/blog/msg/57 http://security.tencent.com/index.php/blog/msg/19 http://www.walu.cc/phpbook/ http://www.php-internals.com/book/?p=index http://www.nowamagic.net/librarys/veda/detail/1543

## Replies
