最近(被迫)拿到了不少经过混淆的PHP代码样本,尤其是我使用的某个开源软件里面竟然也有被混淆的PHP代码(还有几十个JS后门),导致我不得不把它们都解混淆来检查一下。不过,这些只要20分钟就能写出通用解混淆代码的混淆有什么意义呢?

好想出去玩_(:з」∠)_但出不去,只好在家里应XCTF抗疫赛邀请出题了。但是实在没题出怎么办?恰巧看到Xray的一篇吐槽安全从业人员代码平均开发能力差的文章,考虑到解混淆需要一定的开发能力,不如来用混淆水一题吧……

顺带一提,本次比赛中,我观赏了一下各个队伍的去混淆脚本,基本上都是正则表达式+黑魔法的写法,根本看不懂……

How?

我相信很多人对写一个PHP混淆与去混淆是一头雾水,完全不知道怎么下手的状态,或者除了正则表达式以外就没有思路了。实际上,写混淆器等于写半个编译器。如果你的程序能够正确理解PHP代码中每一个“单词”的意思,那么你的混淆器就基本开发完成一半了。

一个编译器通常分为编译器前端和后端两个部分,编译器前端负责对代码的解析。我们要着眼的也基本就是前端部分。编译过程中的第一步是词法分析,词法分析器读入源程序的字符流,把他们组织成有意义的词素(lexeme);对于每个词素,词法分析器产生对应的词法单元(token)。如果我们使用PHP来开发的话,这个过程不需要我们来做。PHP有一个函数token_get_all,可以直接把PHP代码转换成token数组。

Token?

基于token数组,我们可以开发一个简单的变量重命名器:

$file = file_get_contents($path);

$variable = 0;

$map = [];

$tokens = token_get_all($file);

foreach ($tokens as $token) {

if ($token[0] === T_VARIABLE) {

if (!isset($map[$token[1]])) {

if (!preg_match('/^\$[a-zA-Z0-9_]+$/', $token[1])) {

$file = str_replace($token[1], '$v' . $variable++, $file);

$map[$token[1]] = $variable;

}

}

}

}

非常简单,可以将所有由不可见字符组成的变量名改成正常人可读的变量名。

enphp 即是直接基于该数组开发。由于词法分析器并不负责维护每个token之间的关系,enphp不得不维护相当多的状态,导致其后续的开发和维护较为复杂,我们也不会基于这一串token来开发。

编译的第二步是语法分析,由token序列确定语法结构,通常会输出一棵语法树(syntax tree)。PHP是一个成熟的语言,也有一个成熟的解析器。php-parser 可以帮助我们把PHP代码解析成一棵抽象语法树(AST),我们就将基于它来开发。

既然有了能表示代码结构的树,那我们就知道怎么一个正常的混淆器应当怎么开发了:

把原始代码解析成一棵树。

遍历树,修改树上的某些节点。

将树还原成代码。

练手

现在让我们开始吧,php-parser的安装请自行看文档。

我们从最简单的代码变换开始,第一步将

Hello World!

替换成

我们先写一个主体结构:

use PhpParser\Parser;

use PhpParser\ParserFactory;

use PhpParser\NodeTraverser;

use PhpParser\NodeVisitor\NameResolver;

use PhpParser\PrettyPrinter\Standard;

require './vendor/autoload.php';

// 初始化解析器

$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);

// 将代码解析成AST

$ast = $parser->parse(file_get_contents('test/test1.php'));

$traverser = new NodeTraverser();

// 注册一个“游客”跟着一起漫游

$traverser->addVisitor(new HTMLToEcho($parser));

// 开始遍历AST

$ast = $traverser->traverse($ast);

// 将AST转换成代码

$prettyPrinter = new Standard();

$ret = $prettyPrinter->prettyPrint($ast);

echo '<?php ' . $ret;

再写一个游客类:

use PhpParser\Node;

use PhpParser\NodeVisitorAbstract;

class HTMLToEcho extends NodeVisitorAbstract

{

public function leaveNode(Node $node)

{

// 当当前节点的类型是 InlineHTML

if ($node instanceof Node\Stmt\InlineHTML) {

// 将其替换成 echo 'value';

return new Node\Stmt\Echo_([

new Node\Scalar\String_($node->value)

]);

}

}

}

运行试试,是不是很神奇呢?关于NodeVisitor的使用,请直接阅读文档 Walking the AST。

开始

现在让我们开始写一个字符串混淆器和解混淆器。

我们现在想要:

var_dump('Hello World');

变成

var_dump(str_rot13('Uryyb Jbeyq'));

只需要在发现一个字符串调用的时候,把它换成函数就好了:

$traverser->addVisitor(new StringToROT13($parser));

// ......

class StringToROT13 extends NodeVisitorAbstract

