初探PHP-Parser PHP-Parser
是nikic
用PHP编写的PHP5.2到PHP7.4解析器,其目的是简化静态代码分析和操作。
Parsing 创建一个解析器实例:
1 2 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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <?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
1 2 3 4 5 <?php use PhpParser\NodeDumper; $nodeDumper = new NodeDumper; echo $nodeDumper->dump($stmts), "\n";
或者我们使用vendor/bin/php-parse
也是一样的效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 λ 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
格式化代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?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中的所有字符串,并输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <?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中出现的函数以及类中的成员方法
1 2 3 4 5 6 7 8 9 10 11 12 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脚本中函数以及类的成员方法函数名为小写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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个方法:
1 2 3 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混淆的例子,比较简单(郑老板@zsx所说的20分钟内能解密出来的那种),主要是加深一下我们对PhpParser
使用
phpjiami 大部分混淆都会把代码格式搞得很乱,用PhpParser\PrettyPrinter
格式化一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?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);
格式基本能看了
image.png
因为函数和变量的乱码让我们之后的调试比较难受,所以简单替换一下混淆的函数和变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <?php use PhpParser \Error ;use PhpParser \NodeFinder ;use PhpParser \ParserFactory ;require 'vendor/autoload.php' ;$code = file_get_contents("./index_1.php" ); $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); $nodeFinder = new NodeFinder; try { $ast = $parser->parse($code); } catch (Error $error) { echo "Parse error: {$error->getMessage()}\n" ; return ; } $Funcs = $nodeFinder->findInstanceOf($ast, PhpParser\Node\Stmt\Function_::class); $map=[]; $v=0 ; foreach ($Funcs as $func){ $funcname=$func->name->name; if (!isset ($map[$funcname])) { if (!preg_match('/^[a-z0-9A-Z_]+$/' , $funcname)) { $code=str_replace($funcname,"func" .$v,$code); $v++; $map[$funcname]=$v; } } } $v = 0 ; $map = []; $tokens = token_get_all($code); foreach ($tokens as $token) { if ($token[0 ] === T_VARIABLE) { if (!isset ($map[$token[1 ]])) { if (!preg_match('/^\$[a-zA-Z0-9_]+$/' , $token[1 ])) { $code = str_replace($token[1 ], '$v' . $v++, $code); $map[$token[1 ]] = $v; } } } } file_put_contents("index_2.php" ,$code);
变量和函数基本能看了,还是有一些数据是乱码,这个是它自定义函数加密的字符串,大多数都是php内置的函数,我们调试一下就基本能看到了
image.png
但是得注意一下,phpjiami有几个反调试的地方,在35行的地方打个断点
2020-05-09_135436.png
可以看到4个反调试的点:
第一个点:
当你是以cli运行php的时候就会直接die()掉,直接注释掉即可
1 php_sapi_name()=="cli" ? die() : ''
第二个点:
和第一个差不多,也是验证运行环境的,直接注释即可
1 2 3 if (!isset($_SERVER["HTTP_HOST"]) && !isset($_SERVER["SERVER_ADDR"]) && !isset($_SERVER["REMOTE_ADDR"])) { die(); }
第三个点:
如果你在if语句处停留超过100ms的话就会直接die掉,注释即可
1 2 3 4 5 $v46 = microtime(true) * 1000; eval(""); if (microtime(true) * 1000 - $v46 > 100) { die(); }
第四个点:
$51就是整个文件内容,这行是用于加密前的文件对比是否完整,如果不完整则执行$52(),因为$52不存在所以会直接报错退出,而如果对比是完整的话那么就是$53,虽然$53也不存在,但只是抛出一个Warning
,所以我们这里也是直接把这行注释掉。
1 !strpos(func2(substr($v51, func2("???"), func2("???"))), md5(substr($51, func2("??"), func2("???")))) ? $52() : $53;
注释完之后我们在return那里打一个断点,可以发现在return那里我们需要解密的文件内容呈现了出来。
image.png
解密之后的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 ?><?php @eval("//Encode by phpjiami.com,Free user."); ?><?php //Clear the uploads directory every hour highlight_file(__FILE__); $sandbox = "uploads/". md5("De1CTF2020".$_SERVER['REMOTE_ADDR']); @mkdir($sandbox); @chdir($sandbox); if($_POST["submit"]){ if (($_FILES["file"]["size"] < 2048) && Check()){ if ($_FILES["file"]["error"] > 0){ die($_FILES["file"]["error"]); } else{ $filename=md5($_SERVER['REMOTE_ADDR'])."_".$_FILES["file"]["name"]; move_uploaded_file($_FILES["file"]["tmp_name"], $filename); echo "save in:" . $sandbox."/" . $filename; } } else{ echo "Not Allow!"; } } function Check(){ $BlackExts = array("php"); $ext = explode(".", $_FILES["file"]["name"]); $exts = trim(end($ext)); $file_content = file_get_contents($_FILES["file"]["tmp_name"]); if(!preg_match('/[a-z0-9;~^`&|]/is',$file_content) && !in_array($exts, $BlackExts) && !preg_match('/\.\./',$_FILES["file"]["name"])) { return true; } return false; } ?> <html> <head> <meta charset="utf-8"> <title>upload</title> </head> <body> <form action="index.php" method="post" enctype="multipart/form-data"> <input type="file" name="file" id="file"><br> <input type="submit" name="submit" value="submit"> </form> </body> </html><?php
其实phpjiami加密之后的整个脚本就是解密我们文件的脚本,我们的文件内容被加密之后放在了?>
最后面
image.png
整个解密过程也比较简单,其中$v51是我们加密之后内容,$v55是解密后的内容。
1 $v55 = str_rot13(@gzuncompress(func2(substr($v51,-974,$v55))));
其中func2是解密函数
image.png
最后是拿func2解密之后的代码放在这个eval中执行.
image.png
enphp混淆 官网:http://enphp.djunny.com/
github: github
使用官方的加密例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?php include './func_v2.php' ;$options = array ( 'ob_function' => 2 , 'ob_function_length' => 3 , 'ob_call' => 1 , 'insert_mess' => 0 , 'encode_call' => 2 , 'ob_class' => 0 , 'encode_var' => 2 , 'encode_var_length' => 5 , 'encode_str' => 2 , 'encode_str_length' => 3 , 'encode_html' => 2 , 'encode_number' => 1 , 'encode_gz' => 0 , 'new_line' => 1 , 'remove_comment' => 1 , 'debug' => 1 , 'deep' => 1 , 'php' => 7 , ); $file = 'test.php' ; $target_file = 'en_test.php' ; enphp_file($file, $target_file, $options); ?>
加密之后大概长这样子
image.png
可以看到,我们的大部分字符串、函数等等都被替换成了类似于$GLOBALS{乱码}[num]
这种形式,我们将其输出看一下:
image.png
可以看到我们原本的脚本中的字符串都在此数组里面,所以我们只要将$GLOBALS{乱码}[num]
还原成原来对应的字符串即可。
那么我们如何获取$GLOBALS{乱码}
数组的内容,很简单,在我们获取AST节点处打断点即可找到相关内容:
image.png
1 2 3 4 $split=$ast[2]->expr->expr->args[0]->value->value; $all=$ast[2]->expr->expr->args[1]->value->value; $str=explode($split,$all); var_dump($str);
可以看到,和上面输出的是一样的(如果加密等级不一样则还需要加一层gzinflate
)
image.png
然后就是通过AST一个节点一个节点将其替换即可,如果不知道节点类型的同学可以用$GLOBALS[A][1]
,将其输出出来看一下即可,然后根据节点的类型和数据进行判断即可,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class PhpParser\Node\Expr\ArrayDimFetch#1104 (3) { public $var => class PhpParser\Node\Expr\ArrayDimFetch#1102 (3) { public $var => class PhpParser\Node\Expr\Variable#1099 (2) { public $name => string(7) "GLOBALS" protected $attributes => array(2) { ... } } public $dim => class PhpParser\Node\Expr\ConstFetch#1101 (2) { public $name => class PhpParser\Node\Name#1100 (2) { ... } protected $attributes => array(2) { ... } } protected $attributes => array(2) { 'startLine' => int(2) 'endLine' => int(2) } } public $dim => class PhpParser\Node\Scalar\LNumber#1103 (2) { public $value => int(1) protected $attributes => array(3) { 'startLine' => int(2) 'endLine' => int(2) 'kind' => int(10) } } protected $attributes => array(2) { 'startLine' => int(2) 'endLine' => int(2) } }
根据上面的节点编写脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public function leaveNode (Node $node) { if ($node instanceof PhpParser\Node\Expr\ArrayDimFetch && $node->var instanceof PhpParser\Node\Expr\ArrayDimFetch && $node->var->var instanceof PhpParser\Node\Expr\Variable && $node->var->var->name==="GLOBALS" && $node->var->dim instanceof PhpParser\Node\Expr\ConstFetch && $node->var->dim->name instanceof PhpParser\Node\Name && $node->var->dim->name->parts[0 ]===$this ->str && $node->dim instanceof PhpParser\Node\Scalar\LNumber ){ return new PhpParser\Node\Scalar\String_($this ->str_arr[$node->dim->value]); } return null ; }
解出来的内容如下,可以看到大部分已经成功解密出来了
image.png
还有就是解密的一部分出现这样语句:('highlight_file')(__FILE__);
,很明显不符合我们平时的写法,将其节点重命名一下
1 2 3 4 5 6 if (($node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\MethodCall) && $node->name instanceof Node\Scalar\String_) { $node->name = new Node\Name($node->name->value); }
现在看起来就舒服多了
image.png
我们分析剩下乱码的部分
image.png
可以看到是函数里面的局部变量还是乱码,从第一句可以看出所有的局部变量都是以& $GLOBALS[乱码]
为基础的,而& $GLOBALS[乱码]
是我们上面已经找出来的,所以也是将其替换即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if ($node instanceof \PhpParser\Node\Stmt\Expression && $node->expr instanceof \PhpParser\Node\Expr\AssignRef && $node->expr->var instanceof \PhpParser\Node\Expr\Variable && $node->expr->expr instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->expr->expr->var instanceof \PhpParser\Node\Expr\Variable && $node->expr->expr->var->name==="GLOBALS" && $node->expr->expr->dim instanceof \PhpParser\Node\Expr\ConstFetch && $node->expr->expr->dim->name instanceof \PhpParser\Node\Name && $node->expr->expr->dim->name->parts!=[] ){ $this->Localvar=$node->expr->var->name; return NodeTraverser::REMOVE_NODE; }else if ($node instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->var instanceof \PhpParser\Node\Expr\Variable && $node->var->name===$this->Localvar && $node->dim instanceof \PhpParser\Node\Scalar\LNumber ){ return new \PhpParser\Node\Scalar\String_($this->str_arr[$node->dim->value]); }
替换之后,可以看到与& $GLOBALS[乱码]
有关的已经全部被替换了,只有变量部分是乱码了
image.png
替换变量为$v
这种形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function BeautifyVariables($code){ $v = 0; $map = []; $tokens = token_get_all($code); foreach ($tokens as $token) { if ($token[0] === T_VARIABLE) { if (!isset($map[$token[1]])) { if (!preg_match('/^\$[a-zA-Z0-9_]+$/', $token[1])) { $code = str_replace($token[1], '$v' . $v++, $code); $map[$token[1]] = $v; } } } } return $code; }
至此所有代码全部被还原(除了变量名这种不可抗拒因素之外)
image.png
还有一部分是没有用的全局变量和常量,手动或者根据AST去进行删除即可,下面贴一下完整解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 <?php require "./vendor/autoload.php" ;use \PhpParser \Error ;use \PhpParser \ParserFactory ;use \PhpParser \NodeTraverser ;use \PhpParser \NodeVisitorAbstract ;use \PhpParser \Node ;use \PhpParser \PrettyPrinter \Standard ;class MyVisitor extends NodeVisitorAbstract { public $str; public $str_arr; public $Localvar; public function leaveNode (Node $node) { if ($node instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->var instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->var->var instanceof \PhpParser\Node\Expr\Variable && $node->var->var->name==="GLOBALS" && $node->var->dim instanceof \PhpParser\Node\Expr\ConstFetch && $node->var->dim->name instanceof \PhpParser\Node\Name && $node->var->dim->name->parts[0 ]===$this ->str && $node->dim instanceof \PhpParser\Node\Scalar\LNumber ){ return new \PhpParser\Node\Scalar\String_($this ->str_arr[$node->dim->value]); } if (($node instanceof Node\Expr\FuncCall || $node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\MethodCall) && $node->name instanceof Node\Scalar\String_) { $node->name = new Node\Name($node->name->value); } if ($node instanceof \PhpParser\Node\Stmt\Expression && $node->expr instanceof \PhpParser\Node\Expr\AssignRef && $node->expr->var instanceof \PhpParser\Node\Expr\Variable && $node->expr->expr instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->expr->expr->var instanceof \PhpParser\Node\Expr\Variable && $node->expr->expr->var->name==="GLOBALS" && $node->expr->expr->dim instanceof \PhpParser\Node\Expr\ConstFetch && $node->expr->expr->dim->name instanceof \PhpParser\Node\Name && $node->expr->expr->dim->name->parts!=[] ){ $this ->Localvar=$node->expr->var->name; return NodeTraverser::REMOVE_NODE; }else if ($node instanceof \PhpParser\Node\Expr\ArrayDimFetch && $node->var instanceof \PhpParser\Node\Expr\Variable && $node->var->name===$this ->Localvar && $node->dim instanceof \PhpParser\Node\Scalar\LNumber ){ return new \PhpParser\Node\Scalar\String_($this ->str_arr[$node->dim->value]); } return null ; } } function BeautifyVariables ($code) { $v = 0 ; $map = []; $tokens = token_get_all($code); foreach ($tokens as $token) { if ($token[0 ] === T_VARIABLE) { if (!isset ($map[$token[1 ]])) { if (!preg_match('/^\$[a-zA-Z0-9_]+$/' , $token[1 ])) { $code = str_replace($token[1 ], '$v' . $v++, $code); $map[$token[1 ]] = $v; } } } } return $code; } $code = file_get_contents("./en_test.php" ); $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); try { $ast = $parser->parse($code); } catch (Error $error) { echo "Parse error: {$error->getMessage()}\n" ; return ; } var_dump($ast); $split=$ast[2 ]->expr->expr->args[0 ]->value->value; $all=$ast[2 ]->expr->expr->args[1 ]->value->value; $str1=$ast[2 ]->expr->var->dim->name->parts[0 ]; $str_arr=explode($split,$all); $visitor=new MyVisitor; $visitor->str=$str1; $visitor->str_arr=$str_arr; $traverser = New NodeTraverser; $traverser->addVisitor($visitor); $stmts = $traverser->traverse($ast); $prettyPrinter = new Standard; $code=$prettyPrinter->prettyPrintFile($stmts); $code=BeautifyVariables($code); echo $code;
注:需要注意的是enphp使用的全局变量不一定是GLOBALS,也可能是_SERVER、_GET等等,根据具体情况进行判断,还有就是加密等级不同对应的解密方式也会不同的,不过其中的思想都是大同小异
Reference https://www.52pojie.cn/thread-693641-1-1.html
https://www.52pojie.cn/thread-883976-1-1.html
https://github.com/nikic/PHP-Parser/blob/master/doc/2_Usage_of_basic_components.markdown
最后更新时间:2020-08-12 09:34:34
know it then hack it