├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── composer.json ├── index.php ├── src ├── AddOriginalVisitor.php ├── AttrName.php ├── ControlFlowVisitor.php ├── Deobfuscator.php ├── EvalBlock.php ├── Exceptions │ ├── BadValueException.php │ ├── MutableValueException.php │ └── UnknownValueException.php ├── ExtendedPrettyPrinter.php ├── MaybeStmtArray.php ├── MetadataVisitor.php ├── Reducer.php ├── Reducer │ ├── AbstractReducer.php │ ├── BinaryOpReducer.php │ ├── EvalReducer.php │ ├── FuncCallReducer.php │ ├── FuncCallReducer │ │ ├── FileSystemCall.php │ │ ├── FunctionReducer.php │ │ ├── FunctionSandbox.php │ │ ├── MiscFunctions.php │ │ └── PassThrough.php │ ├── MagicReducer.php │ ├── MiscReducer.php │ └── UnaryReducer.php ├── ReducerVisitor.php ├── ResolveValueVisitor.php ├── Resolver.php ├── Scope.php ├── Utils.php ├── ValRef.php ├── ValRef │ ├── AbstractValRef.php │ ├── ArrayVal.php │ ├── ByReference.php │ ├── GlobalVarArray.php │ ├── ObjectVal.php │ ├── ResourceValue.php │ ├── ScalarValue.php │ └── UnknownValRef.php ├── VarRef.php └── VarRef │ ├── ArrayAccessVariable.php │ ├── FutureVarRef.php │ ├── ListVarRef.php │ ├── LiteralName.php │ ├── PropertyAccessVariable.php │ └── UnknownVarRef.php ├── test.php └── tests ├── filesystem.txt ├── goto-tests.txt ├── reducers.txt └── variables.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Composer 2 | /vendor/ 3 | /composer.lock 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-cli-buster 2 | 3 | RUN apt-get update && apt-get install -y git 4 | 5 | RUN curl -s https://getcomposer.org/installer | php && mv composer.phar /usr/local/bin/composer 6 | 7 | COPY . /app 8 | WORKDIR /app 9 | 10 | RUN composer install 11 | 12 | CMD [ "php", "index.php" ] -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 => " "', \$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 | 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 |
50 | 51 |
52 | 53 |
54 | 55 | 56 | HTML; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/AddOriginalVisitor.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/AttrName.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('
')); 103 | $this->scopeStack = array(); 104 | $nodes[] = new Stmt\Return_(null, array( 105 | 'comments' => array(new \PhpParser\Comment('// [PHPDeobfuscator] Implied script end')), 106 | 'impliedReturn' => true 107 | )); 108 | return $nodes; 109 | } 110 | 111 | public function enterNode(Node $node) 112 | { 113 | if ($node instanceof Stmt\Label) { 114 | $block = $this->scope->getBlock($node->name->name); 115 | $block->setDefined(); 116 | if ($this->scope->currentBlock->isEmpty()) { 117 | $this->scope->currentBlock->setAlias($block); 118 | } else { 119 | // Add explicit goto from the implicit fall through 120 | $this->scope->currentBlock->append(new Stmt\Goto_($node->name, array( 121 | 'comments' => array(new \PhpParser\Comment('// [PHPDeobfuscator] Implied goto')) 122 | ))); 123 | $this->moveInto($block); 124 | } 125 | $this->scope->currentBlock = $block->root(); 126 | } 127 | // Stmt\Label done before attaching node 128 | $ignore = $node instanceof Stmt\Else_ 129 | || $node instanceof Stmt\ElseIf_ 130 | || $node instanceof Stmt\Catch_ 131 | || $node instanceof Stmt\Finally_; 132 | if (!$ignore) { 133 | $this->scope->currentBlock->append($node); 134 | } 135 | 136 | // Unwrap to get the expression node 137 | if ($node instanceof Stmt\Expression) { 138 | $node = $node->expr; 139 | } 140 | 141 | $className = get_class($node); 142 | 143 | if (array_key_exists($className, $this->nestedTypes)) { 144 | list($stmtAttr, $name, $subNodes, $changeScope) = $this->nestedTypes[$className]; 145 | return $this->nestedNode($node, $name, $stmtAttr, $subNodes, $changeScope); 146 | } 147 | 148 | if ($node instanceof Stmt\Goto_) { 149 | $block = $this->scope->getBlock($node->name->name); 150 | $this->moveInto($block); 151 | } 152 | 153 | // Nodes that signal unreachability 154 | if ($node instanceof Stmt\Goto_ 155 | || $node instanceof Node\Expr\Exit_ 156 | || $node instanceof Stmt\Return_ 157 | || $node instanceof Stmt\Continue_ 158 | || $node instanceof Stmt\Break_ 159 | || $node instanceof Stmt\Throw_ 160 | || $node instanceof Stmt\HaltCompiler 161 | ) { 162 | $this->scope->currentBlock->setUnreachable(); 163 | } 164 | return \PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN; 165 | } 166 | 167 | private function nestedNode(Node $node, $name, $stmtAttr, array $subNodes = array(), $changeScope = false) 168 | { 169 | $nested = $this->scope->makeNested($name); 170 | if ($changeScope) { 171 | if (count($subNodes) !== 0) { 172 | throw new \Exception("Cannot have sub nodes on scope changing node"); 173 | } 174 | $this->scopeStack[] = $this->scope; 175 | $this->scope = new LabelScope($nested); 176 | $node->setAttribute('changeScope', true); 177 | } else { 178 | $node->setAttribute('buildLater', $nested); 179 | $node->setAttribute('subNodes', $subNodes); 180 | } 181 | $node->setAttribute('stmtAttr', $stmtAttr); 182 | $node->setAttribute('isNested', true); 183 | return new WrappedNode($node, array_merge(array($stmtAttr), $subNodes)); 184 | } 185 | 186 | private function moveInto(CodeBlock $to) 187 | { 188 | $from = $this->scope->currentBlock; 189 | // Only move if the from is reachable, otherwise $to will have a ghost enter point 190 | if (!$from->isUnreachable()) { 191 | $from->setExit($to); 192 | $to->enter($from); 193 | } 194 | } 195 | 196 | public function leaveNode(Node $node) 197 | { 198 | if ($node instanceof WrappedNode) { 199 | $node = $node->unwrap(); 200 | } 201 | if ($node instanceof Stmt\Function_ 202 | || $node instanceof Stmt\ClassMethod 203 | || $node instanceof Node\Expr\Closure) { 204 | // Add implied return 205 | $this->scope->currentBlock->append(new Stmt\Return_(null, array( 206 | 'comments' => array(new \PhpParser\Comment('// [PHPDeobfuscator] Implied return')), 207 | 'impliedReturn' => true 208 | ))); 209 | } 210 | if ($node->getAttribute('changeScope') === true) { 211 | $stmtAttr = $node->getAttribute('stmtAttr'); 212 | $node->$stmtAttr = $this->rebuildAndTrim(); 213 | $this->scope = array_pop($this->scopeStack); 214 | } 215 | if ($node->getAttribute('isNested') === true) { 216 | $this->scope->popStack(); 217 | } 218 | return $node; 219 | } 220 | 221 | private function rebuildAndTrim() 222 | { 223 | $nodes = $this->scope->root->root()->rebuild(); 224 | while ($nodes && $nodes[count($nodes) - 1]->hasAttribute('impliedReturn')) { 225 | array_pop($nodes); 226 | } 227 | return $nodes; 228 | } 229 | 230 | public function afterTraverse(array $nodes) 231 | { 232 | return $this->rebuildAndTrim(); 233 | } 234 | 235 | } 236 | 237 | class WrappedNode implements Node 238 | { 239 | public function __construct(Node $node, array $subNodes) 240 | { 241 | $this->node = $node; 242 | $this->subNodes = $subNodes; 243 | foreach ($subNodes as $name) { 244 | $this->$name = $node->$name; 245 | } 246 | } 247 | 248 | public function getSubNodeNames() : array { return $this->subNodes; } 249 | 250 | public function unwrap() { return $this->node; } 251 | 252 | public function getType() : string { return $this->node->getType(); } 253 | 254 | public function getLine() : int { return $this->node->getLine(); } 255 | 256 | public function getStartLine() : int { return $this->node->getStartLine(); } 257 | 258 | public function getEndLine() : int { return $this->node->getEndLine(); } 259 | 260 | public function getStartTokenPos() : int { return $this->node->getStartTokenPos(); } 261 | 262 | public function getEndTokenPos() : int { return $this->node->getEndTokenPos(); } 263 | 264 | public function getStartFilePos() : int { return $this->node->getStartFilePos(); } 265 | 266 | public function getEndFilePos() : int { return $this->node->getEndFilePos(); } 267 | 268 | public function getComments() : array { return $this->node->getComments(); } 269 | 270 | public function getDocComment() { return $this->node->getDocComment(); } 271 | 272 | public function setDocComment(\PhpParser\Comment\Doc $docComment) { $this->node->setDocComment($docComment); } 273 | 274 | public function setAttribute(string $key, $value) { $this->node->setAttribute($key, $value); } 275 | 276 | public function hasAttribute(string $key) : bool { return $this->node->hasAttribute($key); } 277 | 278 | public function getAttribute(string $key, $default = null) { return $this->node->getAttribute($key, $default); } 279 | 280 | public function getAttributes() : array { return $this->node->getAttributes(); } 281 | 282 | public function setAttributes(array $attributes) { $this->node->setAttributes($attributes); } 283 | } 284 | 285 | class CodeBlock 286 | { 287 | public $name; 288 | private $exitBlock; 289 | // Blocks which enter our block (indexed by their name) 290 | private $enters = array(); 291 | private $nested = array(); 292 | private $unreachable; 293 | private $nodes = array(); 294 | private $unreachableNodes = array(); 295 | private $built = false; 296 | private $aliasOf; 297 | // For every Label we possess, keep track of the number of enters using this name 298 | private $entersPerName = array(); 299 | // Map the name of an enter block (in $enters) to the name we saw it enter by 300 | private $enterByName = array(); 301 | private $exitOrigName; 302 | private $defined = false; 303 | 304 | public function __construct($name) 305 | { 306 | $this->name = $name; 307 | $this->entersPerName[$name] = 0; 308 | } 309 | 310 | public function rebuild($removeLabel = null) 311 | { 312 | $this->checkAlias(); 313 | if ($this->built || !$this->defined) { 314 | return array(); 315 | } 316 | $this->built = true; 317 | $nodes = array(); 318 | $removeGotoExit = false; 319 | if ($this->exitBlock !== null) { 320 | // Remove the jump to our exit block if it's not been built yet 321 | $removeGotoExit = !$this->exitBlock->built && $this->exitBlock->defined; 322 | // If our exit block has only one entrance (i.e. us), then skip outputting its label 323 | $removeExitLabel = $removeGotoExit && $this->exitBlock->entersPerName[$this->exitOrigName] === 1; 324 | $nodes = $this->exitBlock->rebuild($removeExitLabel ? $this->exitOrigName : null); 325 | 326 | // Add these nodes back on so we don't loose code when a block is undefined 327 | if (!$this->exitBlock->defined) { 328 | $this->nodes = array_merge($this->nodes, $this->unreachableNodes); 329 | } 330 | } 331 | $nodes = array_merge(array_filter(array_map(array($this, 'processNode'), $this->nodes), 332 | function($node) use ($removeLabel, $removeGotoExit) { 333 | if ($node instanceof Stmt\Label) { 334 | // If no enters to label or label is $removeLabel, remove. 335 | return $this->entersPerName[$node->name->name] !== 0 && (!$removeLabel || $node->name->name !== $removeLabel); 336 | } 337 | if ($removeGotoExit && $node instanceof Stmt\Goto_) { 338 | return $node->name->name !== $this->exitOrigName; 339 | } 340 | return true; 341 | }), $nodes); 342 | return $nodes; 343 | } 344 | 345 | private function processNode(Node $node) 346 | { 347 | $subBlock = $node->getAttribute('buildLater'); 348 | if ($subBlock !== null) { 349 | $stmtAttr = $node->getAttribute('stmtAttr'); 350 | $node->$stmtAttr = $subBlock->rebuild(); 351 | $subNodes = $node->getAttribute('subNodes'); 352 | foreach ($subNodes as $name) { 353 | if (is_array($node->$name)) { 354 | $node->$name = array_map(array($this, 'processNode'), $node->$name); 355 | } elseif (!is_null($node->$name)) { 356 | $node->$name = $this->processNode($node->$name); 357 | } 358 | } 359 | } 360 | return $node; 361 | } 362 | 363 | public function setAlias(CodeBlock $other) 364 | { 365 | if ($this->aliasOf) { 366 | throw new \Exception("Block {$this->name} already has alias of {$this->aliasOf->name}, tried to set alias to {$other->name}"); 367 | } 368 | if ($this->exitBlock) { 369 | throw new \Exception("Cannot alias if block exits somewhere. Tried to set alias {$other->name} but {$this->name} is exiting at {$this->exitBlock->name}"); 370 | } 371 | if ($this->nested) { 372 | throw new \Exception("Cannot alias if block has nested blocks (block {$this->name})"); 373 | } 374 | if (!$this->isEmpty()) { 375 | throw new \Exception("Cannot alias if block has nodes (block {$this->name})"); 376 | } 377 | $this->aliasOf = $other; 378 | // Anywhere that enters our block will now enter the alias 379 | foreach ($this->enters as $block) { 380 | // It must enter the alias under the name it entered us by, to propagate 381 | // the referenced label name forward 382 | $other->enter($block, $this->enterByName[$block->name]); 383 | $block->exitBlock = $other; 384 | } 385 | $this->enters = array(); // ensure unreachable 386 | $this->enterByName = array(); 387 | if ($this->nodes) { 388 | // Put our label(s) on the other block 389 | $other->nodes = array_merge($this->nodes, $other->nodes); 390 | } 391 | // Any unreferenced aliased names that we picked up should now be picked 392 | // up by the other block (referenced names already handled above) 393 | foreach (array_keys($this->entersPerName) as $ourName) { 394 | if (!array_key_exists($ourName, $other->entersPerName)) { 395 | $other->entersPerName[$ourName] = 0; 396 | } 397 | } 398 | $this->setUnreachable(); 399 | } 400 | 401 | public function isEmpty() 402 | { 403 | // if just labels, then empty 404 | foreach ($this->nodes as $node) { 405 | if (!($node instanceof Stmt\Label)) { 406 | return false; 407 | } 408 | } 409 | return true; 410 | } 411 | 412 | public function setUnreachable() 413 | { 414 | $this->unreachable = true; 415 | } 416 | 417 | public function setDefined() 418 | { 419 | $this->defined = true; 420 | } 421 | 422 | private function checkAlias() 423 | { 424 | if ($this->aliasOf) { 425 | throw new \Exception("Block {$this->name} is alias!"); 426 | } 427 | } 428 | 429 | public function root() 430 | { 431 | $block = $this; 432 | while ($block->aliasOf) { 433 | $block = $block->aliasOf; 434 | } 435 | return $block; 436 | } 437 | 438 | public function append(Node $node) 439 | { 440 | $this->checkAlias(); 441 | if ($this->unreachable) { 442 | if ($this->exitBlock && !$this->exitBlock->defined) { 443 | $this->unreachableNodes[] = $node; 444 | } 445 | return; 446 | } 447 | $this->nodes[] = $node; 448 | } 449 | 450 | public function isUnreachable() 451 | { 452 | return $this->unreachable; 453 | } 454 | 455 | public function setExit(CodeBlock $block) 456 | { 457 | $this->checkAlias(); 458 | $this->exitOrigName = $block->name; 459 | // If our exit block will be an alias, exit to the destination 460 | while ($block->aliasOf) { 461 | $block = $block->aliasOf; 462 | } 463 | if ($this->exitBlock !== null) { 464 | throw new \Exception("Cannot have multiple exits! Was '{$this->exitBlock->name}', attempted exit to '{$block->name}'"); 465 | } 466 | $this->exitBlock = $block; 467 | } 468 | 469 | // $block enters our block, optionally using an alternate name 470 | public function enter(CodeBlock $block, $targetName = null) 471 | { 472 | $targetName = $targetName ?: $this->name; 473 | if ($this->aliasOf) { 474 | $this->aliasOf->enter($block, $targetName); 475 | return; 476 | } 477 | $this->enters[$block->name] = $block; 478 | $this->enterByName[$block->name] = $targetName; 479 | // Increment the refcount of the effective name that entered us 480 | if (!isset($this->entersPerName[$targetName])) { 481 | $this->entersPerName[$targetName] = 0; 482 | } 483 | $this->entersPerName[$targetName]++; 484 | } 485 | 486 | public function addNested(CodeBlock $inner) 487 | { 488 | $this->checkAlias(); 489 | $this->nested[] = $inner; 490 | } 491 | 492 | } 493 | -------------------------------------------------------------------------------- /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/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/Exceptions/BadValueException.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/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 | -------------------------------------------------------------------------------- /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/Reducer.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/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/EvalReducer.php: -------------------------------------------------------------------------------- 1 | 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 ' && $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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/FuncCallReducer/FunctionReducer.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/Reducer/FuncCallReducer/PassThrough.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/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 | -------------------------------------------------------------------------------- /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/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/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/Resolver.php: -------------------------------------------------------------------------------- 1 | scope = null; 33 | $this->newScope('global'); 34 | $this->scope->setSuperGlobal('GLOBALS', new GlobalVarArray($this)); 35 | $this->globalScope = $this->scope; 36 | $this->nameScope = array( 37 | 'function' => '', 38 | 'namespace' => '', 39 | 'class' => '', 40 | 'method' => '', 41 | 'trait' => '' 42 | ); 43 | $this->constants = array( 44 | 'PHP_EOL' => "\n" 45 | ); 46 | } 47 | 48 | public function enterNode(Node $node) 49 | { 50 | if ($node->getAttribute('enterMutableContext')) { 51 | $this->setCurrentVarsMutable(); 52 | } 53 | $this->updateNameScope($node, true); 54 | if ($this->changesScope($node)) { 55 | $this->newScope($this->nameForScope($node)); 56 | // Inherit variables from the use clause 57 | if ($node instanceof Expr\Closure) { 58 | foreach ($node->uses as $use) { 59 | $var = new LiteralName($use->var->name); 60 | $parentScope = $this->scope->getParent(); 61 | if ($use->byRef) { 62 | $val = new ByReference($var, $parentScope); 63 | } else { 64 | $val = $var->getValue($parentScope); 65 | } 66 | // Only assign if variable is known 67 | if ($val !== null) { 68 | $var->assignValue($this->scope, $val); 69 | } 70 | } 71 | } 72 | } 73 | // Transform AssignOp into the longer form BinaryOp 74 | if ($node instanceof Expr\AssignOp) { 75 | $op = str_replace('AssignOp', 'BinaryOp', get_class($node)); 76 | return new Expr\Assign($node->var, new $op($node->var, $node->expr)); 77 | } 78 | 79 | if ($node instanceof Stmt\For_) { 80 | // Everything except the init expression 81 | $this->setNodesInMutableContext($node->cond); 82 | $this->setNodesInMutableContext($node->loop); 83 | $this->setNodesInMutableContext($node->stmts); 84 | } 85 | if ($node instanceof Stmt\Foreach_) { 86 | $this->setNodesInMutableContext($node->stmts); 87 | } 88 | if ($node instanceof Stmt\While_ 89 | || $node instanceof Stmt\Do_ 90 | || $node instanceof Stmt\Case_ 91 | || $node instanceof Stmt\Label) { 92 | $this->setCurrentVarsMutable(); 93 | } 94 | } 95 | 96 | private function setNodesInMutableContext(array $nodes) 97 | { 98 | foreach ($nodes as $node) { 99 | $node->setAttribute('enterMutableContext', true); 100 | return; 101 | } 102 | } 103 | 104 | private function nodeCanBranch(Node $node) 105 | { 106 | return $node instanceof Stmt\If_ 107 | || $node instanceof Stmt\For_ 108 | || $node instanceof Stmt\Foreach_ 109 | || $node instanceof Stmt\While_ 110 | || $node instanceof Stmt\Do_ 111 | || $node instanceof Stmt\Switch_; 112 | } 113 | 114 | public function leaveNode(Node $node) 115 | { 116 | $retNode = null; 117 | $this->updateNameScope($node, false); 118 | if ($this->changesScope($node)) { 119 | $this->leaveScope(); 120 | } 121 | if ($node instanceof Expr\Assign) { 122 | $this->onAssign($node); 123 | // Try to transform BinaryOp back into AssignOp 124 | if ($node->expr instanceof Expr\BinaryOp) { 125 | $op = str_replace('BinaryOp', 'AssignOp', get_class($node->expr)); 126 | if (class_exists($op)) { 127 | $varRef = $this->resolveVariable($node->var); 128 | $leftVar = $this->resolveVariable($node->expr->left); 129 | $isVarRef = !($leftVar instanceof UnknownVarRef) || !$leftVar->notAVarRef(); 130 | // If they are the same reference then we can combine 131 | if ($isVarRef && $varRef == $leftVar) { 132 | $retNode = new $op($node->expr->left, $node->expr->right); 133 | } 134 | } 135 | } 136 | } 137 | if ($node instanceof Expr\AssignRef) { 138 | $this->onAssignRef($node); 139 | } 140 | if ($node instanceof Stmt\Unset_) { 141 | $this->onUnset($node); 142 | } 143 | if ($node instanceof Stmt\Global_) { 144 | foreach ($node->vars as $var) { 145 | $var = $this->resolveVariable($var); 146 | $val = $var->getValue($this->getGlobalScope()); 147 | $this->assign($var, $val); 148 | } 149 | } 150 | if ($node instanceof Expr\FuncCall) { 151 | $this->onFuncCall($node); 152 | } 153 | if ($this->nodeCanBranch($node)) { 154 | $this->setCurrentVarsMutable(); 155 | } 156 | return $retNode; 157 | } 158 | 159 | private function setCurrentVarsMutable() 160 | { 161 | foreach ($this->scope->getVariables() as $var) { 162 | $var->setMutable(true); 163 | } 164 | } 165 | 166 | private function changesScope(Node $node) 167 | { 168 | return $node instanceof Stmt\Function_ 169 | || $node instanceof Stmt\ClassMethod 170 | || $node instanceof Expr\Closure; 171 | } 172 | 173 | private function nameForScope(Node $node) 174 | { 175 | if ($node instanceof Stmt\Function_ || $node instanceof Stmt\ClassMethod) { 176 | return $this->nameScope['function']; 177 | } 178 | if ($node instanceof Expr\Closure) { 179 | return 'closure'; 180 | } 181 | return $node->getType(); 182 | } 183 | 184 | private function newScope($name) 185 | { 186 | $this->scope = new Scope($name, $this->scope); 187 | } 188 | 189 | private function updateNameScope(Node $node, $isEnter) 190 | { 191 | $key = null; 192 | if ($node instanceof Stmt\Namespace_) { 193 | $key = 'namespace'; 194 | } elseif ($node instanceof Stmt\Function_) { 195 | $key = 'function'; 196 | } elseif ($node instanceof Stmt\Class_) { 197 | $key = 'class'; 198 | } elseif ($node instanceof Stmt\ClassMethod) { 199 | $key = 'method'; 200 | } elseif ($node instanceof Stmt\Trait_) { 201 | $key = 'trait'; 202 | } else { 203 | return; 204 | } 205 | if ($isEnter) { 206 | // name is either Name or Identifier, both have toString 207 | $name = $node->name ? $node->name->toString() : ''; 208 | if ($key == 'method') { 209 | // function is set to the name of the method 210 | $this->nameScope['function'] = $name; 211 | $parentName = $this->nameScope['class'] . $this->nameScope['trait']; 212 | if ($parentName) { 213 | $name = $parentName . '::' . $name; 214 | } 215 | } 216 | // If we've entered into a trait, the class can't be known 217 | if ($key == 'trait') { 218 | $this->nameScope['class'] = null; 219 | } 220 | if (in_array($key, array('class', 'trait', 'function')) && $this->nameScope['namespace']) { 221 | $name = $this->nameScope['namespace'] . '\\' . $name; 222 | } 223 | if ($key == 'function') { 224 | $this->nameScope['method'] = $name; 225 | } 226 | $this->nameScope[$key] = $name; 227 | } else { 228 | $this->nameScope[$key] = ''; 229 | if ($key == 'method') { 230 | $this->nameScope['function'] = ''; 231 | } 232 | if ($key == 'function') { 233 | $this->nameScope['method'] = ''; 234 | } 235 | if ($key == 'trait') { 236 | $this->nameScope['class'] = ''; 237 | } 238 | } 239 | } 240 | 241 | private function leaveScope() 242 | { 243 | $this->scope = $this->scope->getParent(); 244 | } 245 | 246 | public function getConstant($name) 247 | { 248 | if (isset($this->constants[$name])) { 249 | return new ScalarValue($this->constants[$name]); 250 | } 251 | // PHP assumes a string of the name of the constant 252 | return new ScalarValue($name); 253 | } 254 | 255 | public function getCurrentScope() 256 | { 257 | return $this->scope; 258 | } 259 | 260 | public function getGlobalScope() 261 | { 262 | return $this->globalScope; 263 | } 264 | 265 | public function cloneScope() 266 | { 267 | // TODO nameScope and constants 268 | return clone $this->scope; 269 | } 270 | 271 | public function resetScope(Scope $scope) 272 | { 273 | $this->scope = clone $scope; 274 | // Reset globalScope to ensure correct reference 275 | do { 276 | $this->globalScope = $scope; 277 | $scope = $scope->getParent(); 278 | } while ($scope != null); 279 | } 280 | 281 | public function currentClass() 282 | { 283 | return $this->nameScope['class']; 284 | } 285 | 286 | public function currentFunction() 287 | { 288 | return $this->nameScope['function']; 289 | } 290 | 291 | public function currentMethod() 292 | { 293 | return $this->nameScope['method']; 294 | } 295 | 296 | public function currentNamespace() 297 | { 298 | return $this->nameScope['namespace']; 299 | } 300 | 301 | public function currentTrait() 302 | { 303 | return $this->nameScope['trait']; 304 | } 305 | 306 | private function onFuncCall(Expr\FuncCall $expr) 307 | { 308 | $name = null; 309 | if ($expr->name instanceof Node\Name) { 310 | $name = $expr->name->toString(); 311 | } else { 312 | $nameRef = $this->resolveValue($expr->name); 313 | if ($nameRef !== null && !$nameRef->isMutable()) { 314 | $name = $nameRef->getValue(); 315 | } 316 | } 317 | if ($name === null) { 318 | return; 319 | } 320 | $argCount = count($expr->args); 321 | // Should set vars mutable here if name is null due to the chance that 322 | // it's really parse_str or extract, but that's unlikely so don't ruin all variables 323 | // for something very unlikely 324 | switch ($name) { 325 | case 'parse_str': 326 | if ($argCount > 1) { 327 | break; 328 | } 329 | case 'extract': 330 | $this->setCurrentVarsMutable(); 331 | break; 332 | case 'define': 333 | if ($argCount >= 2) { 334 | $this->onDefine($expr->args[0]->value, $expr->args[1]->value); 335 | } 336 | break; 337 | } 338 | } 339 | 340 | private function onDefine(Expr $name, Expr $value) 341 | { 342 | $nameRef = $this->resolveValue($name); 343 | if ($nameRef === null || $nameRef->isMutable()) { 344 | return; 345 | } 346 | $valRef = $this->resolveValue($value); 347 | if ($valRef === null || $valRef->isMutable() || !($valRef instanceof ScalarValue)) { 348 | return; 349 | } 350 | $constName = $nameRef->getValue(); 351 | if (array_key_exists($constName, $this->constants)) { 352 | return; // PHP won't override existing constants 353 | } 354 | $this->constants[$constName] = $valRef->getValue(); 355 | } 356 | 357 | private function onAssign(Expr\Assign $expr) 358 | { 359 | $varRef = $this->resolveVariable($expr->var); 360 | $valRef = $this->resolveValue($expr->expr); 361 | $this->assign($varRef, $valRef); 362 | } 363 | 364 | private function onAssignRef(Expr\AssignRef $expr) 365 | { 366 | $var = $this->resolveVariable($expr->var); 367 | $ref = $this->resolveVariable($expr->expr); 368 | if (!($ref instanceof UnknownVarRef) || !$ref->notAVarRef()) { 369 | $val = new ByReference($ref, $this->scope); 370 | } else { 371 | // Possible assignment to a non-variable - just a normal assignment 372 | $val = $this->resolveValue($expr->expr); 373 | } 374 | $this->assign($var, $val); 375 | } 376 | 377 | private function assign(VarRef $var, ValRef $val = null) 378 | { 379 | $didAssign = false; 380 | if ($val !== null) { 381 | while (($oldValue = $var->getValue($this->scope)) instanceof ByReference) { 382 | $var = $oldValue->getVariable(); 383 | } 384 | $didAssign = $var->assignValue($this->scope, $val); 385 | } 386 | if (!$didAssign) { 387 | if ($var instanceof UnknownVarRef) { 388 | if ($var->getContext() === null) { 389 | // If this was an unknown variable assignment with no parent context, all bets are off 390 | $this->setCurrentVarsMutable(); 391 | } else { 392 | // Otherwise, only the parent needs to be set mutable 393 | $var = $var->getContext(); 394 | } 395 | } 396 | if ($var instanceof ListVarRef) { 397 | foreach ($var->getVars() as $listVar) { 398 | if ($listVar === null) { 399 | continue; 400 | } 401 | $oldValue = $listVar->getValue($this->scope); 402 | if ($oldValue !== null) { 403 | $oldValue->setMutable(true); 404 | } 405 | } 406 | } else { 407 | $oldValue = $var->getValue($this->scope); 408 | if ($oldValue !== null) { 409 | $oldValue->setMutable(true); 410 | } 411 | } 412 | } 413 | } 414 | 415 | private function onUnset(Stmt\Unset_ $stmt) 416 | { 417 | foreach ($stmt->vars as $expr) { 418 | $var = $this->resolveVariable($expr); 419 | $var->unsetVar($this->scope); 420 | } 421 | } 422 | 423 | private function resolveValue(Expr $expr, $tryUnknownVar = false) 424 | { 425 | try { 426 | return Utils::getValueRef($expr); 427 | } catch (Exceptions\UnknownValueException $e) { 428 | if ($tryUnknownVar) { 429 | return $this->resolveVariable($expr)->getValue($this->scope); 430 | } 431 | return null; 432 | } 433 | } 434 | 435 | // See FutureVarRef for why $tryUnknownVar is needed 436 | public function resolveVariable(Expr $var, $tryUnknownVar = false) 437 | { 438 | if ($var instanceof Expr\Variable) { 439 | $varName = $var->name; 440 | if (is_string($varName)) { 441 | return new LiteralName($varName); 442 | } else { 443 | $nameRef = $this->resolveValue($varName, $tryUnknownVar); 444 | if ($nameRef !== null && !$nameRef->isMutable()) { 445 | // Replace name in tree 446 | $var->name = $nameRef->getValue(); 447 | return new LiteralName($nameRef->getValue()); 448 | } 449 | return UnknownVarRef::$ANY; 450 | } 451 | } elseif ($var instanceof Expr\List_) { 452 | $vars = array(); 453 | foreach ($var->items as $item) { 454 | if ($item === null) { 455 | $vars[] = null; 456 | continue; 457 | } 458 | if($item->key !== null || $item->byRef) { 459 | throw new \Exception("Don't know how to handle element in list()"); 460 | } 461 | $varExpr = $item->value; 462 | $varRef = $this->resolveVariable($varExpr, $tryUnknownVar); 463 | if ($varRef instanceof UnknownVarRef) { 464 | $varRef = new FutureVarRef($varExpr, $this); 465 | } 466 | $vars[] = $varRef; 467 | } 468 | return new ListVarRef($vars); 469 | } elseif ($var instanceof Expr\ArrayDimFetch) { 470 | $arrVar = $this->resolveVariable($var->var, $tryUnknownVar); 471 | if ($arrVar instanceof UnknownVarRef) { 472 | return $arrVar; 473 | } 474 | if ($var->dim === null) { // e.g. $arr[] = 1; 475 | $dim = new ScalarValue(null); 476 | } else { 477 | $dim = $this->resolveValue($var->dim, $tryUnknownVar); 478 | } 479 | if ($dim !== null && !$dim->isMutable()) { 480 | return new ArrayAccessVariable($arrVar, $dim->getValue()); 481 | } 482 | return new UnknownVarRef($arrVar); 483 | } elseif ($var instanceof Expr\PropertyFetch) { 484 | $objVar = $this->resolveVariable($var->var, $tryUnknownVar); 485 | if ($objVar instanceof UnknownVarRef) { 486 | return $objVar; 487 | } 488 | if ($var->name instanceof Expr) { 489 | $nameVal = $this->resolveValue($var->name, $tryUnknownVar); 490 | if ($nameVal !== null && !$nameVal->isMutable()) { 491 | $name = $nameVal->getValue(); 492 | } else { 493 | $name = null; 494 | } 495 | } else { 496 | $name = $var->name->name; 497 | } 498 | if ($name !== null) { 499 | return new PropertyAccessVariable($objVar, $name); 500 | } 501 | return new UnknownVarRef($objVar); 502 | } elseif ($var instanceof Expr\StaticPropertyFetch) { 503 | // TODO 504 | } 505 | return UnknownVarRef::$NOT_A_VAR_REF; 506 | } 507 | } 508 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Utils.php: -------------------------------------------------------------------------------- 1 | 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 | -------------------------------------------------------------------------------- /src/ValRef.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/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 | -------------------------------------------------------------------------------- /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/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/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/ResourceValue.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/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/ValRef/UnknownValRef.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/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/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/LiteralName.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/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/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 | -------------------------------------------------------------------------------- /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 = "', $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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------