{

public function leaveNode(Node $node)

{

if ($node instanceof Node\Scalar\String_) {

$name = $node->value;

return new Expr\FuncCall(

new Node\Name("str_rot13"),

[new Node\Arg(new Node\Scalar\String_(str_rot13($name)))]

);

}

}

}

解混淆器,就是一个反向的过程。发现一个函数调用str_rot13,且第一个参数为字符串,就把它替换回来:

class ROT13ToString extends NodeVisitorAbstract

{

public function leaveNode(Node $node)

{

if ($node instanceof Node\Expr\FuncCall &&

$node->name instanceof Node\Name &&

$node->name->parts[0] == 'str_rot13' &&

$node->args[0]->value instanceof Node\Scalar\String_

) {

$value = $node->args[0]->value->value;

return new Node\Scalar\String_(str_rot13($value));

}

}

}

毫无难度,对吗 :)

对比上面两边的代码,会发现,解混淆器本质上和混淆器区别极小,在这个例子中毫无区别。两者的模式都是寻找可以替换的特征,之后将其替换成另一种实现。

再进一步

虽说混淆器和解混淆器区别极小,但这不代表没有,它们在开发时的侧重点不太一样。实际上,上面的混淆器在很多情况下是无法工作的,例如:

function a ($a = 'abcd') { echo $a; }

把这行代码进行混淆,就有出错的可能。因为这里’abcd’作为函数的默认值,PHP要求它必须在编译时就已知。因此,我们必须给混淆器加上一个判断。下面的代码可以部分规避这个问题。

public function enterNode(Node $node)

{

if ($node instanceof Node\Param || $node instanceof Node\Stmt\Static_) {

$this->_inStatic = true;

}

}

public function leaveNode(Node $node)

{

if ($node instanceof Node\Param || $node instanceof Node\Stmt\Static_) {

$this->_inStatic = false;

}

if ($this->_inStatic) {

return;

}

// original code...

}

混淆器是将代码复杂化,因此它必须考虑相当多的边边角角。而解混淆器作为将代码简单化的工具,不需要考虑这种情况。解混淆器考虑的情况则是另外一种。

让我们写一个稍微高阶一些的混淆和解混淆:

$a = true;

$b = false;

$c = 12345;

$d = 'abcdefg';

写成

$array = [true, false, 12345, 'abcdefg'];

$a = $array[0];

$b = $array[1];

$c = $array[2];

$d = $array[3];

可以发现,这种混淆不再是原先的直接替换节点就能解决的混淆了,它引入了一个外部依赖。我们试着写一个混淆器:

use PhpParser\Lexer;

use PhpParser\Node;

use PhpParser\Node\Expr;

use PhpParser\NodeVisitorAbstract;

class ConstantToArray extends NodeVisitorAbstract

