├── .gitignore ├── src ├── Exceptions │ ├── BadValueException.php │ ├── UnknownValueException.php │ └── MutableValueException.php ├── AttrName.php ├── Reducer.php ├── VarRef.php ├── Reducer │ ├── FuncCallReducer │ │ ├── FunctionReducer.php │ │ ├── FunctionSandbox.php │ │ ├── PassThrough.php │ │ ├── FileSystemCall.php │ │ └── MiscFunctions.php │ ├── AbstractReducer.php │ ├── MiscReducer.php │ ├── MagicReducer.php │ ├── FuncCallReducer.php │ ├── EvalReducer.php │ ├── UnaryReducer.php │ └── BinaryOpReducer.php ├── ValRef │ ├── UnknownValRef.php │ ├── GlobalVarArray.php │ ├── ScalarValue.php │ ├── ResourceValue.php │ ├── ObjectVal.php │ ├── AbstractValRef.php │ ├── ArrayVal.php │ └── ByReference.php ├── ValRef.php ├── AddOriginalVisitor.php ├── EvalBlock.php ├── ExtendedPrettyPrinter.php ├── VarRef │ ├── LiteralName.php │ ├── UnknownVarRef.php │ ├── ArrayAccessVariable.php │ ├── PropertyAccessVariable.php │ ├── ListVarRef.php │ └── FutureVarRef.php ├── MaybeStmtArray.php ├── ReducerVisitor.php ├── Scope.php ├── Utils.php ├── ResolveValueVisitor.php ├── Deobfuscator.php ├── MetadataVisitor.php ├── ControlFlowVisitor.php └── Resolver.php ├── Dockerfile ├── composer.json ├── LICENSE ├── index.php ├── tests ├── filesystem.txt ├── goto-tests.txt ├── reducers.txt └── variables.txt ├── test.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /vendor/ 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /src/Exceptions/BadValueException.php: -------------------------------------------------------------------------------- 1 | deobfusator = $deobfusator; 14 | } 15 | 16 | public function enterNode(Node $node) 17 | { 18 | if (!($node instanceof Node\Scalar\EncapsedStringPart)) { 19 | $node->setAttribute('comments', array(new \PhpParser\Comment('/* ' . $this->deobfusator->prettyPrint(array($node), false) . ' */'))); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/EvalBlock.php: -------------------------------------------------------------------------------- 1 | stmts = $stmts; 18 | $this->origStmts = $origStmts; 19 | } 20 | 21 | public function getSubNodeNames() : array 22 | { 23 | return array('stmts'); 24 | } 25 | 26 | public function getType() : string 27 | { 28 | return 'Expr_EvalBlock'; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ExtendedPrettyPrinter.php: -------------------------------------------------------------------------------- 1 | pStmts($block->stmts) . $this->nl . "}"; 12 | } 13 | 14 | // Escape all non-printable characters 15 | // The parent printer already handles the 00-1F range 16 | protected function escapeString($string, $quote) { 17 | return preg_replace_callback('/([\x7F\x80-\xFF])/', function ($matches) { 18 | return '\\x' . bin2hex($matches[1]); 19 | }, parent::escapeString($string, $quote)); 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/Reducer/FuncCallReducer/FunctionSandbox.php: -------------------------------------------------------------------------------- 1 | name = $name; 16 | } 17 | 18 | public function getValue(Scope $scope) 19 | { 20 | return $scope->getVariable($this->name); 21 | } 22 | 23 | public function assignValue(Scope $scope, ValRef $valRef) 24 | { 25 | $scope->setVariable($this->name, $valRef); 26 | return true; 27 | } 28 | 29 | public function unsetVar(Scope $scope) 30 | { 31 | $scope->unsetVariable($this->name); 32 | } 33 | 34 | public function __toString() 35 | { 36 | return "Var{{$this->name}}"; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/ValRef/GlobalVarArray.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 16 | } 17 | 18 | protected function &backingArray() 19 | { 20 | return $this->resolver->getGlobalScope()->getVariables(); 21 | } 22 | 23 | public function arrayFetch($dim) 24 | { 25 | $this->checkMutable(); 26 | return $this->resolver->getGlobalScope()->getVariable($dim); 27 | } 28 | 29 | public function arrayAssign($dim, ValRef $valRef) 30 | { 31 | $this->resolver->getGlobalScope()->setVariable($dim, $valRef); 32 | } 33 | 34 | public function arrayUnset($dim) 35 | { 36 | $this->resolver->getGlobalScope()->unsetVariable($dim); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/MaybeStmtArray.php: -------------------------------------------------------------------------------- 1 | stmts = $stmts; 26 | $this->expr = $expr; 27 | } 28 | 29 | public function getSubNodeNames() : array 30 | { 31 | throw new \LogicException("Not a real node"); 32 | } 33 | 34 | public function getType() : string 35 | { 36 | throw new \LogicException("Not a real node"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon816 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ValRef/ScalarValue.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public function __toString() 20 | { 21 | return "Val{{$this->value}}"; 22 | } 23 | 24 | protected function getValueImpl() 25 | { 26 | return $this->value; 27 | } 28 | 29 | public function arrayFetch($dim) 30 | { 31 | $val = $this->getValue(); 32 | if (isset($val[$dim])) { 33 | return new ScalarValue($val[$dim]); 34 | } 35 | return new ScalarValue(null); 36 | } 37 | 38 | public function arrayAssign($dim, ValRef $valRef) 39 | { 40 | if ($dim === null) { 41 | $this->value[] = $valRef->getValue(); 42 | } else { 43 | $this->value[$dim] = $valRef->getValue(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Reducer/FuncCallReducer/PassThrough.php: -------------------------------------------------------------------------------- 1 | filename = $filename; 16 | $this->resource = $resource; 17 | } 18 | 19 | public function isMutable() 20 | { 21 | return true; 22 | } 23 | 24 | protected function getValueImpl() 25 | { 26 | // Do nothing 27 | } 28 | 29 | public function __toString() 30 | { 31 | return "resource{{$this->filename}}"; 32 | } 33 | 34 | public function getFilename() 35 | { 36 | return $this->filename; 37 | } 38 | 39 | public function close() 40 | { 41 | $this->isClosed = true; 42 | } 43 | 44 | public function isClosed() 45 | { 46 | return $this->isClosed; 47 | } 48 | 49 | public function getResource() 50 | { 51 | if ($this->isClosed) { 52 | throw new \LogicException("Tried to use closed resource: {$this->filename}"); 53 | } 54 | return $this->resource; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/VarRef/UnknownVarRef.php: -------------------------------------------------------------------------------- 1 | context = $parentContext; 20 | $this->notAVarRef = $notAVarRef; 21 | } 22 | 23 | public function notAVarRef() 24 | { 25 | return $this->notAVarRef; 26 | } 27 | 28 | public function getValue(Scope $scope) 29 | { 30 | return null; 31 | } 32 | 33 | public function assignValue(Scope $scope, ValRef $valRef) 34 | { 35 | return false; 36 | } 37 | 38 | public function unsetVar(Scope $scope) 39 | { 40 | } 41 | 42 | public function __toString() 43 | { 44 | return "Unknown{{$this->context}}"; 45 | } 46 | 47 | public function getContext() 48 | { 49 | return $this->context; 50 | } 51 | } 52 | UnknownVarRef::$ANY = new UnknownVarRef(null); 53 | UnknownVarRef::$NOT_A_VAR_REF = new UnknownVarRef(null, true); 54 | -------------------------------------------------------------------------------- /src/ValRef/ObjectVal.php: -------------------------------------------------------------------------------- 1 | propArr as $name => $ref) { 15 | $value->$name = $ref->getValue(); 16 | } 17 | return $value; 18 | } 19 | 20 | public function propertyFetch($name) 21 | { 22 | if (isset($this->propArr[$name])) { 23 | return $this->propArr[$name]; 24 | } 25 | return null; 26 | } 27 | 28 | public function propertyAssign($name, ValRef $valRef) 29 | { 30 | $this->propArr[$name] = $valRef; 31 | } 32 | 33 | public function propertyUnset($name) 34 | { 35 | unset($this->propArr[$name]); 36 | } 37 | 38 | public function __toString() 39 | { 40 | $arr = $this->propArr; 41 | return 'Object(' . implode(', ', array_map(function ($key) use (&$arr) { 42 | return "$key => " . $arr[$key]; 43 | }, array_keys($arr))) . ')'; 44 | } 45 | 46 | public function __clone() 47 | { 48 | foreach ($this->propArr as $name => &$ref) { 49 | $ref = clone $ref; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ValRef/AbstractValRef.php: -------------------------------------------------------------------------------- 1 | isMutable = $mutable; 15 | } 16 | 17 | public function isMutable() 18 | { 19 | return $this->isMutable; 20 | } 21 | 22 | protected function checkMutable() 23 | { 24 | if ($this->isMutable()) { 25 | throw new Exceptions\MutableValueException($this); 26 | } 27 | } 28 | 29 | public function getValue() 30 | { 31 | $this->checkMutable(); 32 | return $this->getValueImpl(); 33 | } 34 | 35 | protected abstract function getValueImpl(); 36 | 37 | public function arrayFetch($dim) 38 | { 39 | return null; 40 | } 41 | 42 | public function arrayAssign($dim, ValRef $valRef) 43 | { 44 | } 45 | 46 | public function arrayUnset($dim) 47 | { 48 | } 49 | 50 | public function propertyFetch($name) 51 | { 52 | return null; 53 | } 54 | 55 | public function propertyAssign($name, ValRef $valRef) 56 | { 57 | } 58 | 59 | public function propertyUnset($name) 60 | { 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/VarRef/ArrayAccessVariable.php: -------------------------------------------------------------------------------- 1 | arr = $array; 17 | $this->dim = $dim; 18 | } 19 | 20 | public function getValue(Scope $scope) 21 | { 22 | $arrVal = $this->arr->getValue($scope); 23 | if ($arrVal !== null && !$arrVal->isMutable()) { 24 | return $arrVal->arrayFetch($this->dim); 25 | } 26 | return null; 27 | } 28 | 29 | public function assignValue(Scope $scope, ValRef $valRef) 30 | { 31 | $arrVal = $this->arr->getValue($scope); 32 | if ($arrVal !== null) { 33 | $arrVal->arrayAssign($this->dim, $valRef); 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | public function unsetVar(Scope $scope) 40 | { 41 | $arrVal = $this->arr->getValue($scope); 42 | if ($arrVal !== null) { 43 | $arrVal->arrayUnset($this->dim); 44 | } 45 | } 46 | 47 | public function __toString() 48 | { 49 | return "{$this->arr}[{$this->dim}]"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/VarRef/PropertyAccessVariable.php: -------------------------------------------------------------------------------- 1 | object = $object; 17 | $this->name = $propName; 18 | } 19 | 20 | public function getValue(Scope $scope) 21 | { 22 | $objVal = $this->object->getValue($scope); 23 | if ($objVal !== null && !$objVal->isMutable()) { 24 | return $objVal->propertyFetch($this->name); 25 | } 26 | return null; 27 | } 28 | 29 | public function assignValue(Scope $scope, ValRef $valRef) 30 | { 31 | $objVal = $this->object->getValue($scope); 32 | if ($objVal !== null) { 33 | $objVal->propertyAssign($this->name, $valRef); 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | public function unsetVar(Scope $scope) 40 | { 41 | $objVal = $this->object->getValue($scope); 42 | if ($objVal !== null) { 43 | $objVal->propertyUnset($this->name); 44 | } 45 | } 46 | 47 | public function __toString() 48 | { 49 | return "{$this->object}->{{$this->name}}"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/VarRef/ListVarRef.php: -------------------------------------------------------------------------------- 1 | vars = $vars; 15 | } 16 | 17 | public function getValue(Scope $scope) 18 | { 19 | return null; // Cannot get value of list expression 20 | } 21 | 22 | public function assignValue(Scope $scope, ValRef $valRef) 23 | { 24 | if (!($valRef instanceof ArrayVal)) { 25 | return false; 26 | } 27 | $didAssignAll = true; 28 | for ($i = count($this->vars) - 1; $i >=0; $i--) { 29 | $var = $this->vars[$i]; 30 | if ($var === null) { 31 | continue; 32 | } 33 | $val = $valRef->arrayFetch($i); 34 | if ($val === null) { 35 | continue; 36 | } 37 | $didAssignAll = $var->assignValue($scope, $val) && $didAssignAll; 38 | } 39 | return $didAssignAll; 40 | } 41 | 42 | public function unsetVar(Scope $scope) 43 | { 44 | } 45 | 46 | public function getVars() 47 | { 48 | return $this->vars; 49 | } 50 | 51 | public function __toString() 52 | { 53 | return "List(" . implode(', ', $this->vars) . ")"; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/VarRef/FutureVarRef.php: -------------------------------------------------------------------------------- 1 | expr = $expr; 28 | $this->resolver = $resolver; 29 | } 30 | 31 | public function getValue(Scope $scope) 32 | { 33 | return $this->tryResolve()->getValue($scope); 34 | } 35 | 36 | public function assignValue(Scope $scope, ValRef $valRef) 37 | { 38 | return $this->tryResolve()->assignValue($scope, $valRef); 39 | } 40 | 41 | public function unsetVar(Scope $scope) 42 | { 43 | $this->tryResolve()->unsetVar($scope); 44 | } 45 | 46 | public function __toString() 47 | { 48 | return $this->tryResolve()->__toString(); 49 | } 50 | 51 | private function tryResolve() 52 | { 53 | return $this->resolver->resolveVariable($this->expr, true); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Reducer/AbstractReducer.php: -------------------------------------------------------------------------------- 1 | methodMapping === null) { 16 | $this->methodMapping = array(); 17 | $class = new \ReflectionClass($this); 18 | foreach ($class->getMethods() as $method) { 19 | if (strncmp($method->name, 'reduce', 6) === 0 && $method->class != self::class) { 20 | if ($method->getNumberOfParameters() !== 1) { 21 | throw new \LogicException("Number of parameters is not 1"); 22 | } 23 | $param = $method->getParameters()[0]; 24 | $type = $param->getClass(); 25 | if (!$type->implementsInterface(Node::class)) { 26 | throw new \LogicException("Parameter must be instance of Node"); 27 | } 28 | if (isset($this->methodMapping[$type->name])) { 29 | throw new \LogicException("Parameter type already mapped. {$type->name} to {$this->methodMapping[$type->name]}, attempted: {$method->name}"); 30 | } 31 | $this->methodMapping[$type->name] = $method->name; 32 | } 33 | } 34 | } 35 | return array_keys($this->methodMapping); 36 | } 37 | 38 | public function reduce(Node $node) 39 | { 40 | return $this->{$this->methodMapping[get_class($node)]}($node); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/ValRef/ArrayVal.php: -------------------------------------------------------------------------------- 1 | backingArray = $items; 14 | } 15 | 16 | protected function &backingArray() 17 | { 18 | return $this->backingArray; 19 | } 20 | 21 | protected function getValueImpl() 22 | { 23 | $value = array(); 24 | foreach ($this->backingArray() as $name => $ref) { 25 | $value[$name] = $ref->getValue(); 26 | } 27 | return $value; 28 | } 29 | 30 | public function arrayFetch($dim) 31 | { 32 | $this->checkMutable(); 33 | if (!isset($this->backingArray()[$dim])) { 34 | return null; 35 | } 36 | return $this->backingArray()[$dim]; 37 | } 38 | 39 | public function arrayAssign($dim, ValRef $valRef) 40 | { 41 | if ($dim === null) { 42 | $this->backingArray()[] = $valRef; 43 | } else { 44 | $this->backingArray()[$dim] = $valRef; 45 | } 46 | } 47 | 48 | public function arrayUnset($dim) 49 | { 50 | unset($this->backingArray()[$dim]); 51 | } 52 | 53 | public function __toString() 54 | { 55 | $arr = $this->backingArray(); 56 | return 'Array(' . implode(', ', array_map(function ($key) use (&$arr) { 57 | return "$key => " . $arr[$key]; 58 | }, array_keys($arr))) . ')'; 59 | } 60 | 61 | public function __clone() 62 | { 63 | foreach($this->backingArray() as $name => &$ref) { 64 | $ref = clone $ref; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | getFilesystem()->write($virtualPath, $code); 14 | $deobf->setCurrentFilename($virtualPath); 15 | $tree = $deobf->parse($code); 16 | $tree = $deobf->deobfuscate($tree); 17 | $newCode = $deobf->prettyPrint($tree); 18 | return array($tree, $newCode); 19 | } 20 | 21 | $nodeDumper = new PhpParser\NodeDumper(); 22 | if (php_sapi_name() == 'cli') { 23 | $opts = getopt('tof:'); 24 | if (!isset($opts['f'])) { 25 | die("Missing required parameter -f\n"); 26 | } 27 | $filename = $opts['f']; 28 | $orig = isset($opts['o']); 29 | list($tree, $code) = deobfuscate(file_get_contents($filename), $filename, $orig); 30 | echo $code, "\n"; 31 | if (isset($opts['t'])) { 32 | echo $nodeDumper->dump($tree), "\n"; 33 | } 34 | } else { 35 | if (isset($_POST['phpdata'])) { 36 | $orig = array_key_exists('orig', $_GET); 37 | $php = $_POST['phpdata']; 38 | header('Content-Type: text/plain'); 39 | list($tree, $code) = deobfuscate($php, 'input.php', $orig); 40 | echo $code, "\n\n"; 41 | if (array_key_exists('tree', $_GET)) { 42 | echo '======== Tree =======', "\n"; 43 | echo $nodeDumper->dump($tree), "\n"; 44 | } 45 | } else { 46 | echo << 48 |
49 | 54 | 55 | 56 | HTML; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Reducer/MiscReducer.php: -------------------------------------------------------------------------------- 1 | parts as $part) { 15 | if ($part instanceof Node\Scalar\EncapsedStringPart) { 16 | $newString .= $part->value; 17 | } else { 18 | try { 19 | $newString .= Utils::getValue($part); 20 | } catch (\InvalidArgumentException $e) { 21 | return null; 22 | } 23 | } 24 | } 25 | return Utils::scalarToNode($newString); 26 | } 27 | 28 | public function reduceTernary(Node\Expr\Ternary $node) 29 | { 30 | return Utils::scalarToNode(Utils::getValue($node->cond) ? Utils::getValue($node->if) : Utils::getValue($node->else)); 31 | } 32 | 33 | public function reduceEcho(Node\Stmt\Echo_ $node) 34 | { 35 | $exprs = array(); 36 | foreach ($node->exprs as $expr) { 37 | try { 38 | $exprs[] = Utils::scalarToNode(Utils::getValue($expr)); 39 | } catch (Exceptions\UnknownValueException $e) { 40 | $exprs[] = $expr; 41 | } 42 | } 43 | return new Node\Stmt\Echo_($exprs); 44 | } 45 | 46 | public function reducePrint(Node\Expr\Print_ $node) 47 | { 48 | return new Node\Expr\Print_(Utils::scalarToNode(Utils::getValue($node->expr))); 49 | } 50 | 51 | public function reduceReturn(Node\Stmt\Return_ $node) 52 | { 53 | if ($node->expr === null) { 54 | return; 55 | } 56 | return new Node\Stmt\Return_(Utils::scalarToNode(Utils::getValue($node->expr))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/filesystem.txt: -------------------------------------------------------------------------------- 1 | INPUT 2 | 3 | $f = fopen(__FILE__, 'r'); 4 | $str = fread($f, 200); 5 | list(,, $payload) = explode('?>', $str); 6 | eval($payload . ''); 7 | ?> 8 | if ($doBadThing) { 9 | evil_payload(); 10 | } 11 | 12 | OUTPUT 13 | 14 | $f = fopen("/var/www/html/testcase.php", 'r'); 15 | $str = "', \$str);\neval(\$payload . '');\n?>\nif (\$doBadThing) {\n evil_payload();\n}"; 16 | list(, , $payload) = array(0 => " "', \$str);\neval(\$payload . '');\n", 2 => "\nif (\$doBadThing) {\n evil_payload();\n}"); 17 | eval /* PHPDeobfuscator eval output */ { 18 | if ($doBadThing) { 19 | evil_payload(); 20 | } 21 | }; 22 | ?> 23 | if ($doBadThing) { 24 | evil_payload(); 25 | } 26 | 27 | INPUT 28 | include "../test.php"; 29 | 30 | OUTPUT 31 | include "../test.php"; 32 | 33 | INPUT 34 | 35 | $f = fopen('test.txt', 'w'); 36 | fwrite($f, 'test'); 37 | fwrite($f, ' file'); 38 | fclose($f); 39 | $f = fopen('test.txt', 'r'); 40 | echo fread($f, 100); 41 | fclose($f); 42 | echo file_get_contents('test.txt'); 43 | 44 | OUTPUT 45 | 46 | $f = fopen('test.txt', 'w'); 47 | fwrite($f, 'test'); 48 | fwrite($f, ' file'); 49 | fclose($f); 50 | $f = fopen('test.txt', 'r'); 51 | echo "test file"; 52 | fclose($f); 53 | echo "test file"; 54 | 55 | INPUT 56 | 57 | $f = fopen('test.txt', 'w'); 58 | fwrite($f, 'test'); 59 | fclose($f); 60 | fwrite($f, 'closed'); 61 | fclose($f); 62 | $f = fopen('test.txt', 'r'); 63 | echo fread($f, 100); 64 | fclose($f); 65 | echo fread($f, 100); 66 | fclose($f); 67 | 68 | OUTPUT 69 | 70 | $f = fopen('test.txt', 'w'); 71 | fwrite($f, 'test'); 72 | fclose($f); 73 | fwrite($f, 'closed'); 74 | fclose($f); 75 | $f = fopen('test.txt', 'r'); 76 | echo "test"; 77 | fclose($f); 78 | echo fread($f, 100); 79 | fclose($f); 80 | -------------------------------------------------------------------------------- /src/Reducer/MagicReducer.php: -------------------------------------------------------------------------------- 1 | deobf = $deobf; 19 | $this->resolver = $resolver; 20 | } 21 | 22 | private static function nodeOrNull($value) 23 | { 24 | return $value === null ? null : Utils::scalarToNode($value); 25 | } 26 | 27 | public function reduceClass(MagicConst\Class_ $node) 28 | { 29 | return self::nodeOrNull($this->resolver->currentClass()); 30 | } 31 | 32 | public function reduceDir(MagicConst\Dir $node) 33 | { 34 | return self::nodeOrNull(dirname($this->deobf->getCurrentFilename())); 35 | } 36 | 37 | public function reduceFile(MagicConst\File $node) 38 | { 39 | return self::nodeOrNull($this->deobf->getCurrentFilename()); 40 | } 41 | 42 | public function reduceFunction(MagicConst\Function_ $node) 43 | { 44 | return self::nodeOrNull($this->resolver->currentFunction()); 45 | } 46 | 47 | public function reduceLine(MagicConst\Line $node) 48 | { 49 | if ($node->hasAttribute('startLine')) { 50 | return self::nodeOrNull($node->getAttribute('startLine')); 51 | } 52 | } 53 | 54 | public function reduceMethod(MagicConst\Method $node) 55 | { 56 | return self::nodeOrNull($this->resolver->currentMethod()); 57 | } 58 | 59 | public function reduceNamespace(MagicConst\Namespace_ $node) 60 | { 61 | return self::nodeOrNull($this->resolver->currentNamespace()); 62 | } 63 | 64 | public function reduceTrait(MagicConst\Trait_ $node) 65 | { 66 | return self::nodeOrNull($this->resolver->currentTrait()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Reducer/FuncCallReducer.php: -------------------------------------------------------------------------------- 1 | getSupportedNames() as $funcName) { 18 | if (isset($this->funcCallMap[$funcName])) { 19 | throw new \RuntimeException("Tried adding {$funcName} from reducer " . get_class($reducer) 20 | . "but was already added from " . get_class($this->funcCallMap[$funcName])); 21 | } 22 | $this->funcCallMap[$funcName] = $reducer; 23 | } 24 | } 25 | 26 | public function reduceFunctionCall(Node\Expr\FuncCall $node) 27 | { 28 | if ($node->name instanceof Node\Name) { 29 | $name = $node->name->toString(); 30 | } else { 31 | $name = Utils::getValue($node->name); 32 | $nameNode = new Node\Name($name); 33 | // Special case for MetadataVisitor 34 | $nameNode->setAttribute('replaces', $node->name); 35 | $node->name = $nameNode; 36 | } 37 | // Normalise to lowercase - function names are case insensitive 38 | return $this->makeFunctionCall(strtolower($name), $node); 39 | } 40 | 41 | private function makeFunctionCall($name, $node) 42 | { 43 | if(!isset($this->funcCallMap[$name])) { 44 | return; 45 | } 46 | $args = array(); 47 | foreach ($node->args as $arg) { 48 | $valRef = Utils::getValueRef($arg->value); 49 | if ($arg->byRef) { 50 | return; // "Call-time pass-by-reference has been removed" 51 | } 52 | $args[] = $valRef; 53 | } 54 | return $this->funcCallMap[$name]->execute($name, $args, $node); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/goto-tests.txt: -------------------------------------------------------------------------------- 1 | INPUT 2 | 3 | goto label4; 4 | label1: 5 | func4(); 6 | exit; 7 | label2: 8 | func3(); 9 | goto label1; 10 | label3: 11 | func2(); 12 | goto label2; 13 | label4: 14 | func1(); 15 | goto label3; 16 | 17 | OUTPUT 18 | 19 | func1(); 20 | func2(); 21 | func3(); 22 | func4(); 23 | exit; 24 | 25 | INPUT 26 | 27 | goto LabelA; 28 | LabelA: 29 | LabelB: 30 | LabelC: 31 | echo 'hello'; 32 | 33 | OUTPUT 34 | 35 | echo "hello"; 36 | 37 | INPUT 38 | 39 | A: 40 | B: 41 | C: 42 | 1; 43 | 44 | OUTPUT 45 | 46 | 1; 47 | 48 | INPUT 49 | 50 | if (1) { 51 | goto A; 52 | } else { 53 | goto A; 54 | } 55 | 56 | goto B; 57 | 58 | A: 59 | C: 60 | B: 61 | 1; 62 | 63 | OUTPUT 64 | 65 | if (1) { 66 | goto A; 67 | } else { 68 | goto A; 69 | } 70 | A: 71 | 1; 72 | 73 | INPUT 74 | 75 | if (1) { 76 | 2; 77 | goto end; 78 | } 79 | 3; 80 | end: 81 | 4; 82 | 83 | OUTPUT 84 | 85 | if (1) { 86 | 2; 87 | goto end; 88 | } 89 | 3; 90 | end: 91 | 4; 92 | 93 | INPUT 94 | 95 | $something = false; 96 | $otherthing = false; 97 | $another = true; 98 | 99 | if ($something) { 100 | goto abc; 101 | abc: 102 | echo "true"; 103 | } elseif ($otherthing) { 104 | echo "other 1"; 105 | } elseif ($another) { 106 | echo "alt"; 107 | goto def; 108 | } else { 109 | goto def; 110 | def: 111 | echo "false"; 112 | goto abc; 113 | } 114 | 115 | OUTPUT 116 | 117 | $something = false; 118 | $otherthing = false; 119 | $another = true; 120 | if ($something) { 121 | abc: 122 | echo "true"; 123 | } elseif ($otherthing) { 124 | echo "other 1"; 125 | } elseif ($another) { 126 | echo "alt"; 127 | def: 128 | echo "false"; 129 | goto abc; 130 | } else { 131 | goto def; 132 | } 133 | 134 | INPUT 135 | 136 | function () { 137 | goto A; 138 | B: 139 | 1; 140 | return; 141 | A: 142 | 2; 143 | goto B; 144 | }; 145 | 146 | OUTPUT 147 | 148 | function () { 149 | 2; 150 | 1; 151 | return; 152 | }; 153 | -------------------------------------------------------------------------------- /src/ValRef/ByReference.php: -------------------------------------------------------------------------------- 1 | variable = $varRef; 18 | $this->scope = $scope; 19 | } 20 | 21 | public function isMutable() 22 | { 23 | return $this->getVal()->isMutable(); 24 | } 25 | 26 | public function setMutable($mutable) 27 | { 28 | try { 29 | $this->getVal()->setMutable($mutable); 30 | } catch (Exceptions\UnknownValueException $e) { 31 | // Don't care 32 | } 33 | } 34 | 35 | public function getValue() 36 | { 37 | return $this->getVal()->getValue(); 38 | } 39 | 40 | public function arrayFetch($dim) 41 | { 42 | return $this->getVal()->arrayFetch($dim); 43 | } 44 | 45 | public function arrayAssign($dim, ValRef $valRef) 46 | { 47 | $this->getVal()->arrayAssign($dim, $valRef); 48 | } 49 | 50 | public function arrayUnset($dim) 51 | { 52 | $this->getVal()->arrayUnset($dim); 53 | } 54 | 55 | public function propertyFetch($name) 56 | { 57 | return $this->getVal()->propertyFetch($name); 58 | } 59 | 60 | public function propertyAssign($name, ValRef $valRef) 61 | { 62 | $this->getVal()->propertyAssign($name, $valRef); 63 | } 64 | 65 | public function propertyUnset($name) 66 | { 67 | $this->getVal()->propertyUnset($name); 68 | } 69 | 70 | public function __toString() 71 | { 72 | return "ByRef{{$this->variable} in scope {$this->scope}}"; 73 | } 74 | 75 | public function getVariable() 76 | { 77 | return $this->variable; 78 | } 79 | 80 | private function getVal() 81 | { 82 | $val = $this->variable->getValue($this->scope); 83 | if ($val === null) { 84 | throw new Exceptions\UnknownValueException("Cannot get value of reference"); 85 | } 86 | return $val; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/ReducerVisitor.php: -------------------------------------------------------------------------------- 1 | getNodeClasses() as $className) { 15 | if (isset($this->reducerByClass[$className])) { 16 | throw new \RuntimeException("Tried adding {$className} from reducer " . get_class($reducer) 17 | . "but was already added from " . get_class($this->reducerByClass[$className])); 18 | } 19 | $this->reducerByClass[$className] = $reducer; 20 | } 21 | } 22 | 23 | public function enterNode(Node $node) 24 | { 25 | // For MaybeStmtArray, we tag the inner expression node as being attached to a Stmt\Expression 26 | if ($node instanceof Node\Stmt\Expression) { 27 | $node->expr->setAttribute(AttrName::IN_EXPR_STMT, true); 28 | } 29 | } 30 | 31 | public function leaveNode(Node $node) 32 | { 33 | // If Stmt\Expression was forwarded a MaybeStmtArray, now is the time to action it 34 | if ($node instanceof Node\Stmt\Expression && $node->expr instanceof MaybeStmtArray) { 35 | return $node->expr->stmts; 36 | } 37 | try { 38 | $newNode = $this->reduceNode($node); 39 | // Reducer wants to return a statement array, we forward this request if we'e inside a Stmt\Expression 40 | // Otherwise, use the fallback expression 41 | if ($newNode instanceof MaybeStmtArray) { 42 | if ($node->getAttribute(AttrName::IN_EXPR_STMT) === true) { 43 | return $newNode; 44 | } 45 | return $newNode->expr; 46 | } 47 | return $newNode; 48 | } catch (Exceptions\BadValueException $e) { 49 | } 50 | } 51 | 52 | private function reduceNode(Node $node) 53 | { 54 | $className = get_class($node); 55 | if (isset($this->reducerByClass[$className])) { 56 | return $this->reducerByClass[$className]->reduce($node, $this); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Scope.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->superGlobals = $parent ? $parent->getSuperGlobals() : array(); 16 | $this->parentScope = $parent; 17 | } 18 | 19 | public function getParent() 20 | { 21 | return $this->parentScope; 22 | } 23 | 24 | public function getSuperGlobals() 25 | { 26 | return $this->superGlobals; 27 | } 28 | 29 | public function setSuperGlobal($name, ValRef $val) 30 | { 31 | if ($this->parentScope !== null) { 32 | throw new \LogicException("Must be global scope to set a super global"); 33 | } 34 | $this->superGlobals[$name] = $val; 35 | } 36 | 37 | public function setVariable($name, ValRef $val) 38 | { 39 | if (isset($this->superGlobals[$name])) { 40 | // Superglobals can be reassigned. They simply take the value of whatever is given 41 | $this->superGlobals[$name] = $val; 42 | } 43 | $this->variables[$name] = $val; 44 | } 45 | 46 | public function getVariable($name) 47 | { 48 | if (isset($this->superGlobals[$name])) { 49 | return $this->superGlobals[$name]; 50 | } 51 | if (isset($this->variables[$name])) { 52 | return $this->variables[$name]; 53 | } 54 | return null; 55 | } 56 | 57 | public function unsetVariable($name) 58 | { 59 | unset($this->variables[$name]); 60 | } 61 | 62 | public function __clone() 63 | { 64 | if ($this->parentScope) { 65 | $this->parentScope = clone $this->parentScope; 66 | } 67 | foreach ($this->variables as $name => &$val) { 68 | $val = clone $val; 69 | } 70 | foreach ($this->superGlobals as $name => &$val) { 71 | $val = clone $val; 72 | } 73 | } 74 | 75 | public function &getVariables() 76 | { 77 | return $this->variables; 78 | } 79 | 80 | public function __toString() 81 | { 82 | return "{$this->parentScope}.{$this->name}"; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /test.php: -------------------------------------------------------------------------------- 1 | array(), 'output' => array()); 23 | $lines = null; 24 | while (!feof($f)) { 25 | $line = fgets($f); 26 | if (trim($line) === 'INPUT') { 27 | if ($lines !== null) { 28 | $tests[] = $curTest; 29 | $curTest = array('input' => array(), 'output' => array()); 30 | } 31 | $lines = &$curTest['input']; 32 | continue; 33 | } elseif (trim($line) === 'OUTPUT') { 34 | $lines = &$curTest['output']; 35 | continue; 36 | } 37 | if ($lines !== null) { 38 | $lines[] = $line; 39 | } 40 | } 41 | if ($lines !== null) { 42 | $tests[] = $curTest; 43 | } 44 | fclose($f); 45 | foreach ($tests as $i => $test) { 46 | $name = $testfile . '/' . ($i + 1); 47 | $code = "getFilesystem()->write($virtualPath, $code); 50 | $deobf->setCurrentFilename($virtualPath); 51 | try { 52 | $out = $deobf->prettyPrint($deobf->deobfuscate($deobf->parse($code))); 53 | } catch (\Exception | \Error $e) { 54 | echo "Test $name failed:\n"; 55 | echo "Exception: " . $e->getMessage() . "\n"; 56 | echo $e->getTraceAsString() . "\n"; 57 | continue; 58 | } 59 | $expect = " Scalar\String_::KIND_DOUBLE_QUOTED), $attrs)); 28 | } 29 | if (is_null($value)) { 30 | return new Node\Expr\ConstFetch(new Node\Name('null'), $attrs); 31 | } 32 | if (is_bool($value)) { 33 | return new Node\Expr\ConstFetch(new Node\Name($value ? 'true' : 'false'), $attrs); 34 | } 35 | if (is_array($value)) { 36 | $items = array(); 37 | $valArray = array(); 38 | foreach ($value as $key => $val) { 39 | $valNode = self::scalarToNode($val); 40 | $keyNode = self::scalarToNode($key); 41 | $items[] = new Node\Expr\ArrayItem($valNode, $keyNode); 42 | $valArray[self::getValue($keyNode)] = self::getValueRef($valNode); 43 | } 44 | $attrs[AttrName::VALUE] = new ArrayVal($valArray); 45 | return new Node\Expr\Array_($items, $attrs); 46 | } 47 | throw new \Exception("Unknown value type"); 48 | } 49 | 50 | public static function getValueRef(Node $node) 51 | { 52 | $valRef = $node->getAttribute(AttrName::VALUE); 53 | if ($valRef === null) { 54 | throw new Exceptions\UnknownValueException("Cannot determine value of node"); 55 | } 56 | return $valRef; 57 | } 58 | 59 | public static function getValue(Node $node) 60 | { 61 | return self::getValueRef($node)->getValue(); 62 | } 63 | 64 | public static function refsToValues(array $refs) 65 | { 66 | $values = array(); 67 | foreach ($refs as $ref) { 68 | $values[] = $ref->getValue(); 69 | } 70 | return $values; 71 | } 72 | 73 | public static function safeFileExists(Filesystem $fileSystem, $path) 74 | { 75 | try { 76 | return $fileSystem->fileExists($path); 77 | } catch (PathTraversalDetected $e) { 78 | return false; 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /tests/reducers.txt: -------------------------------------------------------------------------------- 1 | INPUT 2 | 3 | eval(base64_decode("ZWNobyAnSGVsbG8gV29ybGQnOwo=")); 4 | 5 | OUTPUT 6 | 7 | eval /* PHPDeobfuscator eval output */ { 8 | echo "Hello World"; 9 | }; 10 | 11 | INPUT 12 | echo chr((1 << 6) + 1) . "bc"; 13 | 14 | OUTPUT 15 | echo "Abc"; 16 | 17 | INPUT 18 | $a = 'fo' . 'o'; 19 | print "test$a\n"; 20 | eval('print "test$a\n";'); 21 | 22 | OUTPUT 23 | $a = 'foo'; 24 | print "testfoo\n"; 25 | print "testfoo\n"; 26 | 27 | INPUT 28 | print_r(explode('.', 'a.b.c')); 29 | print implode('', array(1, 2, 3)); 30 | 31 | OUTPUT 32 | print_r(array(0 => "a", 1 => "b", 2 => "c")); 33 | print "123"; 34 | 35 | INPUT 36 | print preg_replace('/([a-z])/e', 'strtoupper("$1")', 'hello world'); 37 | 38 | OUTPUT 39 | print "HELLO WORLD"; 40 | 41 | INPUT 42 | 43 | $a = 'test'; 44 | echo $a; 45 | print $a; 46 | 47 | OUTPUT 48 | 49 | $a = 'test'; 50 | echo "test"; 51 | print "test"; 52 | 53 | INPUT 54 | 55 | $a = null; 56 | $b = 10; 57 | $a ?? $b; 58 | $a = 2; 59 | $a ?? $b; 60 | $b ** $a; 61 | $a <=> $b; 62 | $b <=> $a; 63 | $a <=> $a; 64 | 65 | OUTPUT 66 | 67 | $a = null; 68 | $b = 10; 69 | 10; 70 | $a = 2; 71 | 2; 72 | 100; 73 | -1; 74 | 1; 75 | 0; 76 | 77 | INPUT 78 | 79 | $arr = ['a' => 1, 'b' => 10]; 80 | $obj = new stdClass(); 81 | $obj->a = 1; 82 | $obj->b = 10; 83 | $a = "a"; 84 | $a++; 85 | $arr['a']++; 86 | $obj->a++; 87 | foo($a++) . foo($a++); 88 | 89 | $arr = ['a' => 1, 'b' => 10]; 90 | $obj = new stdClass(); 91 | $obj->a = 1; 92 | $obj->b = 10; 93 | $a = 1; 94 | ++$a; 95 | ++$arr['a']; 96 | ++$obj->a; 97 | foo(++$a); 98 | 99 | $arr = ['a' => 1, 'b' => 10]; 100 | $obj = new stdClass(); 101 | $obj->a = 1; 102 | $obj->b = 10; 103 | $b = 10; 104 | $b--; 105 | $arr['b']--; 106 | $obj->b--; 107 | foo($b--) . foo($b--); 108 | 109 | $arr = ['a' => 1, 'b' => 10]; 110 | $obj = new stdClass(); 111 | $obj->a = 1; 112 | $obj->b = 10; 113 | $b = 10; 114 | --$b; 115 | --$arr['b']; 116 | --$obj->b; 117 | foo(--$b); 118 | 119 | OUTPUT 120 | 121 | $arr = ['a' => 1, 'b' => 10]; 122 | $obj = new stdClass(); 123 | $obj->a = 1; 124 | $obj->b = 10; 125 | $a = "a"; 126 | $a = "b"; 127 | $arr['a'] = 2; 128 | $obj->a = 2; 129 | foo((function () use(&$a) { 130 | $a = "c"; 131 | return "b"; 132 | })()) . foo((function () use(&$a) { 133 | $a = "d"; 134 | return "c"; 135 | })()); 136 | $arr = ['a' => 1, 'b' => 10]; 137 | $obj = new stdClass(); 138 | $obj->a = 1; 139 | $obj->b = 10; 140 | $a = 1; 141 | $a = 2; 142 | $arr['a'] = 2; 143 | $obj->a = 2; 144 | foo($a = 3); 145 | $arr = ['a' => 1, 'b' => 10]; 146 | $obj = new stdClass(); 147 | $obj->a = 1; 148 | $obj->b = 10; 149 | $b = 10; 150 | $b = 9; 151 | $arr['b'] = 9; 152 | $obj->b = 9; 153 | foo((function () use(&$b) { 154 | $b = 8; 155 | return 9; 156 | })()) . foo((function () use(&$b) { 157 | $b = 7; 158 | return 8; 159 | })()); 160 | $arr = ['a' => 1, 'b' => 10]; 161 | $obj = new stdClass(); 162 | $obj->a = 1; 163 | $obj->b = 10; 164 | $b = 10; 165 | $b = 9; 166 | $arr['b'] = 9; 167 | $obj->b = 9; 168 | foo($b = 8); 169 | 170 | INPUT 171 | 172 | $x = ChR(65); 173 | $func = 'oRD'; 174 | $y = $func('A'); 175 | 176 | OUTPUT 177 | 178 | $x = "A"; 179 | $func = 'oRD'; 180 | $y = 65; 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPDeobfuscator 2 | 3 | ## Overview 4 | 5 | This deobfuscator attempts to reverse common obfuscation techniques applied to PHP source code. 6 | 7 | It is implemented in PHP with the help of [PHP-Parser](https://github.com/nikic/PHP-Parser). 8 | 9 | ## Features 10 | 11 | - Reduces all constant expressions e.g. `1 + 2` is replaced by `3` 12 | - Safely run whitelisted PHP functions e.g. `base64_decode` 13 | - Deobfuscate `eval` expressions 14 | - Unwrap deeply nested obfuscation 15 | - Filesystem virtualization 16 | - Variable resolver (e.g. `$var1 = 10; $var2 = &$var1; $var2 = 20;` can determine `$var1` equals `20`) 17 | - Rewrite control flow obfuscation 18 | 19 | ## Installation 20 | 21 | PHP Deobfuscator uses [Composer](https://getcomposer.org/) to manage its dependencies. Make sure Composer is installed first. 22 | 23 | Run `composer install` in the root of this project to fetch dependencies. 24 | 25 | ## Usage 26 | 27 | ### CLI 28 | 29 | ``` 30 | php index.php [-f filename] [-t] [-o] 31 | 32 | required arguments: 33 | 34 | -f The obfuscated PHP file 35 | 36 | optional arguments: 37 | 38 | -t Dump the output node tree for debugging 39 | -o Output comments next to each expression with the original code 40 | ``` 41 | 42 | The deobfuscated output is printed to STDOUT. 43 | 44 | ### Web Server 45 | 46 | `index.php` outputs a simple textarea to paste the PHP code into. Deobfuscated code is printed when the form is submitted 47 | 48 | ## Examples 49 | 50 | #### Input 51 | ```php 52 | ', $str); 70 | eval($payload . ''); 71 | ?> 72 | if ($doBadThing) { 73 | evil_payload(); 74 | } 75 | ``` 76 | 77 | #### Output 78 | ```php 79 | ', \$str);\neval(\$payload . '');\n?>\nif (\$doBadThing) {\n evil_payload();\n}\n"; 83 | list(, , $payload) = array(0 => "\n\$f = fopen(__FILE__, 'r');\n\$str = fread(\$f, 200);\nlist(,, \$payload) = explode('", 1 => "', \$str);\neval(\$payload . '');\n", 2 => "\nif (\$doBadThing) {\n evil_payload();\n}\n"); 84 | eval /* PHPDeobfuscator eval output */ { 85 | if ($doBadThing) { 86 | evil_payload(); 87 | } 88 | }; 89 | ?> 90 | if ($doBadThing) { 91 | evil_payload(); 92 | } 93 | ``` 94 | 95 | #### Input 96 | ```php 97 | deobfuscator = $deobfuscator; 21 | $this->outputAsEvalStr = $outputAsEvalStr; 22 | } 23 | 24 | public function reduceEval(Expr\Eval_ $node) 25 | { 26 | $expr = Utils::getValue($node->expr); 27 | if (!is_string($expr)) { 28 | return null; 29 | } 30 | $newExpr = $this->tryRunEval($expr); 31 | return $newExpr; 32 | } 33 | 34 | public function reduceInclude(Expr\Include_ $node) 35 | { 36 | // TODO $node->type 37 | // TODO should this replace the include with an eval or should it just export the symbols? 38 | // need to handle recursive includes 39 | // One of Include_::(TYPE_INCLUDE, TYPE_INCLUDE_ONCE, TYPE_REQUIRE, TYPE_REQUIRE_ONCE) 40 | $file = Utils::getValue($node->expr); 41 | $fileSystem = $this->deobfuscator->getFilesystem(); 42 | if (!Utils::safeFileExists($fileSystem, $file)) { 43 | return; 44 | } 45 | $code = $fileSystem->read($file); 46 | return $this->tryRunEval($code); 47 | } 48 | 49 | private function tryRunEval($code) 50 | { 51 | try { 52 | return $this->runEval($code); 53 | } catch (\Exception $e) { 54 | print "Error traversing". PHP_EOL; 55 | echo $e->getMessage() . PHP_EOL; 56 | echo $e->getTraceAsString() . PHP_EOL; 57 | return null; 58 | } 59 | } 60 | 61 | public function runEval($code) 62 | { 63 | $origTree = $this->parseCode($code); 64 | $tree = $this->deobfTree($origTree); 65 | // If it's just a single expression, return directly 66 | // XXX this is not semantically correct because eval does not return 67 | // anything by default 68 | if (count($tree) === 1 && $tree[0] instanceof Stmt\Expression) { 69 | return $tree[0]->expr; 70 | } 71 | if (count($tree) === 1 && $tree[0] instanceof Stmt\Return_) { 72 | return $tree[0]->expr; 73 | } 74 | if ($this->outputAsEvalStr) { 75 | $expr = new Expr\Eval_(new String_($this->deobfuscator->prettyPrint($tree, false), array( 76 | 'kind' => String_::KIND_NOWDOC, 'docLabel' => 'EVAL' . rand() 77 | ))) ; 78 | } else { 79 | $expr = new EvalBlock($tree, $origTree); 80 | } 81 | return $expr; 82 | } 83 | 84 | private function parseCode($code) 85 | { 86 | /* Convert ?> into */ 87 | if (substr($code, 0, 2) == '?>' && $code[2] != '<') { 88 | $code[0] = '<'; 89 | $code[1] = '?'; 90 | } 91 | $prefix = substr($code, 0, 2) == '' ? '' : 'deobfuscator->parse("{$prefix}{$code}"); 93 | } 94 | 95 | private function deobfTree($tree) 96 | { 97 | return $this->deobfuscator->deobfuscate($tree); 98 | } 99 | 100 | public function runEvalTree($code) 101 | { 102 | return $this->deobfTree($this->parseCode($code)); 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /tests/variables.txt: -------------------------------------------------------------------------------- 1 | INPUT 2 | $x = 'y'; 3 | $$x = 10; 4 | echo $y * 2; 5 | 6 | OUTPUT 7 | $x = 'y'; 8 | $y = 10; 9 | echo 20; 10 | 11 | INPUT 12 | $s = "abc"; 13 | $s[0] = 0; 14 | echo $s; 15 | 16 | OUTPUT 17 | $s = "abc"; 18 | $s[0] = 0; 19 | echo "0bc"; 20 | 21 | INPUT 22 | $a = []; 23 | $a[] = "abc"; 24 | echo $a[0]; 25 | 26 | OUTPUT 27 | $a = []; 28 | $a[] = "abc"; 29 | echo "abc"; 30 | 31 | INPUT 32 | 33 | $obj = new stdClass(); 34 | $obj->foo = 'bar'; 35 | echo '' . $obj->foo; 36 | 37 | OUTPUT 38 | 39 | $obj = new stdClass(); 40 | $obj->foo = 'bar'; 41 | echo "bar"; 42 | 43 | INPUT 44 | 45 | $a = "base64_decode"; 46 | $b = $a("YmFzZTY0X2RlY29kZQ=="); 47 | ${$b("dGhlQ29kZQ==")} = "VGVzdA=="; 48 | 49 | function test() { 50 | global $b, $theCode; 51 | echo "{$b("$theCode")} 123\n"; 52 | 53 | } 54 | 55 | test(); 56 | 57 | OUTPUT 58 | 59 | $a = "base64_decode"; 60 | $b = "base64_decode"; 61 | $theCode = "VGVzdA=="; 62 | function test() 63 | { 64 | global $b, $theCode; 65 | echo "Test 123\n"; 66 | } 67 | test(); 68 | 69 | INPUT 70 | 71 | $test = 'abc'; 72 | echo $test; 73 | for ($i = 0; $i < 10; $i++) { 74 | $arr[$i] = $i; 75 | $arr[2] = 100; 76 | $temp = "a"; 77 | $b = $temp; 78 | echo "$b"; 79 | echo "$j"; 80 | echo "{$arr[$i]}"; 81 | } 82 | $a = array(1, 2,3); 83 | $a[0] = $a[2]; 84 | echo "$b" . "$temp" . "$a" . "$i" . "$test"; 85 | echo "$test"; 86 | 87 | OUTPUT 88 | 89 | $test = 'abc'; 90 | echo "abc"; 91 | for ($i = 0; $i < 10; $i++) { 92 | $arr[$i] = $i; 93 | $arr[2] = 100; 94 | $temp = "a"; 95 | $b = $temp; 96 | echo "a"; 97 | echo "{$j}"; 98 | echo "{$arr[$i]}"; 99 | } 100 | $a = array(1, 2, 3); 101 | $a[0] = $a[2]; 102 | echo "{$b}" . "{$temp}" . "Array" . "{$i}" . "{$test}"; 103 | echo "{$test}"; 104 | 105 | INPUT 106 | 107 | namespace NS; 108 | echo __LINE__; 109 | echo __FILE__; 110 | echo __DIR__; 111 | 112 | function func() { 113 | echo __FUNCTION__; 114 | } 115 | 116 | class Foo { 117 | function bar() { 118 | echo __CLASS__; 119 | echo __METHOD__; 120 | echo __NAMESPACE__; 121 | echo __FUNCTION__; 122 | } 123 | } 124 | trait T { 125 | function f() { 126 | echo __TRAIT__; 127 | echo __NAMESPACE__; 128 | echo __CLASS__; 129 | } 130 | } 131 | 132 | OUTPUT 133 | 134 | namespace NS; 135 | 136 | echo 3; 137 | echo "/var/www/html/testcase.php"; 138 | echo "/var/www/html"; 139 | function func() 140 | { 141 | echo "NS\\func"; 142 | } 143 | class Foo 144 | { 145 | function bar() 146 | { 147 | echo "NS\\Foo"; 148 | echo "NS\\Foo::bar"; 149 | echo "NS"; 150 | echo "bar"; 151 | } 152 | } 153 | trait T 154 | { 155 | function f() 156 | { 157 | echo "NS\\T"; 158 | echo "NS"; 159 | echo __CLASS__; 160 | } 161 | } 162 | 163 | INPUT 164 | 165 | function foo() { 166 | $myVar = "123"; 167 | echo eval('return $myVar;'); 168 | } 169 | 170 | OUTPUT 171 | 172 | function foo() 173 | { 174 | $myVar = "123"; 175 | echo "123"; 176 | } 177 | 178 | INPUT 179 | 180 | $var = "foo"; 181 | function () { 182 | return $var; 183 | }; 184 | function () use ($var) { 185 | return $var; 186 | }; 187 | function () use (&$var) { 188 | return $var; 189 | }; 190 | 191 | OUTPUT 192 | 193 | $var = "foo"; 194 | function () { 195 | return $var; 196 | }; 197 | function () use($var) { 198 | return "foo"; 199 | }; 200 | function () use(&$var) { 201 | return "foo"; 202 | }; 203 | 204 | INPUT 205 | 206 | $a = 0; 207 | LABEL: 208 | print $a; 209 | if ($a < 10) { 210 | $a++; 211 | goto LABEL; 212 | } 213 | 214 | OUTPUT 215 | 216 | $a = 0; 217 | LABEL: 218 | print $a; 219 | if ($a < 10) { 220 | $a++; 221 | goto LABEL; 222 | } 223 | -------------------------------------------------------------------------------- /src/ResolveValueVisitor.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 21 | } 22 | 23 | private function getConstant($name) 24 | { 25 | $lower = strtolower($name); 26 | if ($lower === 'null') { 27 | return new ScalarValue(null); 28 | } 29 | if ($lower === 'true') { 30 | return new ScalarValue(true); 31 | } 32 | if ($lower === 'false') { 33 | return new ScalarValue(false); 34 | } 35 | return $this->resolver->getConstant($name); 36 | } 37 | 38 | public function leaveNode(Node $node) 39 | { 40 | if ($node instanceof Expr) { 41 | try { 42 | $this->eagerSetValue($node); 43 | } catch (Exceptions\BadValueException $e) { 44 | } 45 | } 46 | } 47 | 48 | private function eagerSetValue(Expr $expr) 49 | { 50 | if ($expr->hasAttribute(AttrName::VALUE)) { 51 | return; 52 | } 53 | $value = null; 54 | if ($expr instanceof Expr\ConstFetch) { 55 | $name = $expr->name->toString(); 56 | $value = $this->getConstant($name); 57 | } elseif ($expr instanceof Expr\Array_) { 58 | $valArray = array(); 59 | foreach ($expr->items as $item) { 60 | try { 61 | $valRef = Utils::getValueRef($item->value); 62 | } catch (Exceptions\UnknownValueException $e) { 63 | // Allow for partially known arrays (don't bomb out if 64 | // there's an unknown, instead just mark as unknown) 65 | $valRef = UnknownValRef::$INSTANCE; 66 | } 67 | if ($item->key === null) { 68 | $valArray[] = $valRef; 69 | } else { 70 | $valArray[Utils::getValue($item->key)] = $valRef; 71 | } 72 | } 73 | $value = new ArrayVal($valArray); 74 | } elseif ($expr instanceof Scalar\String_) { 75 | $value = new ScalarValue($expr->value); 76 | } elseif ($expr instanceof Scalar\DNumber) { 77 | $value = new ScalarValue($expr->value); 78 | } elseif ($expr instanceof Scalar\LNumber) { 79 | $value = new ScalarValue($expr->value); 80 | } elseif ($expr instanceof Expr\New_) { 81 | $class = null; 82 | if ($expr->class instanceof Expr) { 83 | $nameRef = Utils::getValueRef($expr->class); 84 | if ($nameRef !== null && !$nameRef->isMutable()) { 85 | $name = $nameRef->getValue(); 86 | } 87 | } else { 88 | $class = $expr->class->toString(); 89 | } 90 | if ($class != null) { 91 | if (strtolower($class) === 'stdclass') { 92 | $value = new ObjectVal(); 93 | } 94 | } 95 | } elseif ($expr instanceof Expr\ErrorSuppress) { 96 | $value = Utils::getValueRef($expr->expr); 97 | } 98 | if ($value === null) { 99 | // Try resolving any variable references 100 | $varRef = $this->resolver->resolveVariable($expr); 101 | $value = $varRef->getValue($this->resolver->getCurrentScope()); 102 | } 103 | if ($value !== null) { 104 | $expr->setAttribute(AttrName::VALUE, $value); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/Deobfuscator.php: -------------------------------------------------------------------------------- 1 | parser = (new \PhpParser\ParserFactory())->create(\PhpParser\ParserFactory::PREFER_PHP7); 22 | $this->prettyPrinter = new ExtendedPrettyPrinter(); 23 | 24 | $this->firstPass = new \PhpParser\NodeTraverser; 25 | $this->secondPass = new \PhpParser\NodeTraverser; 26 | 27 | $this->firstPass->addVisitor(new ControlFlowVisitor()); 28 | 29 | if ($dumpOrig) { 30 | $this->secondPass->addVisitor(new AddOriginalVisitor($this)); 31 | } 32 | $resolver = new Resolver(); 33 | $this->secondPass->addVisitor($resolver); 34 | $this->secondPass->addVisitor(new ResolveValueVisitor($resolver)); 35 | 36 | $this->fileSystem = new Filesystem(new InMemoryFilesystemAdapter()); 37 | 38 | $evalReducer = new Reducer\EvalReducer($this); 39 | 40 | $funcCallReducer = new Reducer\FuncCallReducer(); 41 | $funcCallReducer->addReducer(new Reducer\FuncCallReducer\FunctionSandbox()); 42 | $funcCallReducer->addReducer(new Reducer\FuncCallReducer\FileSystemCall($this->fileSystem)); 43 | $funcCallReducer->addReducer(new Reducer\FuncCallReducer\MiscFunctions($evalReducer, $resolver)); 44 | $funcCallReducer->addReducer(new Reducer\FuncCallReducer\PassThrough()); 45 | 46 | $reducer = new ReducerVisitor(); 47 | $reducer->addReducer(new Reducer\BinaryOpReducer()); 48 | $reducer->addReducer($evalReducer); 49 | $reducer->addReducer($funcCallReducer); 50 | $reducer->addReducer(new Reducer\MagicReducer($this, $resolver)); 51 | $reducer->addReducer(new Reducer\UnaryReducer($resolver)); 52 | $reducer->addReducer(new Reducer\MiscReducer()); 53 | 54 | $this->secondPass->addVisitor($reducer); 55 | 56 | if ($annotateReductions) { 57 | $this->metaVisitor = new MetadataVisitor($this); 58 | $this->secondPass->addVisitor($this->metaVisitor); 59 | } else { 60 | $this->metaVisitor = null; 61 | } 62 | } 63 | 64 | public function getFilesystem() 65 | { 66 | return $this->fileSystem; 67 | } 68 | 69 | public function getCurrentFilename() 70 | { 71 | return $this->filename; 72 | } 73 | 74 | public function setCurrentFilename($filename) 75 | { 76 | $this->filename = $filename; 77 | } 78 | 79 | public function parse($phpCode) 80 | { 81 | $phpCode = str_ireplace('=', 'parser->parse($phpCode); 85 | } 86 | 87 | public function prettyPrint(array $tree, $file = true) 88 | { 89 | if ($file) { 90 | return $this->prettyPrinter->prettyPrintFile($tree); 91 | } else { 92 | return $this->prettyPrinter->prettyPrint($tree); 93 | } 94 | } 95 | 96 | public function printFileReductions(array $stmts) 97 | { 98 | if ($this->metaVisitor === null) { 99 | throw new \LogicException("annotateReductions was not set on construction"); 100 | } 101 | return $this->metaVisitor->printFileReductions($stmts); 102 | } 103 | 104 | public function deobfuscate(array $tree) 105 | { 106 | $tree = $this->firstPass->traverse($tree); 107 | $tree = $this->secondPass->traverse($tree); 108 | return $tree; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/Reducer/UnaryReducer.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 20 | } 21 | 22 | public function reduceUnaryMinus(Expr\UnaryMinus $node) 23 | { 24 | return Utils::scalarToNode(-Utils::getValue($node->expr)); 25 | } 26 | 27 | public function reduceBoolCast(Cast\Bool_ $node) 28 | { 29 | $val = Utils::getValue($node->expr); 30 | return Utils::scalarToNode((bool) $val); 31 | } 32 | 33 | public function reduceDoubleCast(Cast\Double $node) 34 | { 35 | $val = Utils::getValue($node->expr); 36 | return Utils::scalarToNode((double) $val); 37 | } 38 | 39 | public function reduceIntCast(Cast\Int_ $node) 40 | { 41 | $val = Utils::getValue($node->expr); 42 | return Utils::scalarToNode((int) $val); 43 | } 44 | 45 | public function reduceStringCast(Cast\String_ $node) 46 | { 47 | $val = Utils::getValue($node->expr); 48 | return Utils::scalarToNode((string) $val); 49 | } 50 | 51 | 52 | public function reducePostInc(Expr\PostInc $node) 53 | { 54 | return $this->postIncDec($node, true); 55 | } 56 | 57 | public function reducePostDec(Expr\PostDec $node) 58 | { 59 | return $this->postIncDec($node, false); 60 | } 61 | 62 | public function reducePreInc(Expr\PreInc $node) 63 | { 64 | return $this->preIncDec($node, true); 65 | } 66 | 67 | public function reducePreDec(Expr\PreDec $node) 68 | { 69 | return $this->preIncDec($node, false); 70 | } 71 | 72 | private function postIncDec(Expr $node, $isInc) 73 | { 74 | // Perform the operation and create old and new nodes 75 | $val = Utils::getValue($node->var); 76 | $oldValNode = Utils::scalarToNode($val); 77 | $isInc ? $val++ : $val--; 78 | $newValNode = Utils::scalarToNode($val); 79 | 80 | // Internally set the new value on the variable 81 | $var = $this->resolver->resolveVariable($node->var); 82 | $newValRef = Utils::getValueRef($newValNode); 83 | $var->assignValue($this->resolver->getCurrentScope(), $newValRef); 84 | 85 | $varNode = $node->var; 86 | if ($varNode instanceof Expr\PropertyFetch) { 87 | $varNode = $varNode->var; 88 | } elseif ($varNode instanceof Expr\ArrayDimFetch) { 89 | $varNode = $varNode->var; 90 | } 91 | // If the return value is ignored, attempt to return the final assignment 92 | // Fall back to an immediately invoked closure that implements the correct 93 | // semantics. 94 | $stmts = [ 95 | new Stmt\Expression(new Expr\Assign($node->var, $newValNode)), 96 | ]; 97 | $expr = new Expr\FuncCall(new Expr\Closure([ 98 | 'uses' => [new Expr\ClosureUse($varNode, true)], 99 | 'stmts' => [ 100 | new Stmt\Expression(new Expr\Assign($node->var, $newValNode)), 101 | new Stmt\Return_($oldValNode), 102 | ], 103 | ])); 104 | return new MaybeStmtArray($stmts, $expr); 105 | } 106 | 107 | private function preIncDec(Expr $node, $isInc) 108 | { 109 | $val = Utils::getValue($node->var); 110 | $isInc ? ++$val : --$val; 111 | $var = $this->resolver->resolveVariable($node->var); 112 | $valNode = Utils::scalarToNode($val); 113 | $valRef = Utils::getvalueRef($valNode); 114 | $var->assignValue($this->resolver->getCurrentScope(), $valRef); 115 | return new Expr\Assign($node->var, $valNode); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Reducer/FuncCallReducer/FileSystemCall.php: -------------------------------------------------------------------------------- 1 | fileSystem = $fileSystem; 21 | } 22 | 23 | public function getSupportedNames() 24 | { 25 | return array( 26 | 'file_get_contents', 27 | 'file', 28 | 'fopen', 29 | 'fread', 30 | 'fwrite', 31 | 'fclose', 32 | ); 33 | } 34 | 35 | public function execute($name, array $args, FuncCall $node) 36 | { 37 | if (method_exists($this, $name . 'Prepare')) { 38 | $args = call_user_func(array($this, $name . 'Prepare'), $args, $node); 39 | } else { 40 | $args = Utils::refsToValues($args); 41 | } 42 | return call_user_func_array(array($this, $name), $args); 43 | } 44 | 45 | private function file_get_contents($filename, $flags = 0, $context = null, $offset = -1, $maxlen = -1) 46 | { 47 | if (Utils::safeFileExists($this->fileSystem, $filename)) { 48 | return Utils::scalarToNode($this->fileSystem->read($filename)); 49 | } 50 | return null; 51 | } 52 | 53 | private function file($filename, $flags = 0, $context = null) 54 | { 55 | if (Utils::safeFileExists($this->fileSystem, $filename)) { 56 | $content = $this->fileSystem->read($filename); 57 | $lines = preg_split("/(\r\n|\r|\n)/", $content); 58 | return Utils::scalarToNode($lines); 59 | } 60 | return null; 61 | } 62 | 63 | private function fopenPrepare(array $args, FuncCall $node) 64 | { 65 | return array_merge(array($node), Utils::refsToValues($args)); 66 | } 67 | 68 | private function fopen(FuncCall $node, $filename, $mode, $use_include_path = false, $context = null) 69 | { 70 | if (strpos($mode, 'r') !== false) { 71 | try { 72 | $stream = $this->fileSystem->readStream($filename); 73 | } catch (FileNotFoundException $e) { 74 | return; 75 | } 76 | } elseif (strpos($mode, 'w') !== false) { 77 | $stream = fopen('php://memory', 'w+b'); 78 | $this->fileSystem->writeStream($filename, $stream); 79 | } else { 80 | return; 81 | } 82 | $node->setAttribute(AttrName::VALUE, new ResourceValue($filename, $stream)); 83 | } 84 | 85 | private function firstArgIsResource(array $args) 86 | { 87 | $newArgs = array(); 88 | foreach ($args as $i => $arg) { 89 | if ($i == 0) { 90 | if (!($arg instanceof ResourceValue)) { 91 | throw new Exceptions\BadValueException("file handle is not a resource"); 92 | } 93 | if ($arg->isClosed()) { 94 | throw new Exceptions\BadValueException("file handle is closed"); 95 | } 96 | $newArgs[] = $arg; 97 | } else { 98 | $newArgs[] = $arg->getValue(); 99 | } 100 | } 101 | return $newArgs; 102 | } 103 | 104 | private function freadPrepare(array $args, FuncCall $node) 105 | { 106 | return $this->firstArgIsResource($args); 107 | } 108 | 109 | private function fread(ResourceValue $handle, $length) 110 | { 111 | return Utils::scalarToNode(fread($handle->getResource(), $length)); 112 | } 113 | 114 | private function fwritePrepare(array $args, FuncCall $node) 115 | { 116 | return $this->firstArgIsResource($args); 117 | } 118 | 119 | private function fwrite(ResourceValue $handle, $string, $length = null) 120 | { 121 | if ($length !== null) { 122 | fwrite($handle->getResource(), $string, $length); 123 | } else { 124 | fwrite($handle->getResource(), $string); 125 | } 126 | $this->fileSystem->writeStream($handle->getFilename(), $handle->getResource()); 127 | } 128 | 129 | private function fclosePrepare(array $args, FuncCall $node) 130 | { 131 | return $this->firstArgIsResource($args); 132 | } 133 | 134 | private function fclose(ResourceValue $handle) 135 | { 136 | fclose($handle->getResource()); 137 | $handle->close(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Reducer/BinaryOpReducer.php: -------------------------------------------------------------------------------- 1 | left instanceof String_ ? $node->left->getAttribute('kind', $dbl) : $dbl; 17 | $rkind = $node->right instanceof String_ ? $node->right->getAttribute('kind', $dbl) : $dbl; 18 | // Don't prefer single quotes 19 | if ($lkind === String_::KIND_SINGLE_QUOTED) { 20 | $kind = $rkind; 21 | } elseif ($rkind === String_::KIND_SINGLE_QUOTED) { 22 | $kind = $lkind; 23 | } else { 24 | $kind = $rkind; 25 | } 26 | $attrs['kind'] = $kind; 27 | } 28 | return Utils::scalarToNode($result, $attrs); 29 | } 30 | 31 | private function left(BinaryOp $node) 32 | { 33 | return Utils::getValue($node->left); 34 | } 35 | 36 | private function right(BinaryOp $node) 37 | { 38 | return Utils::getValue($node->right); 39 | } 40 | 41 | public function reduceBitwiseAnd(BinaryOp\BitwiseAnd $node) 42 | { return $this->postProcess($node, $this->left($node) & $this->right($node)); } 43 | 44 | public function reduceBitwiseOr(BinaryOp\BitwiseOr $node) 45 | { return $this->postProcess($node, $this->left($node) | $this->right($node)); } 46 | 47 | public function reduceBitwiseXor(BinaryOp\BitwiseXor $node) 48 | { return $this->postProcess($node, $this->left($node) ^ $this->right($node)); } 49 | 50 | public function reduceBooleanAnd(BinaryOp\BooleanAnd $node) 51 | { return $this->postProcess($node, $this->left($node) && $this->right($node)); } 52 | 53 | public function reduceBooleanOr(BinaryOp\BooleanOr $node) 54 | { return $this->postProcess($node, $this->left($node) || $this->right($node)); } 55 | 56 | public function reduceCoalesce(BinaryOp\Coalesce $node) 57 | { return $this->postProcess($node, $this->left($node) ?? $this->right($node)); } 58 | 59 | public function reduceConcat(BinaryOp\Concat $node) 60 | { 61 | $left = $this->left($node); 62 | $right = $this->right($node); 63 | if (is_array($left) || is_array($right)) { 64 | return null; 65 | } 66 | return $this->postProcess($node, $left . $right); 67 | } 68 | 69 | public function reduceDiv(BinaryOp\Div $node) 70 | { 71 | $left = $this->left($node); 72 | $right = $this->right($node); 73 | if ((float) $right == 0.0) { 74 | return null; 75 | } 76 | return $this->postProcess($node, $left / $right); 77 | } 78 | 79 | public function reduceEqual(BinaryOp\Equal $node) 80 | { return $this->postProcess($node, $this->left($node) == $this->right($node)); } 81 | 82 | public function reduceGreater(BinaryOp\Greater $node) 83 | { return $this->postProcess($node, $this->left($node) > $this->right($node)); } 84 | 85 | public function reduceGreaterOrEqual(BinaryOp\GreaterOrEqual $node) 86 | { return $this->postProcess($node, $this->left($node) >= $this->right($node)); } 87 | 88 | public function reduceIdentical(BinaryOp\Identical $node) 89 | { return $this->postProcess($node, $this->left($node) === $this->right($node)); } 90 | 91 | public function reduceLogicalAnd(BinaryOp\LogicalAnd $node) 92 | { return $this->postProcess($node, $this->left($node) and $this->right($node)); } 93 | 94 | public function reduceLogicalOr(BinaryOp\LogicalOr $node) 95 | { return $this->postProcess($node, $this->left($node) or $this->right($node)); } 96 | 97 | public function reduceLogicalXor(BinaryOp\LogicalXor $node) 98 | { return $this->postProcess($node, $this->left($node) xor $this->right($node)); } 99 | 100 | public function reduceMinus(BinaryOp\Minus $node) 101 | { return $this->postProcess($node, $this->left($node) - $this->right($node)); } 102 | 103 | public function reduceMod(BinaryOp\Mod $node) 104 | { return $this->postProcess($node, $this->left($node) % $this->right($node)); } 105 | 106 | public function reduceMul(BinaryOp\Mul $node) 107 | { return $this->postProcess($node, $this->left($node) * $this->right($node)); } 108 | 109 | public function reduceNotEqual(BinaryOp\NotEqual $node) 110 | { return $this->postProcess($node, $this->left($node) != $this->right($node)); } 111 | 112 | public function reduceNotIdentical(BinaryOp\NotIdentical $node) 113 | { return $this->postProcess($node, $this->left($node) !== $this->right($node)); } 114 | 115 | public function reducePlus(BinaryOp\Plus $node) 116 | { return $this->postProcess($node, $this->left($node) + $this->right($node)); } 117 | 118 | public function reducePow(BinaryOp\Pow $node) 119 | { return $this->postProcess($node, $this->left($node) ** $this->right($node)); } 120 | 121 | public function reduceShiftLeft(BinaryOp\ShiftLeft $node) 122 | { return $this->postProcess($node, $this->left($node) << $this->right($node)); } 123 | 124 | public function reduceShiftRight(BinaryOp\ShiftRight $node) 125 | { return $this->postProcess($node, $this->left($node) >> $this->right($node)); } 126 | 127 | public function reduceSmaller(BinaryOp\Smaller $node) 128 | { return $this->postProcess($node, $this->left($node) < $this->right($node)); } 129 | 130 | public function reduceSmallerOrEqual(BinaryOp\SmallerOrEqual $node) 131 | { return $this->postProcess($node, $this->left($node) <= $this->right($node)); } 132 | 133 | public function reduceSpaceship(BinaryOp\Spaceship $node) 134 | { return $this->postProcess($node, $this->left($node) <=> $this->right($node)); } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/Reducer/FuncCallReducer/MiscFunctions.php: -------------------------------------------------------------------------------- 1 | evalReducer = $evalReducer; 21 | $this->resolver = $resolver; 22 | } 23 | 24 | public function getSupportedNames() 25 | { 26 | return array( 27 | 'preg_replace', 28 | 'reset', 29 | 'create_function', 30 | ); 31 | } 32 | 33 | public function execute($name, array $args, FuncCall $node) 34 | { 35 | $args = Utils::refsToValues($args); 36 | switch ($name) { 37 | case 'preg_replace': 38 | return $this->safePregReplace($args[0], $args[1], $args[2]); 39 | case 'reset': 40 | // Pass by reference 41 | $arg = &$args[0]; 42 | return Utils::scalarToNode(reset($arg)); 43 | case 'create_function': 44 | return $this->createFunction($args[0], $args[1]); 45 | } 46 | } 47 | 48 | private function safePregReplace($pattern, $replacement, $subject) 49 | { 50 | preg_match('/((\W).*(?:\2|\}|\]|\>))([imsxeADSUXJu]*)/', $pattern, $patternMatch); 51 | if (!empty($patternMatch)) { 52 | $modifiers = $patternMatch[3]; 53 | if (strpos($modifiers, 'e') !== false) { 54 | $pattern = $patternMatch[1] . str_replace('e', '', $modifiers); 55 | // Try different strategies in order of preference 56 | // Clone the old scope so we can reset if it fails 57 | // TODO potential edge case where scope does not completely clone / reset properly 58 | // alternative is to not retry strategies but try them all in one pass 59 | $oldScope = $this->resolver->cloneScope(); 60 | $result = $this->evalPregReplace($pattern, $replacement, $subject); 61 | if ($result === null) { 62 | $this->resolver->resetScope($oldScope); 63 | $result = $this->obfuscatedEvalPregReplace($pattern, $replacement, $subject); 64 | } 65 | if ($result === null) { 66 | $this->resolver->resetScope($oldScope); 67 | $result = $this->fallbackEvalPregReplace($pattern, $replacement, $subject); 68 | } 69 | if ($result === null) { 70 | $this->resolver->resetScope($oldScope); 71 | } 72 | 73 | return $result; 74 | } 75 | } 76 | return Utils::scalarToNode(preg_replace($pattern, $replacement, $subject)); 77 | } 78 | 79 | private function evalPregReplace($pattern, $replacement, $subject) 80 | { 81 | $wasSuccessful = true; 82 | $result = preg_replace_callback($pattern, function($match) use ($replacement, &$wasSuccessful) { 83 | $rep = $replacement; 84 | for($i = 1; $i < count($match); $i++) { 85 | $rep = str_replace(array("\\{$i}", "\${$i}"), addslashes($match[$i]), $rep); 86 | } 87 | $expr = null; 88 | try { 89 | // Prepend "return" to force $rep to be an expression, throwing if not 90 | $stmts = $this->evalReducer->runEvalTree("return $rep ?>"); 91 | $expr = $stmts[0]->expr; 92 | } catch (\Exception $e) { 93 | } 94 | try { 95 | if ($expr !== null) { 96 | return Utils::getValue($expr); 97 | } 98 | } catch (Exceptions\BadValueException $e) { 99 | } 100 | $wasSuccessful = false; 101 | return ""; 102 | }, $subject); 103 | if ($wasSuccessful) { 104 | return Utils::scalarToNode($result); 105 | } 106 | } 107 | 108 | // A common obfuscation technique is to embed the payload in a preg_replace 109 | // Something like: preg_replace("/.*/e", "eval($payload)", ".") 110 | private function obfuscatedEvalPregReplace($pattern, $replacement, $subject) 111 | { 112 | if (preg_match($pattern, $subject, $match)) { 113 | // If the entire string matched, then it is equivalent to just running the replacement 114 | // expression on the subject (replacing group references as necessary) 115 | if ($match[0] == $subject) { 116 | for($i = 1; $i < count($match); $i++) { 117 | $replacement = str_replace(array("\\{$i}", "\${$i}"), addslashes($match[$i]), $replacement); 118 | } 119 | try { 120 | return $this->evalReducer->runEval("$replacement ?>"); 121 | } catch (\Exception $e) { 122 | } 123 | } 124 | } 125 | return null; 126 | } 127 | 128 | private function fallbackEvalPregReplace($pattern, $replacement, $subject) 129 | { 130 | // replace markers up to $100 131 | for($i = 1; $i < 100; $i++) { 132 | $replacement = str_replace(array("\\{$i}", "\${$i}"), "\$__preg_replace_match_result[$i]", $replacement); 133 | } 134 | try { 135 | $evalNode = $this->evalReducer->runEval("return $replacement ?>"); 136 | } catch (\Exception $e) { 137 | return null; 138 | } 139 | return new FuncCall(new Node\Name('preg_replace'), array( 140 | new Node\Arg(Utils::scalarToNode($pattern)), 141 | new Node\Arg($evalNode), 142 | new Node\Arg(Utils::scalarToNode($subject)) 143 | )); 144 | } 145 | 146 | private function createFunction($args, $code) 147 | { 148 | try { 149 | // Wrap it into a closure and return that closure 150 | $stmts = $this->evalReducer->runEvalTree("(function($args) { $code });"); 151 | return $stmts[0]; 152 | } catch (\Exception $e) { 153 | return null; 154 | } 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /src/MetadataVisitor.php: -------------------------------------------------------------------------------- 1 | key = $key; 18 | $this->_realClass = $realClass; 19 | } 20 | 21 | public function getSubNodeNames() : array 22 | { 23 | return ['key']; 24 | } 25 | 26 | public function getRealClass() 27 | { 28 | return $this->_realClass; 29 | } 30 | 31 | public function getType() : string 32 | { 33 | return 'FakeNode'; 34 | } 35 | 36 | } 37 | 38 | class FakeNode extends NodeAbstract 39 | { 40 | use FakeTrait; 41 | } 42 | 43 | // Keep types similar to real types 44 | 45 | class FakeNodeName extends Name 46 | { 47 | use FakeTrait; 48 | 49 | public function __construct($key, $realType) 50 | { 51 | parent::__construct($key); 52 | $this->key = $key; 53 | $this->_realType = $realType; 54 | } 55 | } 56 | 57 | class FakeNodeExpr extends Expr 58 | { 59 | use FakeTrait; 60 | } 61 | 62 | class FakeNodeStmt extends Stmt 63 | { 64 | use FakeTrait; 65 | } 66 | 67 | class FakeNodeVar extends Expr\Variable 68 | { 69 | use FakeTrait; 70 | 71 | public function __construct($key, $realType) 72 | { 73 | parent::__construct($key); 74 | $this->key = $key; 75 | $this->_realType = $realType; 76 | } 77 | } 78 | 79 | class FakePrinter extends ExtendedPrettyPrinter 80 | { 81 | public function printNode(Node $node) 82 | { 83 | // Don't call handleMagicTokens here - the tokens are needed for later 84 | if ($node instanceof Stmt) { 85 | return ltrim($this->pStmts([$node], false)); 86 | } 87 | return $this->p($node); 88 | } 89 | 90 | protected function pFakeNode(Node $node) 91 | { 92 | return "__NODE[{$node->key}]"; 93 | } 94 | 95 | // Copied from PrettyPrinterAbstract 96 | // Deals with FakeNode to get the real precedence 97 | protected function pPrec(Node $node, int $parentPrecedence, int $parentAssociativity, int $childPosition) : string { 98 | $class = \get_class($node); 99 | if ($node->getType() === 'FakeNode') { 100 | $class = $node->getRealClass(); 101 | } 102 | if (isset($this->precedenceMap[$class])) { 103 | $childPrecedence = $this->precedenceMap[$class][0]; 104 | if ($childPrecedence > $parentPrecedence 105 | || ($parentPrecedence === $childPrecedence && $parentAssociativity !== $childPosition) 106 | ) { 107 | return '(' . $this->p($node) . ')'; 108 | } 109 | } 110 | 111 | return $this->p($node); 112 | } 113 | } 114 | 115 | class MetadataVisitor extends \PhpParser\NodeVisitorAbstract 116 | { 117 | private $printer; 118 | private $nodeStack = array(); 119 | 120 | public function __construct(Deobfuscator $deobfuscator) 121 | { 122 | $this->printer = new FakePrinter(); 123 | } 124 | 125 | public function enterNode(Node $node) 126 | { 127 | $this->nodeStack[] = [$node, nodeChildren($node)]; 128 | } 129 | 130 | public function leaveNode(Node $newNode) 131 | { 132 | list($origNode, $origChildren) = array_pop($this->nodeStack); 133 | if (nodeChanged($origNode, $origChildren, $newNode)) { 134 | $this->processReduction($origNode, $origChildren, $newNode); 135 | } 136 | } 137 | 138 | private function processReduction($origNode, $origChildren, $newNode) 139 | { 140 | $p = $this->printer; 141 | $substituteNodes = []; 142 | $newNode->setAttribute('origClass', get_class($origNode)); 143 | $origCurrentChildren = []; 144 | 145 | $evalBlockReplace = $newNode instanceof EvalBlock && $newNode->origStmts !== null; 146 | $evalExprReplace = $origNode instanceof Expr\Eval_ && !($newNode instanceof EvalBlock); 147 | if ($evalBlockReplace) { 148 | // Our "original node" becomes an EvalBlock with the original statements 149 | // That node's original node is the original Eval(String) node 150 | $replacedNode = new EvalBlock($newNode->stmts, null); 151 | $this->processReduction($origNode, $origChildren, $replacedNode); 152 | $origChildren = ['stmts' => $newNode->origStmts]; 153 | $origNode = $replacedNode; 154 | } 155 | 156 | 157 | foreach ($origNode->getSubNodeNames() as $subName) { 158 | $childNode = $origNode->$subName; 159 | $origCurrentChildren[$subName] = $childNode; 160 | // Special case for FuncCallReducer 161 | if ($childNode instanceof Node && $childNode->hasAttribute('replaces')) { 162 | $repl = $childNode->getAttribute('replaces'); 163 | $this->processReduction($repl, nodeChildren($repl), $childNode); 164 | } 165 | $substituteNodes[$subName] = getSub($origChildren[$subName], $childNode); 166 | $origNode->$subName = fake($substituteNodes[$subName], $subName); 167 | } 168 | 169 | $oldStr = $p->printNode($origNode); 170 | $sections = [['str', $oldStr]]; 171 | foreach ($origNode->getSubNodeNames() as $subName) { 172 | $origNode->$subName = $origCurrentChildren[$subName]; 173 | replaceFake($substituteNodes[$subName], $sections, $subName, $p); 174 | } 175 | 176 | $flatter = flattenSections($sections); 177 | $newStr = $p->printNode($newNode); 178 | 179 | $replace = null; 180 | if ($evalBlockReplace) { 181 | $evalReduced = $origNode->getAttribute(AttrName::REDUCED_FROM); 182 | $replace = $evalReduced['O']; 183 | } 184 | if ($evalExprReplace) { 185 | $newReduced = $newNode->getAttribute(AttrName::REDUCED_FROM); 186 | $replace = $flatter; 187 | $flatter = $newReduced['O']; 188 | } 189 | 190 | $oldDerrived = stringifyFlat($flatter); 191 | 192 | // No changes to "new" in this round - just use previous round 193 | if ($oldDerrived === $newStr) { 194 | if ($replace !== null) { 195 | $newNode->setAttribute(AttrName::REDUCED_FROM, ['O' => $flatter, 'R' => $replace]); 196 | } else { 197 | // P = passthrough, always removed by the flattener 198 | $newNode->setAttribute(AttrName::REDUCED_FROM, ['P' => $flatter]); 199 | } 200 | return; 201 | } 202 | $oldObj = $flatter; 203 | if (count($flatter) === 1) { 204 | $oldObj = $flatter[0]; 205 | } 206 | $reduced = ['O' => $oldObj, 'N' => $newStr]; 207 | if ($replace !== null) { 208 | $reduced['R'] = $replace; 209 | } 210 | $newNode->setAttribute(AttrName::REDUCED_FROM, $reduced); 211 | } 212 | 213 | public function printFileReductions(array $stmts) 214 | { 215 | $p = $this->printer; 216 | $fileStr = $p->prettyPrintFile(fake($stmts, 'ROOT')); 217 | $sections = [['str', $fileStr]]; 218 | replaceFake($stmts, $sections, 'ROOT', $p); 219 | // XXX: fix indent token 220 | return _realFixIndent("", flattenSections($sections), 'INDENT_TOK', true); 221 | } 222 | 223 | } 224 | 225 | function stringifyFlat(array $flatter) 226 | { 227 | return implode('', array_map(function ($part) { 228 | if (is_array($part)) { 229 | if (isset($part['R']) && !isset($part['N'])) { 230 | if (is_array($part['O'])) { 231 | return stringifyFlat($part['O']); 232 | } 233 | return $part['O']; 234 | } 235 | return $part['N']; 236 | } 237 | return $part; 238 | }, $flatter)); 239 | } 240 | 241 | function getSub($origNode, $currNode) 242 | { 243 | // Prefer current node if it has a reduced from attr 244 | if ($currNode instanceof Node && $currNode->hasAttribute(AttrName::REDUCED_FROM)) { 245 | return $currNode; 246 | } elseif (is_array($currNode)) { 247 | $arr = []; 248 | foreach ($currNode as $i => $elem) { 249 | $arr[] = getSub($origNode[$i], $elem); 250 | } 251 | return $arr; 252 | } else { 253 | // Fallback to original node 254 | return $origNode; 255 | } 256 | } 257 | 258 | // Flatten the sections slightly 259 | function flattenSections($sections) 260 | { 261 | $flatter = []; 262 | $canAppend = false; 263 | foreach($sections as $section) { 264 | list($type, $value) = $section; 265 | if ($type === 'str' || $type === 'node') { 266 | if ($canAppend) { 267 | $flatter[count($flatter) - 1] .= $value; 268 | } else { 269 | $flatter[] = $value; 270 | $canAppend = true; 271 | } 272 | } elseif ($type == 'reducedNode') { 273 | // passthrough 274 | if (isset($value['P'])) { 275 | foreach($value['P'] as $toMerge) { 276 | if (gettype($toMerge) === 'string') { 277 | if ($canAppend) { 278 | $flatter[count($flatter) - 1] .= $toMerge; 279 | } else { 280 | $flatter[] = $toMerge; 281 | $canAppend = true; 282 | } 283 | } else { 284 | $flatter[] = $toMerge; 285 | $canAppend = false; 286 | } 287 | } 288 | } else { 289 | $flatter[] = $value; 290 | $canAppend = false; 291 | } 292 | } 293 | } 294 | return $flatter; 295 | } 296 | 297 | function replaceFake($val, &$sections, $name, $p) 298 | { 299 | if (is_array($val)) { 300 | foreach($val as $i => $elem) { 301 | replaceFake($elem, $sections, "{$name}[{$i}]", $p); 302 | } 303 | } elseif ($val instanceof Node && !($val instanceof Node\Scalar\EncapsedStringPart)) { 304 | processSubstitutions($sections, $name, $val, $p); 305 | } 306 | 307 | } 308 | 309 | function fake($val, $name) 310 | { 311 | if (is_array($val)) { 312 | $fakeArr = []; 313 | foreach($val as $i => $elem) { 314 | $fakeArr[] = fake($elem, "{$name}[{$i}]"); 315 | } 316 | return $fakeArr; 317 | } elseif ($val instanceof Name) { 318 | return new FakeNodeName($name, $val->getAttribute('origClass')); 319 | } elseif ($val instanceof Node\Scalar\EncapsedStringPart) { 320 | return $val; 321 | } elseif ($val instanceof Expr\Variable) { 322 | return new FakeNodeVar($name, $val->getAttribute('origClass')); 323 | } elseif ($val instanceof Expr) { 324 | return new FakeNodeExpr($name, $val->getAttribute('origClass')); 325 | } elseif ($val instanceof Stmt) { 326 | return new FakeNodeStmt($name, $val->getAttribute('origClass')); 327 | } elseif ($val instanceof Node) { 328 | return new FakeNode($name, $val->getAttribute('origClass')); 329 | } 330 | return $val; 331 | } 332 | 333 | function valIdentifier($value) 334 | { 335 | if (gettype($value) === 'object') { 336 | return spl_object_hash($value); 337 | } 338 | if (is_array($value)) { 339 | return array_map('valIdentifier', $value); 340 | } 341 | return $value; 342 | } 343 | 344 | function nodeChildren(Node $node) 345 | { 346 | $originalChildren = []; 347 | foreach($node->getSubNodeNames() as $subName) { 348 | $originalChildren[$subName] = $node->$subName; 349 | } 350 | return $originalChildren; 351 | } 352 | 353 | function nodeChanged(Node $oldNode, array $oldChildren, Node $newNode) { 354 | if ($oldNode !== $newNode) { 355 | return true; 356 | } 357 | if(nodeChildren($newNode) !== $oldChildren) { 358 | return true; 359 | } 360 | foreach ($oldChildren as $name => $childNode) { 361 | if (childIsReduced($childNode)) { 362 | return true; 363 | } 364 | } 365 | return false; 366 | } 367 | 368 | function childIsReduced($childNode) 369 | { 370 | if (is_array($childNode)) { 371 | foreach ($childNode as $elem) { 372 | if (childIsReduced($elem)) { 373 | return true; 374 | } 375 | } 376 | return false; 377 | } 378 | if ($childNode instanceof Node) { 379 | return $childNode->hasAttribute(AttrName::REDUCED_FROM); 380 | } 381 | return false; 382 | } 383 | 384 | function getIndent($str) 385 | { 386 | $lastLinePos = strrpos($str, "\n", -1); 387 | if ($lastLinePos === false) { 388 | $lastLinePos = 0; 389 | } else { 390 | $lastLinePos += 1; 391 | } 392 | $indentSize = strspn($str, " ", $lastLinePos); 393 | // Have spaces on line but there's stuff after it 394 | if ($indentSize + $lastLinePos != strlen($str)) { 395 | return 0; 396 | } 397 | return $indentSize; 398 | } 399 | 400 | function fixIdent($indent, $value, $noIndentToken) 401 | { 402 | if ($indent === 0) { 403 | return $value; 404 | } 405 | return _fixNodeIndent(str_repeat(" ", $indent), $value, $noIndentToken); 406 | } 407 | 408 | function _fixNodeIndent($indent, $value, $noIndentToken, $stripNoIndent = false) 409 | { 410 | if (is_array($value)) { 411 | $rebuilt = []; 412 | foreach($value as $key => $subVal) { 413 | $rebuilt[$key] = _realFixIndent($indent, $subVal, $noIndentToken, $stripNoIndent); 414 | } 415 | return $rebuilt; 416 | } 417 | return _realFixIndent($indent, $value, $noIndentToken, $stripNoIndent); 418 | } 419 | 420 | function _realFixIndent($indent, $value, $noIndentToken, $stripNoIndent = false) 421 | { 422 | if (is_array($value)) { 423 | return array_map(function($entry) use ($indent, $noIndentToken, $stripNoIndent) { 424 | return _fixNodeIndent($indent, $entry, $noIndentToken, $stripNoIndent); 425 | }, $value); 426 | } 427 | $ret = preg_replace('/\n(?!$|' . $noIndentToken . ')/', "\n$indent", $value); 428 | if ($stripNoIndent) { 429 | $ret = str_replace($noIndentToken, '', $ret); 430 | } 431 | return $ret; 432 | } 433 | 434 | function processSubstitutions(array &$sections, $nodeName, Node $node, FakePrinter $p) 435 | { 436 | $newSections = []; 437 | foreach ($sections as $section) { 438 | list($type, $value) = $section; 439 | if ($type === 'str') { 440 | $split = explode("__NODE[$nodeName]", $value); 441 | $c = count($split); 442 | if ($c != 1) { 443 | for ($i = 0; $i < $c; $i++) { 444 | $s = $split[$i]; 445 | // Skip over empty strings 446 | if ($s !== '') { 447 | $newSections[] = ['str', $s]; 448 | } 449 | // If not last section 450 | if ($i != $c - 1) { 451 | $indent = getIndent($s); 452 | if ($node->hasAttribute(AttrName::REDUCED_FROM)) { 453 | $reducedFrom = $node->getAttribute(AttrName::REDUCED_FROM); 454 | // XXX: Fix indent token 455 | $newSections[] = ['reducedNode', fixIdent($indent, $reducedFrom, 'INDENT_TOK')]; 456 | } else { 457 | // This could just be 'str' but don't bother 458 | // running replacements on it so make it a different type 459 | // XXX: fix indent token 460 | $newSections[] = ['node', fixIdent($indent, $p->printNode($node), 'INDENT_TOK')]; 461 | } 462 | } 463 | } 464 | continue; 465 | } 466 | } 467 | $newSections[] = $section; 468 | } 469 | $sections = $newSections; 470 | } 471 | 472 | -------------------------------------------------------------------------------- /src/ControlFlowVisitor.php: -------------------------------------------------------------------------------- 1 | counter = 0; 19 | $this->blockStack = array(); 20 | $this->blocks = array(); 21 | $this->currentBlock = $this->root = $root; 22 | $root->setDefined(); 23 | $this->blockStack[] = $root; 24 | } 25 | 26 | public function getBlock($label) 27 | { 28 | if (!array_key_exists($label, $this->blocks)) { 29 | $this->blocks[$label] = new CodeBlock($label); 30 | } 31 | return $this->blocks[$label]; 32 | } 33 | 34 | private function mkname($key) 35 | { 36 | return $this->currentBlock->name . $key . $this->counter++; 37 | } 38 | 39 | public function makeNested($name) 40 | { 41 | $this->blockStack[] = $this->currentBlock; 42 | $nested = $this->getBlock($this->mkname($name)); 43 | $nested->setDefined(); 44 | $this->currentBlock->addNested($nested); 45 | $this->currentBlock = $nested; 46 | return $nested; 47 | } 48 | 49 | public function popStack() 50 | { 51 | $this->currentBlock = array_pop($this->blockStack); 52 | } 53 | 54 | public function getBlocks() 55 | { 56 | return $this->blocks; 57 | } 58 | } 59 | 60 | class ControlFlowVisitor extends \PhpParser\NodeVisitorAbstract 61 | { 62 | private $scope; 63 | private $scopeStack = array(); 64 | private $nestedTypes = array(); 65 | 66 | public function __construct() 67 | { 68 | $this->defineNested('If_', 'stmts', array('elseifs', 'else')); 69 | $this->defineNested('ElseIf_', 'stmts'); 70 | $this->defineNested('Else_', 'stmts'); 71 | 72 | $this->defineNested('Switch_', 'cases'); 73 | $this->defineNested('Case_', 'stmts'); 74 | 75 | $this->defineNested('TryCatch', 'stmts', array('catches', 'finally')); 76 | $this->defineNested('Catch_', 'stmts'); 77 | $this->defineNested('Finally_', 'stmts'); 78 | 79 | $this->defineNested('For_', 'stmts'); 80 | $this->defineNested('Foreach_', 'stmts'); 81 | $this->defineNested('Do_', 'stmts'); 82 | $this->defineNested('While_', 'stmts'); 83 | 84 | $this->defineNested('Function_', 'stmts', array(), true); 85 | $this->defineNested('Trait_', 'stmts'); 86 | $this->defineNested('Class_', 'stmts'); 87 | $this->defineNested('ClassMethod', 'stmts', array(), true); 88 | $this->defineNested('Interface_', 'stmts'); 89 | $this->defineNested('Namespace_', 'stmts'); 90 | 91 | // Special case: Closure is an expression not a statement 92 | $this->nestedTypes[Node\Expr\Closure::class] = array('stmts', '_Closure', array(), true); 93 | } 94 | 95 | private function defineNested($class, $stmtAttr, array $subNodes = array(), $changeScope = false) 96 | { 97 | $this->nestedTypes[Stmt::class . '\\' . $class] = array($stmtAttr, '_' . $class, $subNodes, $changeScope); 98 | } 99 | 100 | public function beforeTraverse(array $nodes) 101 | { 102 | $this->scope = new LabelScope(new CodeBlock('