Eswlnk Blog Eswlnk Blog
  • 资源
    • 精彩视频
    • 破解专区
      • WHMCS
      • WordPress主题
      • WordPress插件
    • 其他分享
    • 极惠VPS
    • PDF资源
  • 关于我
    • 论文阅读
    • 关于本站
    • 通知
    • 左邻右舍
    • 玩物志趣
    • 日志
    • 专题
  • 热议话题
    • 游戏资讯
  • 红黑
    • 渗透分析
    • 攻防对抗
    • 代码发布
  • 自主研发
    • 知识库
    • 插件
      • ToolBox
      • HotSpot AI 热点创作
    • 区块
    • 快乐屋
    • 卡密
  • 乱步
    • 文章榜单
    • 热门标签
  • 问答中心反馈
  • 注册
  • 登录
首页 › 攻防对抗 › 开发简单的PHP混淆器与解混淆器

开发简单的PHP混淆器与解混淆器

Eswlnk的头像
Eswlnk
2023-06-02 14:38:47
开发简单的PHP混淆器与解混淆器-Eswlnk Blog
智能摘要 AI
文章主要讨论了PHP代码混淆与解混淆的技术细节及其实现方法。作者因处理多个被混淆的PHP代码样本,探讨了如何编写通用解混淆代码,并指出简单混淆的意义有限。文章详细介绍了使用`php-parser`库解析PHP代码为抽象语法树(AST),并通过遍历AST实现变量重命名、字符串混淆等功能。混淆器的核心步骤包括解析代码、修改节点和还原代码,而解混淆器则是反向操作。作者还强调了混淆器需处理语法兼容性和信息隐藏,解混淆器则需精确识别混淆模式并恢复数据。最后,作者提到模块化设计的重要性,并指出高级混淆器应具备更复杂的控制流打乱能力。

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

开发简单的PHP混淆器与解混淆器-Eswlnk Blog

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

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),我们就将基于它来开发。

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

  1. 把原始代码解析成一棵树。
  2. 遍历树,修改树上的某些节点。
  3. 将树还原成代码。

练手

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

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

Hello World!

替换成

<?php echo 'Hello World!';

我们先写一个主体结构:

<?php
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;

再写一个游客类:

<?php
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];

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

<?php
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])
            );
    }
}

而解混淆又要怎么写呢?

<?php
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)));
            }
        }
    }

}

PHPCopy

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

  1. 如果代码中有别的符合这个模式的代码,解混淆器就可能会出现错误。
  2. 如果代码中的数组赋值是别的模式,就必须重写此部份代码以适配该模式。

总结

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

  1. 拿到尽可能多的PHP样本,寻找各种可能的语法不兼容问题。
  2. 基于信息不对称性,努力将混淆器引入的语句与真实的业务代码混为一体。
  3. 尽量打乱原始代码结构,能去除的信息(如变量名)尽可能去除。

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

  1. 准确识别出混淆模式及其依赖的外部信息。
  2. 需要能准确地提取出各类运行时才可获取的密钥、数据。
  3. 一旦信息无法恢复,就需要通过一定的规则还原出近似的信息。

尾声

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

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

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

本站默认网盘访问密码:1166
本站默认网盘访问密码:1166
声明:本站原创文章文字版权归本站所有,转载务必注明作者和出处;本站转载文章仅仅代表原作者观点,不代表本站立场,图文版权归原作者所有。如有侵权,请联系我们删除。
PHP加密混淆
1
0
Eswlnk的头像
Eswlnk
一个有点倒霉的研究牲站长
赞赏
Linux下基于lkm的inline hook学习
上一篇
PentestGPT | GPT 授权的渗透测试工具
下一篇

评论 (0)

请登录以参与评论
现在登录
    发表评论

猜你喜欢

  • 今日热点:伪Clash软件下载陷阱曝光,附防范建议
  • 「攻防对抗」利用 fastjson 原生反序列化与动态代理突破安全限制
  • 「攻防对抗」从上传漏洞到Getshell | 一次完整的渗透过程
  • 「日志记录」从零起步揭开路由器漏洞挖掘的面纱
  • 「攻防对抗」NSmartProxy流量特征的真实表现与应用
Eswlnk的头像

Eswlnk

一个有点倒霉的研究牲站长
1108
文章
319
评论
679
获赞

随便看看

有关xss漏洞攻击与防御的那些事儿
2022-06-15 17:27:08
体验新版本360网络空间测绘|因为看见,所以安全
2022-02-01 22:50:57
「安全通知」:宝塔面板又爆出新漏洞,某网5btc出售
2020-12-28 13:45:54

文章目录

专题展示

WordPress53

工程实践37

热门标签

360 AI API CDN java linux Nginx PDF PHP python SEO Windows WordPress 云服务器 云服务器知识 代码 免费 安全 安卓 工具 开发日志 微信 微软 手机 插件 攻防 攻防对抗 教程 日志 渗透分析 源码 漏洞 电脑 破解 系统 编程 网站优化 网络 网络安全 脚本 苹果 谷歌 软件 运维 逆向
  • 首页
  • 知识库
  • 地图
Copyright © 2023-2025 Eswlnk Blog. Designed by XiaoWu.
本站CDN由 壹盾安全 提供高防CDN安全防护服务
蜀ICP备20002650号-10
页面生成用时 0.800 秒   |  SQL查询 27 次
本站勉强运行:
友情链接: Eswlnk Blog 网站渗透 倦意博客 特资啦!个人资源分享站 祭夜博客 iBAAO壹宝头条
  • WordPress142
  • 网络安全64
  • 漏洞52
  • 软件52
  • 安全48
现在登录
  • 资源
    • 精彩视频
    • 破解专区
      • WHMCS
      • WordPress主题
      • WordPress插件
    • 其他分享
    • 极惠VPS
    • PDF资源
  • 关于我
    • 论文阅读
    • 关于本站
    • 通知
    • 左邻右舍
    • 玩物志趣
    • 日志
    • 专题
  • 热议话题
    • 游戏资讯
  • 红黑
    • 渗透分析
    • 攻防对抗
    • 代码发布
  • 自主研发
    • 知识库
    • 插件
      • ToolBox
      • HotSpot AI 热点创作
    • 区块
    • 快乐屋
    • 卡密
  • 乱步
    • 文章榜单
    • 热门标签
  • 问答中心反馈