├── .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 => "\n\$f = fopen(__FILE__, 'r');\n\$str = fread(\$f, 200);\nlist(,, \$payload) = explode('", 1 => "', \$str);\neval(\$payload . '');\n", 2 => "\nif (\$doBadThing) {\n evil_payload();\n}\n");
84 | eval /* PHPDeobfuscator eval output */ {
85 | if ($doBadThing) {
86 | evil_payload();
87 | }
88 | };
89 | ?>
90 | if ($doBadThing) {
91 | evil_payload();
92 | }
93 | ```
94 |
95 | #### Input
96 | ```php
97 | getFilesystem()->write($virtualPath, $code);
14 | $deobf->setCurrentFilename($virtualPath);
15 | $tree = $deobf->parse($code);
16 | $tree = $deobf->deobfuscate($tree);
17 | $newCode = $deobf->prettyPrint($tree);
18 | return array($tree, $newCode);
19 | }
20 |
21 | $nodeDumper = new PhpParser\NodeDumper();
22 | if (php_sapi_name() == 'cli') {
23 | $opts = getopt('tof:');
24 | if (!isset($opts['f'])) {
25 | die("Missing required parameter -f\n");
26 | }
27 | $filename = $opts['f'];
28 | $orig = isset($opts['o']);
29 | list($tree, $code) = deobfuscate(file_get_contents($filename), $filename, $orig);
30 | echo $code, "\n";
31 | if (isset($opts['t'])) {
32 | echo $nodeDumper->dump($tree), "\n";
33 | }
34 | } else {
35 | if (isset($_POST['phpdata'])) {
36 | $orig = array_key_exists('orig', $_GET);
37 | $php = $_POST['phpdata'];
38 | header('Content-Type: text/plain');
39 | list($tree, $code) = deobfuscate($php, 'input.php', $orig);
40 | echo $code, "\n\n";
41 | if (array_key_exists('tree', $_GET)) {
42 | echo '======== Tree =======', "\n";
43 | echo $nodeDumper->dump($tree), "\n";
44 | }
45 | } else {
46 | echo <<
48 |
49 |
54 |
55 |
56 | HTML;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/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 */
87 | if (substr($code, 0, 2) == '?>' && $code[2] != '<') {
88 | $code[0] = '<';
89 | $code[1] = '?';
90 | }
91 | $prefix = substr($code, 0, 2) == '' ? '' : 'deobfuscator->parse("{$prefix}{$code}");
93 | }
94 |
95 | private function deobfTree($tree)
96 | {
97 | return $this->deobfuscator->deobfuscate($tree);
98 | }
99 |
100 | public function runEvalTree($code)
101 | {
102 | return $this->deobfTree($this->parseCode($code));
103 | }
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------