{

/**

* @var string

*/

private $_variableName = '';

/**

* @var array

*/

private $_constants = [];

private $_parser;

private $_inStatic = false;

public function __construct($_parser)

{

// 生成一个用于存储数据的变量名,比如AAAAA

$this->_variableName = generate_random_variable(5);

$this->_parser = $_parser;

}

public function afterTraverse(array $nodes)

{

$keys = [];

foreach ($this->_constants as $key => $value) {

$keys[] = unserialize($key);

}

$items = base64_encode(serialize($keys));

// 懒得写一大串了。。。

$nodes = array_merge($this->_parser->parse(

"<?php \${$this->_variableName}=unserialize(base64_decode('$items'));"

), $nodes);

return $nodes;

}

public function enterNode(Node $node)

{

// 在每个函数头部插入global $AAAAA

if ($node instanceof Node\Stmt\Function_) {

$global = new Node\Stmt\Global_([new Expr\Variable($this->_variableName)]);

array_unshift($node->stmts, $global);

}

if ($node instanceof Node\Param || $node instanceof Node\Stmt\Static_) {

$this->_inStatic = true;

}

}

public function leaveNode(Node $node)

{

if ($node instanceof Node\Param || $node instanceof Node\Stmt\Static_) {

$this->_inStatic = false;

}

if ($this->_inStatic) {

return;

}

// 处理字符串、数字等类型

if ($node instanceof Node\Scalar

&& (!$node instanceof Node\Scalar\MagicConst)) {

// 使用serialize是为了解决类型问题,PHP是个神奇的弱类型语言

$name = serialize($node->value);

// _constants是个Map,这样做性能会高一些

if (!isset($this->_constants[$name])) {

// 这里最好事先扫描一遍并编制索引以提升随机性

// count仅供测试用,比较好看

$this->_constants[$name] = count($this->_constants);

}

return new Expr\ArrayDimFetch(

new Expr\Variable($this->_variableName),

Node\Scalar\LNumber::fromString($this->_constants[$name])

);

}

// 处理true, false等类型

if ($node instanceof Node\Expr\ConstFetch && $node->name instanceof Node\Name && count($node->name->parts) === 1) {

$name = $node->name->parts[0];

switch (strtolower($name)) {

case 'true':

$name = true;

break;

case 'false':

$name = false;

break;

case 'null':

$name = null;

break;

default:

return;

}

$name = serialize($name);

if (!isset($this->_constants[$name])) {

$this->_constants[$name] = count($this->_constants);

}

return new Expr\ArrayDimFetch(

new Expr\Variable($this->_variableName),

Node\Scalar\LNumber::fromString($this->_constants[$name])

);

}

}

而解混淆又要怎么写呢?

use PhpParser\Node;

use PhpParser\NodeVisitorAbstract;

class ArrayToConstant extends NodeVisitorAbstract

{

/**

* @var string

*/

private $_variableName = '';

/**

* @var array

*/

private $_constants = [];

public function enterNode(Node $node)

{

if ($node instanceof Node\Expr\Assign &&

$node->expr instanceof Node\Expr\FuncCall &&

$node->expr->name instanceof Node\Name &&

is_string($node->expr->name->parts[0]) &&

$node->expr->name->parts[0] == 'unserialize' &&

count($node->expr->args) === 1 &&

$node->expr->args[0] instanceof Node\Arg &&

$node->expr->args[0]->value instanceof Node\Expr\FuncCall &&

$node->expr->args[0]->value->name instanceof Node\Name &&

is_string($node->expr->args[0]->value->name->parts[0]) &&

$node->expr->args[0]->value->name->parts[0] == 'base64_decode'

) {

$string = $node->expr->args[0]->value->args[0]->value->value;

$array = unserialize(base64_decode($string));

$this->_variableName = $node->var->name;

$this->_constants = $array;

return new Node\Expr\Assign($node->var, Node\Scalar\LNumber::fromString("0"));

}

}

public function leaveNode(Node $node)

{

if ($this->_variableName === '') return;

if (

$node instanceof Node\Expr\ArrayDimFetch &&

$node->var->name === $this->_variableName

) {

$val = $this->_constants[$node->dim->value];

if (is_string($val)) {

return new Node\Scalar\String_($val);

} elseif (is_double($val)) {

return new Node\Scalar\DNumber($val);

} elseif (is_int($val)) {

return new Node\Scalar\LNumber($val);

} else {

return new Node\Expr\ConstFetch(new Node\Name\FullyQualified(json_encode($val)));

}

}

}

}

我们看enterNode这里的大if,这里负责寻找$a = unserialize(base64_decode("string"))这种模式的代码,之后获取其表以及变量名。从上面的寻找逻辑,我们可以推测:

如果代码中有别的符合这个模式的代码,解混淆器就可能会出现错误。

如果代码中的数组赋值是别的模式,就必须重写此部份代码以适配该模式。

总结

对于混淆器而言,你要做的事情包括这些:

拿到尽可能多的PHP样本,寻找各种可能的语法不兼容问题。

基于信息不对称性,努力将混淆器引入的语句与真实的业务代码混为一体。

尽量打乱原始代码结构,能去除的信息(如变量名)尽可能去除。

而对于一个解混淆器而言,就需要:

准确识别出混淆模式及其依赖的外部信息。

需要能准确地提取出各类运行时才可获取的密钥、数据。

一旦信息无法恢复,就需要通过一定的规则还原出近似的信息。

尾声

我本次的混淆比较初级,完全不实用,毕竟连混淆器+解混淆器+写文章也就花了十个小时不到吧,性能低下,且不保证兼容性,仅仅是一个示例,仅供参考。不过我认为这个示例级别的混淆器效果要比绝大多数市面上流通的混淆器效果好得多,那些都是什么垃圾.jpg 一个混淆器要走向实用,你至少也要把控制流给打乱掉,就像 yakpro-po 这样吧。

你可能注意到了,本文中的每一个混淆规则都是一个单独的新类,并没有将不同功能的代码混合在一起;之后通过NodeVisitor::addVisitor在遍历的时候让它们按顺序被调用。这是组合模式这种设计模式的应用,这样的模块化设计非常适合进行后续的维护。

对于解混淆而言,大部分混淆都有一部分混淆规则是相同的,这种设计可以非常容易地就能通过不同规则的重新组合来解出一种新的混淆。而对于混淆而言,还有什么比套娃更有意思的事情呢 :D

混淆 php,开发简单的PHP混淆器与解混淆器相关推荐

  1. DS90UH925QSQX 串行器,解串器 48WQFN 汽车应用

    该DS90UH925Q串行器,与所述结合DS90UH926Q解串器,提供了一种安全的显示中的溶液内汽车内容受保护的数字视频tribution娱乐系统.这意味着芯片的并行RGB视频接口为一对高速串行接口 ...

