初探PHP-Parser

PHP-Parsernikic用PHP编写的PHP5.2到PHP7.4解析器,其目的是简化静态代码分析和操作。

Parsing

创建一个解析器实例:

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

ParserFactory接收以下几个参数:

  • ParserFactory::PREFER_PHP7:优先解析PHP7,如果PHP7解析失败则将脚本解析成PHP5
  • ParserFactory::PREFER_PHP5:优先解析PHP5,如果PHP5解析失败则将脚本解析成PHP7
  • ParserFactory::ONLY_PHP7:只解析成PHP7
  • ParserFactory::ONLY_PHP5:只解析成PHP5

将PHP脚本解析成抽象语法树(AST)

<?php
use PhpParser\Error;
use PhpParser\ParserFactory;

require 'vendor/autoload.php';

$code = file_get_contents("./test.php");

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

try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
}
var_dump($ast);
?>

Node dumping

如果是用上面的var_dump的话显示的AST可能会比较乱,那么我们可以使用NodeDumper生成一个更加直观的AST

<?php
use PhpParser\NodeDumper;

$nodeDumper = new NodeDumper;
echo $nodeDumper->dump($stmts), "\n";

或者我们使用vendor/bin/php-parse也是一样的效果

λ vendor/bin/php-parse test.php       
====> File test.php:                  
==> Node dump:                        
array(                                
    0: Stmt_Expression(               
        expr: Expr_Assign(            
            var: Expr_Variable(       
                name: a               
            )                         
            expr: Scalar_LNumber(     
                value: 1              
            )                         
        )                             
    )                                 
)

Node tree structure

PHP是一个成熟的脚本语言,它大约有140个不同的节点。但是为了方便使用,将他们分为三类:

  • PhpParser\Node\Stmts是语句节点,即不返回值且不能出现在表达式中的语言构造。例如,类定义是一个语句,它不返回值,你不能编写类似func(class {})的语句。
  • PhpParser\Node\expr是表达式节点,即返回值的语言构造,因此可以出现在其他表达式中。如:$var (PhpParser\Node\Expr\Variable)func() (PhpParser\Node\Expr\FuncCall)
  • PhpParser\Node\Scalars是表示标量值的节点,如"string" (PhpParser\Node\scalar\string)0 (PhpParser\Node\scalar\LNumber) 或魔术常量,如"FILE" (PhpParser\Node\scalar\MagicConst\FILE) 。所有PhpParser\Node\scalar都是延伸自PhpParser\Node\Expr,因为scalar也是表达式。

  • 需要注意的是PhpParser\Node\NamePhpParser\Node\Arg不在以上的节点之中

Pretty printer

使用PhpParser\PrettyPrinter格式化代码

<?php
use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
require 'vendor/autoload.php';
$code = file_get_contents('./index.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}
$prettyPrinter = new PrettyPrinter\Standard;
$prettyCode = $prettyPrinter->prettyPrintFile($ast);
echo $prettyCode;

Node traversation

使用PhpParser\NodeTraverser我们可以遍历每一个节点,举几个简单的例子:解析php中的所有字符串,并输出

<?php
use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Node;

require 'vendor/autoload.php';

class MyVisitor extends NodeVisitorAbstract{
    public function leaveNode(Node $node)
    {
        if ($node instanceof Node\Scalar\String_)
        {
            echo $node -> value,"\n";
        }

    }
}

$code = file_get_contents("./test.php");

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

$traverser->addVisitor(new MyVisitor);

try {
    $ast = $parser->parse($code);
    $stmts = $traverser->traverse($ast);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}

?>

遍历php中出现的函数以及类中的成员方法

class MyVisitor extends NodeVisitorAbstract{
    public function leaveNode(Node $node)
    {
        if( $node instanceof Node\Expr\FuncCall
            || $node instanceof Node\Stmt\ClassMethod
            || $node instanceof Node\Stmt\Function_
            || $node instanceof Node\Expr\MethodCall
        ) {
            echo $node->name,"\n";
        }
    }
}

替换php脚本中函数以及类的成员方法函数名为小写

class MyVisitor extends NodeVisitorAbstract{
    public function leaveNode(Node $node)
    {
        if( $node instanceof Node\Expr\FuncCall) {
            $node->name->parts[0]=strtolower($node->name->parts[0]);
        }elseif($node instanceof Node\Stmt\ClassMethod){
            $node->name->name=strtolower($node->name->name);
        }elseif ($node instanceof Node\Stmt\Function_){
            $node->name->name=strtolower($node->name->name);
        }elseif($node instanceof Node\Expr\MethodCall){
            $node->name->name=strtolower($node->name->name);
        }
    }
}

需要注意的是所有的visitors都必须实现PhpParser\NodeVisitor接口,该接口定义了如下4个方法:

public function beforeTraverse(array $nodes);
public function enterNode(\PhpParser\Node $node);
public function leaveNode(\PhpParser\Node $node);
public function afterTraverse(array $nodes);
  • beforeTraverse方法在遍历开始之前调用一次,并将其传递给调用遍历器的节点。此方法可用于在遍历之前重置值或准备遍历树。
  • afterTraverse方法与beforeTraverse方法类似,唯一的区别是它只在遍历之后调用一次。
  • 在每个节点上都调用enterNodeleaveNode方法,前者在它被输入时,即在它的子节点被遍历之前,后者在它被离开时。
  • 这四个方法要么返回更改的节点,要么根本不返回(即null),在这种情况下,当前节点不更改。

other

其余的知识点可以参考官方的,这里就不多赘述了。

Documentation for version 4.x (stable; for running on PHP >= 7.0; for parsing PHP 5.2 to PHP 7.4).

Documentation for version 3.x (unsupported; for running on PHP >= 5.5; for parsing PHP 5.2 to PHP 7.2).

PHP代码混淆

下面举两个php混淆的例子,比较简单(郑老板@zsx所说的20分钟内能解密出来的那种),主要是加深一下我们对PhpParser使用

phpjiami

大部分混淆都会把代码格式搞得很乱,用PhpParser\PrettyPrinter格式化一下代码

<?php
use PhpParser\Error;
use PhpParser\ParserFactory;
use PhpParser\PrettyPrinter;
require 'vendor/autoload.php';
$code = file_get_contents('./test.php');
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}
$prettyPrinter = new PrettyPrinter\Standard;
$prettyCode = $prettyPrinter->prettyPrintFile($ast);
file_put_contents('en_test.php', $prettyCode);
点击收藏 | 1 关注 | 2
登录 后跟帖