解个phpjiami搞这么复杂干什么.jpg
初探PHP-Parser
PHP-Parser
是nikic
用PHP编写的PHP5.2到PHP7.4解析器,其目的是简化静态代码分析和操作。
Parsing
创建一个解析器实例:
use PhpParser\ParserFactory;
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
ParserFactory接收以下几个参数:
ParserFactory::PREFER_PHP7
:优先解析PHP7,如果PHP7解析失败则将脚本解析成PHP5ParserFactory::PREFER_PHP5
:优先解析PHP5,如果PHP5解析失败则将脚本解析成PHP7ParserFactory::ONLY_PHP7
:只解析成PHP7ParserFactory::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\Name
和PhpParser\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
方法类似,唯一的区别是它只在遍历之后调用一次。- 在每个节点上都调用
enterNode
和leaveNode
方法,前者在它被输入时,即在它的子节点被遍历之前,后者在它被离开时。 - 这四个方法要么返回更改的节点,要么根本不返回(即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);