  2. JS解混淆-AST还原案例

    目录 一.js混淆了解 1.为什么要混淆? 2.常见的混淆模样(ob/sojson/jsfuck/AAencode/jjEncode/eval) 二.AST初步认识 三.解混淆常用的方法 一.js混淆 ...

  3. JVM--类加载器详解

    42. JVM--类加载器详解 ● 类加载器子系统作用: 1. 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识. 2. ClassLoader只负责 ...

  4. max9286 四合一_硅天下吉比特多媒体串行链路(GMSL)解串器MAX9286详细信息_产品参数_价格_联系方式_DAV数字音视工程网...

    MAX9286吉比特多媒体串行链路(GMSL)解串器接收多达四个GMSL串行器的数据,采用50Ω同轴电缆或100Ω屏蔽双绞线(STP)电缆,通过四个CSI-2通道输出数据.每条串行链路具有嵌入式控制通 ...

  5. 好程序员前端分享使用JS开发简单的音乐播放器

    好程序员前端分享使用JS开发简单的音乐播放器,最近,我们在教学生使用JavaScript,今天就带大家开发一款简单的音乐播放器.首先,最终效果如图所示: 首先,我们来编写html界面index.htm ...

  6. Android 混淆其实很简单

    简介 在 Android 打出正式的 APK 安装包时,通常为了增加别人反编译后阅读源代码的难度,需要做一些混淆工作.在 Android Studio 中配合 Gradle 很容易完成这个操作. Pr ...

  7. Shenandoah收集器和ZGC收集器的简单介绍

    3.6 低延迟垃圾收集器 衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint).吞吐量(Throughput)和延迟(Latency),三者共同构成了一个"不可能三角" ...

  8. python_2开发简单爬虫

    2017年12月03日 16:43:01 独行侠的守望 阅读数:204 标签: python爬虫 更多 个人分类: Python 编辑 版权声明:本文为博主原创文章,转载请注明文章链接. https: ...

  9. python简单爬虫程序分析_[Python专题学习]-python开发简单爬虫

    掌握开发轻量级爬虫,这里的案例是不需要登录的静态网页抓取.涉及爬虫简介.简单爬虫架构.URL管理器.网页下载器(urllib2).网页解析器(BeautifulSoup) 一.爬虫简介以及爬虫的技术价 ...

  10. SpringMVC+Spring4+Mybatis3集成,开发简单Web项目+源码下载

    转载自   SpringMVC+Spring4+Mybatis3集成,开发简单Web项目+源码下载 基本准备工作 1.安装JDK1.6以上版本,安装与配置 2.下载mybatis-3.2.0版:htt ...

最新文章

  1. C++中关键字的理解--Static
  2. postman全局变量操作
  3. UPS技术的“前世今生”【基础篇.PPT】
  4. Avaddon勒索解密工具原理解析
  5. 【转载】一文彻底拿下Java异常
  6. [转载]dynamic的小坑--RuntimeBinderException:“object”未包含“xxx”的定义
  7. rabbin负载均衡
  8. 08r2系统服务器开索引,SQLSERVER2008R2正确使用索引
  9. Spring boot + mybatis + oracle代码生成器
  10. Oracle的SQL语法提示30例,INDEX_JOIN,ORDERED,USE_NL,LEADING
  11. c语言累加和校验_累加和校验算法(CheckSum算法)
  12. 关于STVP写保护等级2的问题
  13. linux下ftp客户端主动模式设置
  14. Mybatis异常:Invalid bound statement (not found): com.xxx.mapper.xxxMapper.selectByExample
  15. CentOS7使用mount命令来挂载CDROM
  16. 通过webSocket实现app产生的数据在网页实时显示
  17. 牛客《今日头条》笔试题
  18. 便携设备 android,mini型便携Android通讯设备——与外围硬件沟通桥梁
  19. 还有脸睡?微信小程序与智能硬件终端蓝牙BLE通讯,实现WIFI配网这么简单都不会,周公都得踹醒你!
  20. 搭建Open edX经验总结

热门文章

  1. PHP从入门到能用(十)创建新闻网站数据库
  2. 百人研发团队的难题:研发管理、绩效考核、组织文化和OKR
  3. Java NIO、BIO介绍
  4. linux查看日志相关命令
  5. Redis文档--详解redis
  6. IMF 自定义 IMFTimer、IMFTimerTask
  7. demo VelocityTracker
  8. 基于javaweb+jsp的员工薪资工资管理系统(JavaWeb JSP MySQL Servlet SSM SpringBoot Layui Ajax)
  9. 2018数模国赛A题分析及训练论文
  10. 基于SSM的毕业论文管理系统