diff --git a/README.md b/README.md index c67e5073..d1185027 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ One target per run, selected with `-t`: | `sojson` | sojson | | `sojsonv7` | sojson v7 | | `obfuscator` | [javascript-obfuscator](https://github.com/javascript-obfuscator/javascript-obfuscator) (obfuscator.io) | +| `obfuscatorx` | the same obfuscator, version-aware — see below for how it differs | | `jsconfuser` | [JS-Confuser](https://github.com/MichaelXF/js-confuser) | ### `obfuscator` @@ -24,6 +25,25 @@ One target per run, selected with `-t`: * transformer (ObjectExpression, SplitString, and etc.) * customCode (self-defending, debug-protection, console-output) +### `obfuscatorx` + +The same encoder as `obfuscator`, decoded era by era rather than against one shape. It is an +**additional** target, not a replacement: `obfuscator` is widely depended on and is left untouched, +and the two are expected to disagree on some samples. + +Two differences worth knowing before choosing between them: + +* **It declines rather than half-decoding.** Where a string-array layer is present and cannot be + read, this entry returns nothing and says why, instead of emitting a partly-resolved program. + A layer it does not own is not that case — it returns the partial decode so the output can be + fed to another target, and logs what it left behind. +* **It reports the encoder era.** The version range is derived from the emitted shape and printed + after the decode. Emitted output can only identify a *range*, never an exact version, and a + sample carrying no evidence on an axis is reported as such rather than guessed at. + +Coverage is **non-contiguous**: the 2.x eras and the pinned 5.5.0, with 3.0.0–4.2.2 unverified. +A sample whose range overlaps that gap is reported as unknown rather than treated as covered. + ### `jsconfuser` Covers JS-Confuser 2.x up to and including the `high` preset. Which transforms are diff --git a/package.json b/package.json index 080132f5..d01d7350 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "scripts": { "decode": "node src/main.js", "deob": "node src/main.js -t obfuscator", + "deobx": "node src/main.js -t obfuscatorx", "dejsc": "node src/main.js -t jsconfuser", "deso": "node src/main.js -t sojson", "desov7": "node src/main.js -t sojsonv7", diff --git a/src/main.js b/src/main.js index b71ebb4f..59501be5 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import PluginJsconfuser from './plugin/jsconfuser.js' import PluginSojson from './plugin/sojson.js' import PluginSojsonV7 from './plugin/sojsonv7.js' import PluginObfuscator from './plugin/obfuscator.js' +import PluginObfuscatorX from './plugin/obfuscatorx.js' import PluginAwsc from './plugin/awsc.js' import logger from './utility/logger.js' @@ -37,6 +38,7 @@ const plugins = { sojson: PluginSojson, sojsonv7: PluginSojsonV7, obfuscator: PluginObfuscator, + obfuscatorx: PluginObfuscatorX, awsc: PluginAwsc, } diff --git a/src/plugin/obfuscatorx.js b/src/plugin/obfuscatorx.js new file mode 100644 index 00000000..dc7bbfe8 --- /dev/null +++ b/src/plugin/obfuscatorx.js @@ -0,0 +1,133 @@ +import { parse } from '@babel/parser' +import generator from '@babel/generator' +import traverse from '@babel/traverse' + +import logger from '../utility/logger.js' +import calculateConstantExp from '../visitor/calculate-constant-exp.js' +import deleteExtra from '../visitor/delete-extra.js' +import pruneIfBranch from '../visitor/prune-if-branch.js' +import parseControlFlowStorage from '../visitor/parse-control-flow-storage.js' +import normalizeStatements from '../visitor/obfuscator/normalize-statements.js' +import decodeStringArray from '../visitor/obfuscator/string-array.js' +import normalizeConverting from '../visitor/obfuscator/normalize-converting.js' +import { createUnflattenSwitchDispatch } from '../visitor/obfuscator/unflatten-switch-dispatch.js' +import unlockEnv from '../visitor/obfuscator/unlock-env.js' +import { reportEra } from '../visitor/obfuscator/report.js' + +/** + * A version-aware entry for javascript-obfuscator output. + * + * **Additive, and the existing `obfuscator` entry is left untouched.** That one is widely depended + * on, so changing it in place risks breaking people relying on its behaviour; this is a second + * target rather than a replacement, and the two are expected to disagree. + * + * **No version in the name, deliberately.** Precedent puts the encoder's version in the suffix + * (`sojson`/`sojsonv7`), but this entry's coverage is non-contiguous and growing - the phase-1 2.x + * eras plus the 5.5.0 pin, with 3.0.0-4.2.2 unverified - so any single version in the name would + * misdescribe it. + * + * **The pipeline is era-invariant**: the same passes in the same order for every era. Era knowledge + * lives in the detector, which matches shape-first and *outputs* an era rather than taking one as + * input, so there are no per-era strategies here and nothing for a registry to key. + */ + +/** + * The fixpoint group. Storage inlining re-opens Converting work that has already reported clean, so + * these are one group repeated rather than a line run once - a cycle in the dependency order, which + * a re-cut of pass boundaries cannot remove. + * + * `prune-if-branch` is load-bearing three times over: it is the un-flattener's precondition, since + * dead-code injection copies flattened blocks into scopes that do not hold their controller + * storage; it is the whole of the dead-code reversal; and it keeps donated helper clones out of the + * anti-tamper strip's way. + */ +function runGroup(ast, maxRounds = 8) { + let previous = null + let rounds = 0 + for (let round = 0; round < maxRounds; round++) { + rounds = round + 1 + normalizeConverting(ast) + traverse(ast, calculateConstantExp) + traverse(ast, parseControlFlowStorage) + traverse(ast, calculateConstantExp) + traverse(ast, pruneIfBranch) + traverse( + ast, + createUnflattenSwitchDispatch(() => {}), + ) + const current = generator(ast, { compact: true }).code + if (current === previous) break + previous = current + } + return rounds +} + +export default function (code) { + let ast + try { + ast = parse(code, { errorRecovery: true, allowReturnOutsideFunction: true }) + } catch (e) { + logger.error( + `[obfuscatorx] cannot parse input: ${e.reasonCode ?? e.message}`, + ) + return null + } + + // Normalization first: the encoder's Simplifying stage packs statement-level control flow into + // operators, and every matcher below navigates by statement boundaries. + normalizeStatements(ast) + + const sa = decodeStringArray(ast) + + // **Refusal is narrow and means one thing: a layer that is mine, which I could not read.** + // Returning falsy is the only signal a plugin has, so it is spent on the case where the output + // would otherwise be silently half-decoded. The diagnostic is the point - a silent fallthrough is + // what makes the existing entry's failures unreadable. + if (sa.status === 'unreadable') { + logger.error( + '[obfuscatorx] refusing: a javascript-obfuscator string-array layer is present and could ' + + 'not be read, so decoding would emit a half-resolved program.', + ) + for (const note of sa.notes) logger.error(`[obfuscatorx] ${note}`) + return null + } + + // `absent` is not a failure - a sample built with `stringArray: false` carries every other + // transform - and `unowned` is success plus residue, so both fall through to the rest of the + // pipeline. Only the log distinguishes them. + if (sa.status === 'unowned') { + logger.log( + '[obfuscatorx] a string-array layer is present that this entry does not own — decoding ' + + "this entry's own layers and leaving it in place.", + ) + for (const note of sa.notes) logger.log(`[obfuscatorx] ${note}`) + } + + const rounds = runGroup(ast) + // The anti-tamper strip runs last, outside the group: its matchers need the string array decoded + // and the control-flow storage inlined, and inside-versus-after was measured byte-identical, so + // after wins on cost. + unlockEnv(ast) + + logger.debugLog(`[obfuscatorx] fixpoint settled in ${rounds} round(s)`) + + // Reporting only. An unrecognised signature yields `unknown` and never blocks a decode whose + // entrypoint resolved, so this runs after the work rather than gating it. + reportEra(sa.signature) + + // `EscapeSequenceTransformer` rewrites only a literal's `raw` spelling, so the parsed VALUE is + // already what we want and there is no shape to match - discarding `extra` is the whole reversal, + // exactly as it is for numbers. It runs *last*, and print-time is the honest slot: the option + // named after it (`unicodeEscapeSequence`) does not gate the transformer at all, it only widens + // which characters are escaped, so every sample carries this whatever it was built with, and no + // matcher above navigates by `raw`. Scheduling it earlier would also re-spell the source that + // `decodeStringArray` hands to its isolate, for no gain. + traverse(ast, deleteExtra) + + // **A truthy return no longer implies fully decoded**, which is a deliberate weakening of the + // contract taken in exchange for chainability: a foreign residual layer returns the partial + // decode so the caller can feed it to the next target, and the log is the only place that + // difference is visible. + return generator(ast, { comments: false, jsescOption: { minimal: true } }) + .code +} diff --git a/src/utility/logger.js b/src/utility/logger.js index 48ea863e..59b45471 100644 --- a/src/utility/logger.js +++ b/src/utility/logger.js @@ -39,8 +39,29 @@ function debugLog(...args) { } } +/** + * Always-on channels, for the two things a caller cannot act on if they are hidden behind `-v`. + * + * `debugLog` above is per-pass tracing and is correctly off by default. A **refusal** is not + * tracing: a plugin that returns falsy has told the caller only that it failed, so the reason has + * to reach them or the failure is unreadable — which is exactly the weakness of a silent + * fallthrough. A **verdict** the entry was asked to produce is likewise not tracing. + * + * Routed through here rather than written as bare `console.error` at each site so that the channel + * stays one thing a caller can redirect or silence, instead of several. + */ +function log(...args) { + console.log(...args) +} + +function error(...args) { + console.error(...args) +} + export default { debugLog, setDebugLogging, isDebugLogging, + log, + error, } diff --git a/src/visitor/atomic/README.md b/src/visitor/atomic/README.md new file mode 100644 index 00000000..4ec8973c --- /dev/null +++ b/src/visitor/atomic/README.md @@ -0,0 +1,48 @@ +# `visitor/atomic/` + +Single-rewrite, plugin-agnostic Babel visitors. One file does one thing to one node shape, so a +plugin composes the ones it needs instead of inheriting a bundle. + +Each file exports: + +- a **default** plain visitor object, for `traverse(ast, visitorDefault)`; +- a named **`create…(onChange)`** factory, for a caller that needs to know whether the visitor + fired — which a fixpoint loop does, since it has to decide whether to run another round. + +## What belongs here + +A visitor that is **not specific to any obfuscator**: it recognises a JavaScript shape and +rewrites it to an equivalent one. If a rewrite needs to know which encoder produced the input, +it belongs in that plugin's own folder (`visitor/obfuscator/`, `visitor/jsconfuser/`) instead. + +**A readability rewrite qualifies only if a later matcher navigates by the shape it produces.** The +test is "does something downstream read this", never "is it prettier": folding `!![]` to `true` +earns its place because a branch spelled as a unary chain cannot be pruned, so the fold is what +lets the pruning and un-flattening passes see it. The same test excludes turning concatenation back +into a template literal — nothing downstream reads template literals, so it is style, and it is +deliberately unbuilt. + +Scheduling does not belong here either. Which visitors run, in what order, and how many times is +a property of the pipeline that consumes them — see `visitor/obfuscator/normalize-statements.js` +for a worked example, where the order within a round is load-bearing and the loop runs to a +fixpoint because the rewrites unlock each other. + +## Two rules every visitor here follows + +- **Gate on position, and decline rather than stop.** Several of these rewrites are only valid + where the node is a *statement* rather than a *value*; the parent test is the whole safety + argument and each file states its own. A node that fails the gate is skipped and traversal + continues. Never `path.stop()` — it halts the entire traversal, not the subtree, and on + obfuscated input the declined sites outnumber the matched ones by roughly two to one, so + stopping at the first would abort the pass before it did any work. +- **Say what is *not* handled, and why.** `lint-logical-if.js` declining `||`, and + `convert-conditional-assign.js` declining a `VariableDeclarator`, are decisions with reasons — + recorded so the next reader does not "fix" them. + +## Relationship to the visitors in `visitor/` + +The flat files one level up (`lint-if-statement.js`, `split-sequence.js`, +`split-variable-declaration.js`, `delete-extra.js`, …) are atoms by the same definition and +predate this folder. They are **not** moved, because every existing plugin imports them by path +and relocating them would touch code this work is meant to leave alone. Migrating them is a +worthwhile refactor on its own terms and belongs in its own commit, not smuggled into a feature. diff --git a/src/visitor/atomic/collapse-property-shorthand.js b/src/visitor/atomic/collapse-property-shorthand.js new file mode 100644 index 00000000..ea8fedd7 --- /dev/null +++ b/src/visitor/atomic/collapse-property-shorthand.js @@ -0,0 +1,55 @@ +import * as t from '@babel/types' + +/** + * Collapse a property whose key and value are the same name back to shorthand: + * + * const { foo: foo } = bar; => const { foo } = bar; + * ({ foo: foo }) => ({ foo }) + * + * Obfuscators expand shorthand so that a renaming pass has two nodes where the source had one - + * in `{ foo }` the single token is both the property read and the binding declared, and a + * renamer must rewrite the binding while leaving the property name alone. Once renaming has + * happened the expanded form carries no information; it is just longer. + * + * **One exclusion, and it is the reverse of the usual `__proto__` trap.** In an + * *ObjectExpression*, `{ __proto__: x }` sets the prototype while the shorthand `{ __proto__ }` + * merely defines an own property - so collapsing it changes meaning. The special case is scoped + * to the `PropertyName : AssignmentExpression` form, which is exactly what collapsing removes. + * Verified by construction rather than from the spec: `Object.getPrototypeOf` reports the + * prototype set for the expanded form and not for the shorthand. + * + * In an *ObjectPattern* there is no such hazard - destructuring `{ __proto__: __proto__ }` and + * `{ __proto__ }` both bind the name - but the gate does not need to distinguish them, because + * refusing the name in both positions costs one unreachable collapse and removes a class of + * error entirely. + * + * **`export { foo as foo }` is deliberately not handled here.** It parses to the *same* AST as + * `export { foo }` - Babel represents both as an `ExportSpecifier` whose `local` and `exported` + * names match - so there is no node to rewrite, and the generator already prints the short form. + * That reversal is free at generation time and needs no pass. + */ +export function createCollapsePropertyShorthand(onChange) { + return { + ObjectProperty(path) { + const { node } = path + if (node.computed || node.shorthand) { + return + } + if (!t.isIdentifier(node.key) || !t.isIdentifier(node.value)) { + return + } + if (node.key.name !== node.value.name) { + return + } + if (node.key.name === '__proto__') { + return + } + node.shorthand = true + if (onChange) { + onChange() + } + }, + } +} + +export default createCollapsePropertyShorthand() diff --git a/src/visitor/atomic/convert-conditional-assign.js b/src/visitor/atomic/convert-conditional-assign.js new file mode 100644 index 00000000..4f6ab7bc --- /dev/null +++ b/src/visitor/atomic/convert-conditional-assign.js @@ -0,0 +1,52 @@ +import * as t from '@babel/types' + +/** + * Distribute an assignment into both branches of a conditional: + * + * r = t ? a : b => t ? r = a : r = b + * + * On its own this is a wash. Its purpose is positional: a conditional in *value* position + * cannot become an `if` statement, and this moves it into statement position where it can. + * So it is a prerequisite for conditional-to-if conversion rather than a simplification in + * its own right, and it is worth nothing unless that conversion runs after it. + * + * Only an `AssignmentExpression` parent is distributed. A `VariableDeclarator` + * (`var r = t ? a : b`) is deliberately left alone: making that convertible means hoisting + * the declaration out of its initializer, which changes where the binding is introduced. + * That is a scope decision, not this visitor's. + * + * The assignment target is cloned into each branch rather than shared, so the two branches + * do not alias one node. + * + * **Not safe for every target.** A target with its own side effects or an unstable value - + * `obj[i++] = t ? a : b` - would have that effect duplicated into both branches. The guard is + * that only one branch ever executes, so the effect still happens exactly once; what changes + * is that it is now evaluated *after* the test rather than before it. Where the test reads + * what the target expression writes, that is observable. + */ +export function createConvertConditionalAssign(onConvert) { + return { + ConditionalExpression: { + exit(path) { + const parent = path.parent + if (!t.isAssignmentExpression(parent) || parent.right !== path.node) { + return + } + const { test, consequent, alternate } = path.node + const { operator, left } = parent + path.parentPath.replaceWith( + t.conditionalExpression( + test, + t.assignmentExpression(operator, t.cloneNode(left), consequent), + t.assignmentExpression(operator, t.cloneNode(left), alternate), + ), + ) + if (onConvert) { + onConvert() + } + }, + }, + } +} + +export default createConvertConditionalAssign() diff --git a/src/visitor/atomic/lint-conditional-if.js b/src/visitor/atomic/lint-conditional-if.js new file mode 100644 index 00000000..027f3495 --- /dev/null +++ b/src/visitor/atomic/lint-conditional-if.js @@ -0,0 +1,96 @@ +import * as t from '@babel/types' + +/** + * Put a conditional used as a statement back into an `if`: + * + * test ? a : b; => if (test) a; else b; + * return test ? a : b; => if (test) return a; else return b; + * + * Obfuscators use the ternary to pack a two-branch `if` onto one line. Both forms restore a + * statement boundary, and each branch keeps the statement kind of the site it replaces - + * which is what makes the return form safe, since two `return`s preserve the function's + * completion where two expression statements would silently drop it. + * + * **The position gate is the whole safety argument**, and it matters more here than for the + * logical form because conditionals in value position are extremely common. The node must be + * the *entire* expression of an `ExpressionStatement`, or the *entire* argument of a + * `ReturnStatement`. Everything else is a value: a declarator initializer, a call argument, + * an operand, an arrow's concise body, a property value. Measured over one 432-sample corpus + * of obfuscated output, this gate matched 435 sites and declined 895 value-position + * conditionals; without it every one of those 895 would have been rewritten wrongly. + * + * A declined site is skipped and traversal continues. Nothing here stops the traversal - on + * obfuscated input the first declined site typically arrives long before the first match, so + * halting would abort the pass before it did any work. + * + * **A conditional in value position is often one rewrite away from being convertible.** See + * `convert-conditional-assign.js`, which distributes an assignment into both branches to move + * the conditional into statement position; run it before this one to reach those sites. + */ +export function createLintConditionalIf(onReverse) { + // Whether this traversal rewrote anything, so `Program.exit` knows if a crawl is owed. Closure + // state rather than module-level, since each factory call is its own visitor instance. + let rewroteSomething = false + return { + // The crawl restores an invariant this rewrite breaks: after a pass, the scope information + // Babel has cached should equal what a fresh parse of that pass's own output would produce. + // `replaceWith` is handed an `IfStatement` built from the conditional's own `test`, + // `consequent` and `alternate`, and it registers those reused subtrees' references a second + // time - the same live node listed twice in `binding.referencePaths`, with nothing detached. + // Measured on one real sample: 14 duplicate entries, 1462 recorded references against 1448 + // real ones. A later consumer gating a deletion on "have I resolved every reference" then + // passes that check while a live reference goes unhandled. + // + // Program-scoped and once per traversal: crawling a narrower scope *adds* duplicates, by + // appending to outer-scope bindings that already hold those references. Gated because a crawl + // is only owed when something moved, and it cannot change the tree. + Program: { + enter() { + rewroteSomething = false + }, + exit(path) { + if (rewroteSomething) path.scope.crawl() + }, + }, + ConditionalExpression: { + exit(path) { + const { test, consequent, alternate } = path.node + const parent = path.parentPath + + if ( + parent.isExpressionStatement() && + parent.node.expression === path.node + ) { + parent.replaceWith( + t.ifStatement( + test, + t.expressionStatement(consequent), + t.expressionStatement(alternate), + ), + ) + rewroteSomething = true + if (onReverse) { + onReverse() + } + return + } + + if (parent.isReturnStatement() && parent.node.argument === path.node) { + parent.replaceWith( + t.ifStatement( + test, + t.returnStatement(consequent), + t.returnStatement(alternate), + ), + ) + rewroteSomething = true + if (onReverse) { + onReverse() + } + } + }, + }, + } +} + +export default createLintConditionalIf() diff --git a/src/visitor/atomic/lint-logical-if.js b/src/visitor/atomic/lint-logical-if.js new file mode 100644 index 00000000..38d7c3d8 --- /dev/null +++ b/src/visitor/atomic/lint-logical-if.js @@ -0,0 +1,54 @@ +import * as t from '@babel/types' + +/** + * Put a short-circuit used as a statement back into an `if`: + * + * test && body; => if (test) body; + * + * Obfuscators use this to pack a branch onto one line. It is semantics-preserving in both + * directions at statement level, where the operator's value is discarded either way, and it + * restores a statement boundary that every statement-level matcher navigates by. + * + * **The position gate is the whole safety argument.** The parent must be the + * `ExpressionStatement` and the node must be its entire expression. That single test excludes + * every position where the operator is carrying a value rather than a branch - an `if`, + * `while` or `for` test, an arrow function's concise body, a declarator initializer, a call + * argument, an operand of an enclosing operator. A site that fails the gate is left alone and + * traversal continues; nothing here stops the traversal, because on obfuscated input the + * declined sites vastly outnumber the matched ones and halting on the first would abort the + * pass before it normalized anything. + * + * **Only the outermost `&&` of a chain qualifies**, and that is what makes a chain come out + * right rather than shredded. `if (c && d) { a(); }` is emitted as `c && d && a();`, which + * parses as `(c && d) && a()`; reversing the outermost alone recovers `if (c && d) a();`. + * A nested `&&`'s parent is the `LogicalExpression` above it, so it is never a candidate. + * + * **`||` is not handled.** It has no equivalent single-branch `if` form - `a || b` runs `b` + * when `a` is *falsy*, so the reversal would need a negated test, and a negation this pass + * introduced is a shape no encoder emitted. Left alone. + */ +export function createLintLogicalIf(onReverse) { + return { + LogicalExpression: { + exit(path) { + if (path.node.operator !== '&&') { + return + } + const stmt = path.parentPath + if ( + !stmt.isExpressionStatement() || + stmt.node.expression !== path.node + ) { + return + } + const { left, right } = path.node + stmt.replaceWith(t.ifStatement(left, t.expressionStatement(right))) + if (onReverse) { + onReverse() + } + }, + }, + } +} + +export default createLintLogicalIf() diff --git a/src/visitor/atomic/split-if-test-sequence.js b/src/visitor/atomic/split-if-test-sequence.js new file mode 100644 index 00000000..e35b3820 --- /dev/null +++ b/src/visitor/atomic/split-if-test-sequence.js @@ -0,0 +1,46 @@ +import * as t from '@babel/types' + +/** + * Hoist all but the last expression out of a sequence used as an `if` test: + * + * if ((a, b)) … => a; if (b) … + * + * A sequence in the test position hides every expression but the last from statement-level + * reading, and the hidden ones are frequently the interesting part - assignments an obfuscator + * folded into the test to get them off their own line. + * + * This is the gap in `split-sequence.js`, which covers a sequence in `ExpressionStatement`, + * `ReturnStatement` and first-`VariableDeclarator` position but not this one. The two are + * complementary and are meant to run together. + * + * **Requires the `if` to be in a statement list**, because the reversal inserts siblings + * before it. An `if` that is itself an unbraced branch of another `if` has nowhere to insert; + * it is left alone here and becomes eligible once `lint-if-statement.js` has given it a block, + * which is why these two want to run in the same loop rather than once each. + * + * Order within the sequence is preserved, and the hoisted expressions still evaluate before + * the test - so this is safe even when they have side effects the test depends on. + */ +export function createSplitIfTestSequence(onSplit) { + return { + IfStatement: { + enter(path) { + const test = path.node.test + if (!t.isSequenceExpression(test) || !path.inList) { + return + } + const rest = test.expressions.slice() + const last = rest.pop() + path.insertBefore( + rest.map((expression) => t.expressionStatement(expression)), + ) + path.get('test').replaceWith(last) + if (onSplit) { + onSplit() + } + }, + }, + } +} + +export default createSplitIfTestSequence() diff --git a/src/visitor/atomic/uncompute-member.js b/src/visitor/atomic/uncompute-member.js new file mode 100644 index 00000000..13202184 --- /dev/null +++ b/src/visitor/atomic/uncompute-member.js @@ -0,0 +1,50 @@ +import * as t from '@babel/types' + +const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/ + +/** + * Drop the brackets from a member read whose key is a plain string: + * + * o["foo"] => o.foo + * + * Obfuscators rewrite every static property name into a computed string so that a string- + * concealing pass can reach it; once the string is back, the brackets carry no information. + * Restoring the dotted form is what lets later matchers key on `o.foo` at all, and it is the + * single most common residue shape in this kind of output. + * + * **The identifier gate is the safety argument, and it is not cosmetic.** `o["foo bar"]` and + * `o["0"]` have no dotted spelling, so they are declined and left computed. A pass that + * rewrote them anyway would emit code that does not parse. The gate doubles as a correctness + * check on the layer beneath: a decoded string sitting in a member key that is *not* a valid + * identifier is evidence that the string was decoded wrongly, because the encoder put a real + * property name there. + * + * **No key needs excluding here, unlike a property *key*.** `o["__proto__"]` and `o.__proto__` + * are the same accessor, and there is no member-read analogue of the object-literal `__proto__` + * special case or of `class C { ["constructor"](){} }`. Those hazards live in + * `uncompute-property-key.js`, which is why the two are separate files rather than one. + * + * Optional members (`o?.["foo"]`) are the same rewrite and are handled alongside. + */ +export function createUncomputeMember(onChange) { + const visit = (path) => { + const { node } = path + if (!node.computed || !t.isStringLiteral(node.property)) { + return + } + if (!IDENTIFIER.test(node.property.value)) { + return + } + node.property = t.identifier(node.property.value) + node.computed = false + if (onChange) { + onChange() + } + } + return { + MemberExpression: visit, + OptionalMemberExpression: visit, + } +} + +export default createUncomputeMember() diff --git a/src/visitor/atomic/uncompute-property-key.js b/src/visitor/atomic/uncompute-property-key.js new file mode 100644 index 00000000..cca2b5ab --- /dev/null +++ b/src/visitor/atomic/uncompute-property-key.js @@ -0,0 +1,69 @@ +import * as t from '@babel/types' + +import safeFunc from '../../utility/safe-func.js' + +const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/ + +/** + * Restore a property or class-member key that was rewritten into a string: + * + * ["foo"]() {} => "foo"() {} => foo() {} + * { "foo": 1 } => { foo: 1 } + * + * **This is two rewrites, not one, and only the first is dangerous.** Un-computing changes + * meaning for three keys; de-literalizing an already-non-computed key changes meaning for none. + * Keeping them as separate steps is what lets the risky half delegate to a guard that already + * exists and the safe half stay a plain rewrite. + * + * **Step 1 - un-compute, via `safeFunc.uncomputeStringKey`.** That helper is shared and already + * refuses the three keys whose meaning changes: `{ ["__proto__"]: v }` defines an own property + * where `{ "__proto__": v }` sets the prototype; `class C { ["constructor"](){} }` is an + * ordinary method where `"constructor"(){}` *is* the class constructor; and + * `static ["prototype"]` is a runtime error un-computed. This pass calls it rather than + * reimplementing the list, so there is one place to correct if a fourth case turns up. + * + * **Step 2 - de-literalize, gated on the identifier form.** Once a key is non-computed the + * remaining change is spelling only: `{ "__proto__": v }` and `{ __proto__: v }` are both the + * prototype setter, and `"constructor"(){}` and `constructor(){}` are both the constructor. So + * no exclusion list applies here, and the only gate is whether the string has an identifier + * spelling at all - `{ "foo bar": 1 }` and `{ "0": 1 }` are declined and stay quoted. + * + * A key that was *already* non-computed skips step 1 and is still eligible for step 2, which is + * what makes the pass idempotent and safe to re-run in a fixpoint loop. + */ +export function createUncomputePropertyKey(onChange) { + const visit = (path) => { + const { node } = path + if (!t.isStringLiteral(node.key)) { + return + } + let changed = false + + // Step 1: the guarded half. `uncomputeStringKey` takes the KEY path and decides for itself + // whether this owner and name may lose the brackets. + if (node.computed) { + safeFunc.uncomputeStringKey(path.get('key')) + if (!node.computed) { + changed = true + } + } + + // Step 2: pure spelling, and only once the key is non-computed. + if (!node.computed && IDENTIFIER.test(node.key.value)) { + node.key = t.identifier(node.key.value) + changed = true + } + + if (changed && onChange) { + onChange() + } + } + return { + ObjectProperty: visit, + ObjectMethod: visit, + ClassMethod: visit, + ClassProperty: visit, + } +} + +export default createUncomputePropertyKey() diff --git a/src/visitor/jsconfuser/variable-masking.js b/src/visitor/jsconfuser/variable-masking.js index cb866ae5..a71cde33 100644 --- a/src/visitor/jsconfuser/variable-masking.js +++ b/src/visitor/jsconfuser/variable-masking.js @@ -140,7 +140,10 @@ function processAssignLeft( ref = cache[ref].value } if (cache[ref].type === 'value') { - right.replaceWith(cache[ref].value) + // Clone: the cache hands out one node object per entry, and inlining it directly puts that + // same node at every site the entry resolves. An AST is a tree, so a later pass resolving + // both occurrences replaces the first and then throws on the second. + right.replaceWith(t.cloneNode(cache[ref].value, true)) vm.evalSync(generator(father.node).code) cache[prop_name] = { type: 'value', @@ -350,7 +353,8 @@ function processReplace(cache, path, prop_name) { return true } if (type === 'value') { - path.replaceWith(value) + // Clone, for the same reason: `value` is the cache's own node and this runs once per use site. + path.replaceWith(t.cloneNode(value, true)) return true } if (type === 'localvar') { diff --git a/src/visitor/lint-if-statement.js b/src/visitor/lint-if-statement.js index c3bc8e2e..0de69a89 100644 --- a/src/visitor/lint-if-statement.js +++ b/src/visitor/lint-if-statement.js @@ -1,5 +1,14 @@ import * as t from '@babel/types' +/** + * Whether this traversal rewrote anything, so the exit handler knows if a crawl is owed. + * + * Module-level because a visitor object carries no per-run state. Safe because every consumer + * invokes this as a standalone, synchronous `traverse(ast, lintIfStatement)` and `Program.enter` + * resets it at the start of each run. + */ +let rewroteSomething = false + function LintIfStatement(path) { let { test, consequent, alternate } = path.node let changed = false @@ -15,9 +24,39 @@ function LintIfStatement(path) { return } path.replaceWith(t.ifStatement(test, consequent, alternate)) + rewroteSomething = true } +/** + * The crawl restores an invariant this rewrite breaks: after a pass, the scope information Babel + * has cached should equal what a fresh parse of that pass's own output would produce. + * + * `replaceWith` is handed a new `IfStatement` built from the old node's own `test`, `consequent` + * and `alternate`, and it registers those reused subtrees' references a *second* time. Nothing is + * detached and nothing sits at two positions - `binding.referencePaths` simply lists the same live + * node twice, and `binding.references` counts it twice. Measured on one real sample: 139 duplicate + * entries, 1587 recorded references against 1448 that exist. + * + * That is invisible to output, which is what makes it dangerous rather than untidy. A later + * consumer asking "have I resolved every reference to this binding, may I delete its declaration?" + * compares its own tally against the inflated one, and the extra entries let that check pass while + * a live reference goes unhandled - deleting a declaration the program still needs, with no + * diagnostic and well-formed output text. + * + * The crawl must be **program-scoped and once per traversal**. Crawling a narrower scope makes it + * worse: it appends to outer-scope bindings that already hold those references. Gated because a + * crawl is only owed when something moved, and a crawl cannot change the tree - so this is + * invisible to output and costs nothing on a traversal that rewrote nothing. + */ export default { + Program: { + enter() { + rewroteSomething = false + }, + exit(path) { + if (rewroteSomething) path.scope.crawl() + }, + }, IfStatement: { exit: LintIfStatement, }, diff --git a/src/visitor/obfuscator/detect.js b/src/visitor/obfuscator/detect.js new file mode 100644 index 00000000..3d298303 --- /dev/null +++ b/src/visitor/obfuscator/detect.js @@ -0,0 +1,1046 @@ +import traverse from '@babel/traverse' +import * as t from '@babel/types' + +import logger from '../../utility/logger.js' + +const debugLog = logger.debugLog + +/** + * Locate javascript-obfuscator's string-array subsystem and resolve its entrypoint. + * + * **Detection is separated from decoding, and it is shape-first.** This file matches a union of + * emitted shapes and hands back handles - the holder, every root calls wrapper, the rotator if + * one is present, and the variable-form aliases. It does not decide, or even ask, which encoder + * version produced the sample. The version report derives that afterwards by looking the returned + * `signature` up in a table; keeping the lookup out of here is what lets a new era become a + * documentation row rather than an edit to this matcher. + * + * **Why the two are separated at all.** Fusing them makes a fingerprint miss indistinguishable + * from "no string array present" - the two collapse into one falsy return, and the diagnostic a + * user actually needs ("this looks like a string array, but the wrapper would not resolve") + * cannot be produced. So this file reports three outcomes, never a boolean: + * + * - `resolved` - the entrypoint is complete and the decoder can run. + * - `absent` - no string-array evidence at all. **A verdict, not a failure.** It is what + * the terminating round of a peel loop is supposed to report, and it must not + * read as a refusal. + * - `unreadable` - evidence is present and the entrypoint would not resolve. This is the + * refusal, and it keeps its narrow meaning: a layer that is ours, which we + * could not read. + * + * Separating `absent` from `unreadable` needs a second, deliberately weaker probe, because the + * strict matchers cannot tell "nothing here" from "here but not in a shape I know" - see + * `weakEvidence`. + * + * **Nothing here keys on identifier text.** `RenameIdentifiers` runs before every stage this + * file reads, so no name in the subsystem is stable. Matching is on AST shape, and every + * cross-reference is resolved through a binding rather than by comparing names. + */ + +/* ------------------------------------------------------------------------- * + * Shape helpers + * ------------------------------------------------------------------------- */ + +const isStringArrayLiteral = (node) => + t.isArrayExpression(node) && + node.elements.length > 0 && + node.elements.every((element) => t.isStringLiteral(element)) + +/** + * A string literal that JavaScript coerces to a number: `'0x151'`, `'42'`. + * + * `stringArrayIndexesType: 'hexadecimal-numeric-string'` emits the index as a *string* and lets + * the arithmetic coerce it, so a wrapper's offset arrives spelled `param - '0x28b'` and a use site + * spelled `-'0x151'`. Read as opaque, those are the whole of one option's output. + */ +function numericStringValue(node) { + if (!t.isStringLiteral(node)) { + return null + } + const text = node.value.trim() + if (!/^[+-]?(0[xX][0-9a-fA-F]+|\d+(\.\d+)?)$/.test(text)) { + return null + } + const value = Number(text) + return Number.isFinite(value) ? value : null +} + +/** + * Fold an arithmetic tree over numeric literals to its value. + * + * Required rather than convenient: `numbersToExpressions` re-spells every numeric constant as an + * arithmetic tree, so a matcher demanding a `NumericLiteral` for the index shift reads it as + * absent on exactly the high-strength samples that matter most. Folding matches the shape; testing + * for a literal matches one spelling of it. + * + * A bare string literal is deliberately **not** folded here - see `foldCoerced`. + */ +function foldNumber(node) { + if (t.isNumericLiteral(node)) { + return node.value + } + if (t.isUnaryExpression(node) && node.operator === '-') { + // Unary minus coerces unconditionally, so a numeric string is safe here whatever it wraps. + const value = foldNumber(node.argument) ?? numericStringValue(node.argument) + return value === null || value === undefined ? null : -value + } + if (t.isBinaryExpression(node)) { + // **`+` concatenates when either operand is a string**, so a numeric string folds only under + // the operators that coerce. Folding `1 + '0x2'` to 3 would be silently wrong output, which is + // the one failure mode an evaluating decoder is supposed to be immune to. + const coerces = + node.operator === '-' || node.operator === '*' || node.operator === '/' + const fold = (side) => + foldNumber(side) ?? (coerces ? numericStringValue(side) : null) + const left = fold(node.left) + const right = fold(node.right) + if (left === null || right === null) { + return null + } + switch (node.operator) { + case '+': + return left + right + case '-': + return left - right + case '*': + return left * right + case '/': + return left / right + default: + return null + } + } + return null +} + +/** + * Fold an operand that sits in a **coercing** position - the right-hand side of a `-`, say. + * + * `foldNumber` deliberately does not accept a bare string literal at top level, because it has no + * way of knowing whether its caller is in a `+`, where a numeric string concatenates instead of + * coercing. A caller that does know its operator says so by using this. + */ +function foldCoerced(node) { + return foldNumber(node) ?? numericStringValue(node) +} + +/** + * Flatten a function body's effects into a list of expressions. + * + * Adjacent-statement merging fuses the templates' statements into sequence expressions, so + * counting statements in a body matches a spelling rather than a shape. Every matcher below reads + * effects through this instead of indexing `body.body`. + */ +function bodyEffects(fnNode) { + const flat = [] + const push = (expr) => { + if (t.isSequenceExpression(expr)) { + expr.expressions.forEach(push) + } else { + flat.push(expr) + } + } + for (const statement of fnNode.body.body ?? []) { + if (t.isExpressionStatement(statement)) { + push(statement.expression) + } else if (t.isReturnStatement(statement) && statement.argument) { + push(statement.argument) + } + } + return flat +} + +/* ------------------------------------------------------------------------- * + * The three components + * ------------------------------------------------------------------------- */ + +/** + * The holder, self-replacing-function form: + * + * function F() { var A = ['…', '…']; F = function () { return A }; return F() } + * + * The first call builds the array and rewrites the binding to a closure returning it. The + * self-reassignment is what distinguishes this from an ordinary function that happens to declare + * an array, so it is required rather than incidental. + */ +function matchHolderFn(node) { + if (!t.isFunctionDeclaration(node) || node.params.length !== 0 || !node.id) { + return null + } + const selfName = node.id.name + let literal = null + let arrayName = null + for (const statement of node.body.body) { + if (!t.isVariableDeclaration(statement)) { + continue + } + for (const declarator of statement.declarations) { + if ( + isStringArrayLiteral(declarator.init) && + t.isIdentifier(declarator.id) + ) { + literal = declarator.init + arrayName = declarator.id.name + } + } + } + if (!literal) { + return null + } + const reassignsSelf = bodyEffects(node).some( + (expr) => + t.isAssignmentExpression(expr) && + t.isIdentifier(expr.left, { name: selfName }) && + t.isFunctionExpression(expr.right), + ) + if (!reassignsSelf) { + return null + } + return { kind: 'fn-self-replacing', name: selfName, arrayName, literal, node } +} + +/** + * The holder, plain-declaration form: `var NAME = ['…', '…'];` + * + * Deliberately permissive here and filtered later. A hand-written `var parts = ['a','b','c']` has + * this exact shape, so a match is only treated as the subsystem's holder once a wrapper indexes it + * or a rotator rotates it - see `selectHolder`. Tightening at the match site instead would make + * the two indistinguishable at the point where the reason is no longer visible. + * + * **Matched on the declarator, not on the declaration that contains it.** What this returns is + * what the decoder removes, and `var a = 1, NAME = ['…'];` is a legal spelling - handing back the + * declaration would take the sibling declarator with it. The extra `var` wrapper needed to + * re-declare the array in an isolate is cheap to add back; a deleted sibling is not recoverable. + */ +function matchHolderVar(node) { + if ( + !t.isVariableDeclarator(node) || + !isStringArrayLiteral(node.init) || + !t.isIdentifier(node.id) + ) { + return null + } + return { + kind: 'var-declaration', + name: node.id.name, + arrayName: node.id.name, + literal: node.init, + node, + } +} + +/** + * The root calls wrapper: two parameters, subtracts a constant from the first, indexes the array + * with the result. + * + * One matcher covers all five wrapper eras, because what varies between them is *where* the body + * lives and *how* it reaches the array, not what it does: + * + * - the self-replacing eras hide the real body in an inner closure assigned to the binding, so + * the matcher looks through that assignment when it is present; + * - the array is reached either by reading an identifier directly, or by calling the holder, + * or - from the era where the holder became a function - by calling it once into a local that + * the body then indexes. + * + * Both facts are returned rather than branched on, since they are exactly the discriminators the + * era table is keyed on. + */ +function matchWrapper(fnNode, selfName) { + if (!fnNode || fnNode.params.length !== 2) { + return null + } + + const inner = bodyEffects(fnNode).find( + (expr) => + t.isAssignmentExpression(expr) && + t.isIdentifier(expr.left, { name: selfName }) && + t.isFunctionExpression(expr.right), + ) + const target = inner ? inner.right : fnNode + + let shift = null + let reads = null + traverse( + t.file( + t.program([ + t.expressionStatement( + t.functionExpression(null, target.params, target.body), + ), + ]), + ), + { + AssignmentExpression(path) { + const { left, right } = path.node + if ( + t.isIdentifier(left) && + t.isBinaryExpression(right, { operator: '-' }) && + t.isIdentifier(right.left, { name: left.name }) + ) { + const value = foldNumber(right.right) + if (value !== null) { + shift = value + } + } + }, + MemberExpression(path) { + const { object, computed } = path.node + if (!computed || reads) { + return + } + if (t.isIdentifier(object)) { + reads = { via: 'identifier', name: object.name } + } else if ( + t.isCallExpression(object) && + t.isIdentifier(object.callee) + ) { + reads = { via: 'call', name: object.callee.name } + } + }, + }, + ) + + // The era where the holder became a function hoists `var a = HOLDER();` into the outer body and + // indexes the local. That reads as `via: 'identifier'` above, which would collide with the + // eras that genuinely index a plain array binding - so resolve the local one step further. + if (reads?.via === 'identifier') { + for (const statement of fnNode.body.body ?? []) { + if (!t.isVariableDeclaration(statement)) { + continue + } + for (const declarator of statement.declarations) { + if ( + t.isIdentifier(declarator.id) && + declarator.id.name === reads.name && + t.isCallExpression(declarator.init) && + t.isIdentifier(declarator.init.callee) + ) { + reads = { via: 'call-hoisted', name: declarator.init.callee.name } + } + } + } + } + + if (shift === null || !reads) { + return null + } + return { + kind: inner ? 'self-replacing' : 'plain', + selfName, + shift, + reads, + node: fnNode, + } +} + +/** + * The rotator: an immediately-invoked two-parameter function that push/shifts the array in a loop + * until a stop condition holds. + * + * **The IIFE is not necessarily the statement's whole expression.** Adjacent-statement merging + * fuses it with whatever follows, so on a sample that also enables a timer the rotator arrives as + * `(function (a, b) { … })(A, 0xb89ba), setInterval(…)`. A matcher reading `.expression` directly + * reports the rotator ABSENT there - and that direction is the dangerous one, because a decode + * that runs the holder and wrapper without the rotator returns real strings from an unrotated + * array. Output then parses, runs, and reads clean on every residue axis while being wrong. So + * the candidate list is flattened before matching. + */ +function matchRotator(statement) { + if (!t.isExpressionStatement(statement)) { + return null + } + const candidates = [] + const flatten = (expr) => { + if (t.isSequenceExpression(expr)) { + expr.expressions.forEach(flatten) + } else if (t.isUnaryExpression(expr)) { + flatten(expr.argument) + } else { + candidates.push(expr) + } + } + flatten(statement.expression) + for (const candidate of candidates) { + const hit = matchRotatorCall(candidate, statement) + if (hit) { + return hit + } + } + return null +} + +function matchRotatorCall(call, statement) { + if (!t.isCallExpression(call)) { + return null + } + const fn = call.callee + if (!t.isFunctionExpression(fn) || fn.params.length !== 2) { + return null + } + + let pushes = false + let hasTry = false + let hasCounterLoop = false + let hasParseInt = false + let divides = false + traverse(t.file(t.program([t.expressionStatement(fn)])), { + CallExpression(path) { + const callee = path.node.callee + if ( + t.isMemberExpression(callee) && + ((t.isStringLiteral(callee.property) && + callee.property.value === 'push') || + t.isIdentifier(callee.property, { name: 'push' })) + ) { + pushes = true + } + if (t.isIdentifier(callee, { name: 'parseInt' })) { + hasParseInt = true + } + }, + TryStatement() { + hasTry = true + }, + WhileStatement(path) { + if (t.isUpdateExpression(path.node.test, { operator: '--' })) { + hasCounterLoop = true + } + }, + BinaryExpression(path) { + if (path.node.operator === '/') { + divides = true + } + }, + }) + if (!pushes) { + return null + } + + const firstArg = call.arguments[0] + return { + // `counter-loop` states its trip count as a literal second argument and can be unrotated by + // reading. `compare-loop` states it nowhere - it searches until a checksum over the array's + // own contents matches - which is why anything decoding that era has to run the loop rather + // than compute it. + kind: hasTry ? 'compare-loop' : hasCounterLoop ? 'counter-loop' : 'other', + comparison: hasParseInt + ? divides + ? 'parseint-div' + : 'parseint-mul' + : 'none', + argName: t.isIdentifier(firstArg) ? firstArg.name : null, + // Both handles, because they are not the same node and the decoder needs each for a + // different job. `call` is what gets evaluated and what gets removed; `node` is the statement + // it arrived in, which on a merged sample also carries unrelated effects that must survive. + call, + node: statement, + } +} + +/* ------------------------------------------------------------------------- * + * The weak probe + * ------------------------------------------------------------------------- */ + +/** + * Is there string-array-shaped evidence here at all? + * + * **This exists to make `absent` and `unreadable` different answers**, and it is deliberately + * weaker than the matchers above - a probe that mirrors a matcher gate for gate can only ever + * find near-misses, and is blind by construction to the case that matters: a whole holder kind or + * wrapper form nobody wrote a branch for. + * + * Each signal is cheap and shape-keyed, and **none is conclusive alone** - which is why the + * caller requires two before refusing. One is not a threshold chosen for caution: every signal + * here occurs in ordinary hand-written code, and `array-of-strings` fires on a literal as plain + * as `var parts = ['alpha', 'beta', 'gamma']`. Refusing on that would make the terminating round + * of a peel loop report a refusal over source that was never obfuscated, which inverts the one + * distinction this probe exists to draw. Two signals is the weakest rule that still separates + * them; the asymmetry is deliberate, because a missed diagnostic costs a re-run while a false + * refusal discards a completed decode. + */ +function weakEvidence(ast) { + const signals = [] + traverse(ast, { + ArrayExpression(path) { + if ( + signals.includes('array-of-strings') || + path.node.elements.length < 3 || + !path.node.elements.every((element) => t.isStringLiteral(element)) + ) { + return + } + signals.push('array-of-strings') + }, + CallExpression(path) { + const callee = path.node.callee + if (!t.isMemberExpression(callee)) { + return + } + const property = t.isStringLiteral(callee.property) + ? callee.property.value + : t.isIdentifier(callee.property) && !callee.computed + ? callee.property.name + : null + if ( + (property === 'push' || property === 'shift') && + !signals.includes('push-shift') + ) { + signals.push('push-shift') + } + }, + Function(path) { + // A two-parameter function that subtracts a constant from a parameter is the wrapper's + // arithmetic with none of its structure required. + if (signals.includes('index-shift') || path.node.params.length !== 2) { + return + } + const names = path.node.params + .filter(t.isIdentifier) + .map((param) => param.name) + if (!names.length) { + return + } + let found = false + path.traverse({ + BinaryExpression(inner) { + if ( + inner.node.operator === '-' && + t.isIdentifier(inner.node.left) && + names.includes(inner.node.left.name) && + foldNumber(inner.node.right) !== null + ) { + found = true + } + }, + }) + if (found) { + signals.push('index-shift') + } + }, + }) + return signals +} + +/* ------------------------------------------------------------------------- * + * Entrypoint resolution + * ------------------------------------------------------------------------- */ + +/** + * Pick the subsystem's own holder out of every array-of-strings in the file. + * + * A self-replacing holder is unambiguous - nothing hand-written has that shape. A plain + * declaration is not, so it counts only when something in the subsystem reads it. Where several + * survive, the largest wins: the encoder emits one array per program and a decoy would be + * smaller, whereas picking the first found makes the answer depend on traversal order. + */ +function selectHolder(holders, wrappers, rotators) { + const readNames = new Set( + [ + ...wrappers.map((wrapper) => wrapper.reads.name), + ...rotators.map((rotator) => rotator.argName), + ].filter(Boolean), + ) + const used = holders.filter( + (holder) => + holder.kind === 'fn-self-replacing' || readNames.has(holder.name), + ) + let best = null + for (const holder of used) { + if ( + !best || + holder.literal.elements.length > best.literal.elements.length + ) { + best = holder + } + } + return best +} + +/** + * Collect the variable-form scope aliases: `var a = W;` where `W` resolves to a root wrapper, + * possibly through another alias. + * + * **Resolved through bindings, never by name.** Renamed output reuses short names across + * non-overlapping scopes, so a name-keyed sweep both over- and under-counts, and a declaration id + * is not a reference. + * + * The function-form scope wrapper is a different shape and is collected separately, by + * `collectScopeWrappers` - it is machinery in its own right rather than an alias. + */ +/** + * Resolve an identifier to the root wrapper it names, through `var a = W` alias chains. + * + * Returns the wrapper's own node rather than a boolean, because with several encodings + * configured there are several root wrappers and *which* one a site reached decides which decode + * body has to evaluate it. Resolution already has that answer; discarding it would make the + * decoder guess. + * + * **Resolved through bindings, never by name.** Renamed output reuses short names across + * non-overlapping scopes, so a name-keyed sweep both over- and under-counts. + */ +function resolveWrapperNode(path, name, wrapperNodes, depth = 0) { + if (depth > 4) { + return null + } + const binding = path.scope.getBinding(name) + if (!binding) { + return null + } + const declared = binding.path.node + if (t.isFunctionDeclaration(declared) && wrapperNodes.has(declared)) { + return declared + } + if (t.isVariableDeclarator(declared)) { + if ( + t.isFunctionExpression(declared.init) && + wrapperNodes.has(declared.init) + ) { + return declared.init + } + if (t.isIdentifier(declared.init)) { + return resolveWrapperNode( + binding.path, + declared.init.name, + wrapperNodes, + depth + 1, + ) + } + } + return null +} + +/** + * The function-form scope wrapper's shape: `function X(a, b) { return W(a - N, b); }`. + * + * Every argument is either a bare parameter or `parameter - `, and at least one is the + * shifted kind. "At least one" rather than "exactly one" is required by chained calls: once the + * upper is another scope wrapper the encoder pads the call to + * `stringArrayWrappersParametersMaxCount` arguments and spells **every** one of them + * `parameter - `, real and fake alike. + * + * Returns the forwarding call, which is what names the upper wrapper. + */ +function matchScopeWrapperShape(fnNode) { + const body = fnNode.body?.body + if ( + !Array.isArray(body) || + body.length !== 1 || + !t.isReturnStatement(body[0]) + ) { + return null + } + const call = body[0].argument + if (!t.isCallExpression(call) || !t.isIdentifier(call.callee)) { + return null + } + const params = new Set( + fnNode.params + .filter((param) => t.isIdentifier(param)) + .map((param) => param.name), + ) + if (params.size !== fnNode.params.length) { + return null + } + let shifted = false + for (const arg of call.arguments) { + if (t.isIdentifier(arg) && params.has(arg.name)) { + continue + } + if ( + t.isBinaryExpression(arg, { operator: '-' }) && + t.isIdentifier(arg.left) && + params.has(arg.left.name) && + foldCoerced(arg.right) !== null + ) { + shifted = true + continue + } + return null + } + return shifted ? call : null +} + +/** + * Collect the function-form scope wrappers, in **extraction order**: every wrapper appears after + * the wrapper it forwards to. + * + * Two things make this more than a traversal. + * + * **The shape alone does not identify one.** `function f(a, b) { return g(a - 1, b); }` is a + * legal thing for a program to contain. What identifies a scope wrapper is that its forwarding + * chain *terminates at a root wrapper*, so candidates are matched structurally and then grown as + * a fixpoint outward from the root wrappers. A candidate whose chain never reaches one is not + * ours and is left entirely alone. + * + * **The fixpoint is a membership test, not a sort.** It answers "is this one of ours", and the + * root-ward layering falls out of it for free. The caller emits the result in that order because a + * dependency-ordered prelude is easier to debug, but nothing depends on the order: every wrapper + * is lifted as a hoisted function declaration, so a chain resolves whichever way round it is + * written. Do not add a guard here asserting the order matters - it does not, and it has been + * measured by reversing the emission. + */ +function collectScopeWrappers(ast, rootWrapperNodes, machineryNodes) { + const insideMachinery = (path) => { + for (let cursor = path.parentPath; cursor; cursor = cursor.parentPath) { + if (machineryNodes.has(cursor.node)) { + return true + } + } + return false + } + + const candidates = [] + const record = (path, node, name, decl) => { + if (insideMachinery(path)) { + return + } + const call = matchScopeWrapperShape(node) + if (call) { + candidates.push({ path, node, name, decl, call }) + } + } + + traverse(ast, { + FunctionDeclaration(path) { + if (path.node.id) { + record(path, path.node, path.node.id.name, 'function-declaration') + } + }, + VariableDeclarator(path) { + if ( + t.isFunctionExpression(path.node.init) && + t.isIdentifier(path.node.id) + ) { + record( + path, + path.node.init, + path.node.id.name, + 'var-function-expression', + ) + } + }, + }) + + // Grow outward from the root wrappers. Each round admits the candidates whose upper is already + // admitted, so the result is layered root-ward by construction and a chain that never reaches a + // root wrapper simply never gets admitted. + const admitted = [] + const admittedNodes = new Set() + let pending = candidates + for (;;) { + const next = [] + let grew = false + for (const candidate of pending) { + const upper = resolveWrapperNode( + candidate.path, + candidate.call.callee.name, + new Set([...rootWrapperNodes, ...admittedNodes]), + ) + if (!upper) { + next.push(candidate) + continue + } + // A wrapper that resolves to itself would loop forever in the prelude; the encoder cannot + // emit one, so this only fires on a hand-edited or hostile sample. + if (upper === candidate.node) { + continue + } + admitted.push({ ...candidate, upper }) + admittedNodes.add(candidate.node) + grew = true + } + if (!grew) { + break + } + pending = next + } + + return admitted +} + +function collectAliases(ast, wrapperNodes, machineryNodes) { + const aliases = [] + const undeclared = [] + const foreignWrappers = [] + const seenForeign = new Set() + + // Is this path inside something the decoder is going to delete wholesale? + // + // **Required, not an optimisation.** The encoder injects scope wrappers into *every* lexical + // scope, including the rotator's own body and the wrapper's inner closure. Those reference the + // root wrapper with non-constant arguments and so look exactly like the scope wrapper this pass + // does not own - but they vanish with the machinery that contains them, so treating them as + // blockers would refuse on samples that are entirely decodable. Measured: it is the whole + // difference between the maximal profile being finishable and not. + const insideMachinery = (path) => { + for (let cursor = path.parentPath; cursor; cursor = cursor.parentPath) { + if (machineryNodes.has(cursor.node)) { + return true + } + } + return false + } + + const resolvesToWrapper = (path, name) => + resolveWrapperNode(path, name, wrapperNodes) !== null + + traverse(ast, { + VariableDeclarator(path) { + if (!t.isIdentifier(path.node.init) || !t.isIdentifier(path.node.id)) { + return + } + if (resolvesToWrapper(path, path.node.init.name)) { + aliases.push({ name: path.node.id.name, path }) + } + }, + + // `_ = W;` with no declaration makes `_` a global, and `scope.getBinding` returns nothing for + // it. Left unreported that reads as a clean zero rather than as a failure, which is the one + // direction a census must never fail in - a real sample needed this edited by hand before any + // tool could touch it. + AssignmentExpression(path) { + const { left, right } = path.node + if (!t.isIdentifier(left) || !t.isIdentifier(right)) { + return + } + if (path.scope.getBinding(left.name)) { + return + } + if (resolvesToWrapper(path, right.name)) { + undeclared.push({ name: left.name, path }) + } + }, + + // A call into the root wrapper whose arguments cannot be evaluated - the signature of a + // function-form scope wrapper passing its own parameters through. That shape is not this + // pass's to decode, and it pins the machinery alive. + // + // **Keyed on the call, then attributed to its innermost enclosing function.** Written the + // other way round - visiting functions and searching their bodies - every ancestor of a scope + // wrapper also matches, since a traversal from a function descends through nested ones. That + // reports a count with no meaning: on one profile it read more blocking wrappers than there + // were blocked references. + CallExpression(path) { + if (!t.isIdentifier(path.node.callee) || insideMachinery(path)) { + return + } + if (!resolvesToWrapper(path, path.node.callee.name)) { + return + } + const opaque = path.node.arguments.some( + (arg) => !t.isStringLiteral(arg) && foldNumber(arg) === null, + ) + if (!opaque) { + return + } + const owner = path.getFunctionParent() + const key = owner ? owner.node : ast.program + if (seenForeign.has(key)) { + return + } + seenForeign.add(key) + foreignWrappers.push({ path: owner, call: path }) + }, + }) + + return { aliases, undeclared, foreignWrappers } +} + +/* ------------------------------------------------------------------------- * + * The detector + * ------------------------------------------------------------------------- */ + +/** + * @param {import('@babel/types').File} ast + * @returns {{ + * status: 'resolved' | 'absent' | 'unreadable', + * signature: { holder: string, wrapper: string, rotate: string }, + * holder: object | null, + * wrappers: object[], + * rotators: object[], + * aliases: object[], + * undeclared: object[], + * scopeWrappers: object[], + * foreignWrappers: object[], + * notes: string[], + * }} + */ +function detectStringArray(ast) { + const holders = [] + const wrappers = [] + const rotators = [] + const notes = [] + + for (const statement of ast.program.body) { + const rotator = matchRotator(statement) + if (rotator) { + rotators.push(rotator) + } + } + + // The holder is not necessarily at program level - control-flow flattening can move it - so the + // whole tree is searched rather than the top-level statement list. + traverse(ast, { + FunctionDeclaration(path) { + const holder = matchHolderFn(path.node) + if (holder) { + holders.push({ ...holder, path }) + } + const wrapper = matchWrapper(path.node, path.node.id?.name) + if (wrapper) { + wrappers.push({ ...wrapper, decl: 'function-declaration', path }) + } + }, + VariableDeclarator(path) { + const holder = matchHolderVar(path.node) + if (holder) { + holders.push({ ...holder, path }) + } + if ( + !t.isFunctionExpression(path.node.init) || + !t.isIdentifier(path.node.id) + ) { + return + } + const wrapper = matchWrapper(path.node.init, path.node.id.name) + if (wrapper) { + wrappers.push({ ...wrapper, decl: 'var-function-expression', path }) + } + }, + }) + + const holder = selectHolder(holders, wrappers, rotators) + + // Every root wrapper whose array read resolves to the holder, not just one: with several + // encodings configured the encoder emits one wrapper per encoding, each with its own decode + // body, and stopping at the first leaves the rest of the call sites undecodable. + const rootWrappers = holder + ? wrappers.filter( + (wrapper) => + wrapper.reads.name === holder.arrayName || + wrapper.reads.name === holder.name, + ) + : [] + + const signature = { + holder: holder ? holder.kind : 'none', + wrapper: rootWrappers.length + ? `${rootWrappers[0].decl}/${rootWrappers[0].kind}/reads-${rootWrappers[0].reads.via}` + : 'none', + rotate: rotators.length + ? `${rotators[0].kind}/${rotators[0].comparison}` + : 'none', + } + + const empty = { + status: 'absent', + signature, + holder: null, + wrappers: [], + rotators: [], + aliases: [], + undeclared: [], + scopeWrappers: [], + foreignWrappers: [], + notes, + } + + if (!holder || !rootWrappers.length) { + const signals = weakEvidence(ast) + if (signals.length < 2) { + debugLog( + `[obfuscatorx] detect: no string array present` + + (signals.length ? ` (${signals[0]} alone is not evidence)` : ''), + ) + return empty + } + // Evidence present, entrypoint unresolved. This is the refusal, and it says which half failed + // so the report can be read without re-running anything. + notes.push( + `string-array evidence present (${signals.join(', ')}) but the entrypoint did not resolve: ` + + `${holder ? 'holder matched' : 'no holder'}, ` + + `${rootWrappers.length ? `${rootWrappers.length} root wrapper(s)` : 'no root wrapper'}`, + ) + debugLog(`[obfuscatorx] detect: ${notes[notes.length - 1]}`) + return { ...empty, status: 'unreadable' } + } + + const wrapperNodes = new Set(rootWrappers.map((wrapper) => wrapper.node)) + // Everything the decoder removes as one unit. Anything lexically inside it is not residue and + // not a blocker - it goes when the machinery goes. + const machineryNodes = new Set([ + holder.node, + ...rootWrappers.map((wrapper) => wrapper.node), + // The rotator's *call*, not the statement it sits in: adjacent-statement merging fuses that + // statement with unrelated effects, and a wrapper reference inside one of those is a real + // use site rather than machinery. + ...rotators.map((rotator) => rotator.call), + ]) + // The function-form scope wrappers are machinery too, so they are resolved *before* the alias + // sweep and then folded into both sets it reads: their bodies stop looking like foreign calls + // into the root wrapper, and an alias to one resolves the same way an alias to a root wrapper + // does. + const scopeWrappers = collectScopeWrappers(ast, wrapperNodes, machineryNodes) + for (const wrapper of scopeWrappers) { + machineryNodes.add(wrapper.node) + wrapperNodes.add(wrapper.node) + } + + const { aliases, undeclared, foreignWrappers } = collectAliases( + ast, + wrapperNodes, + machineryNodes, + ) + + // Returned as well as noted, because the decoder has to *gate* on it rather than log it. An + // alias with no binding has call sites nothing can enumerate, so deleting the machinery after + // missing one is fail-open corruption rather than countable residue. + for (const item of undeclared) { + notes.push( + `wrapper alias '${item.name}' is assigned without a declaration, so it is a global and ` + + `has no binding to resolve through; its call sites cannot be found by scope lookup`, + ) + } + if (foreignWrappers.length) { + notes.push( + `${foreignWrappers.length} function(s) call the string-array machinery with non-constant ` + + `arguments and are not resolvable scope wrappers; those call sites are not this pass's ` + + `to decode, and the machinery cannot be removed while they reference it`, + ) + } + if (!rotators.length) { + // Legal - rotation is an option - so this is recorded, never treated as a miss. What tells + // the two apart is not visible here: it is whether the extracted machinery evaluates, which + // only the decoder can try. + notes.push( + 'no rotator present; the array is either unrotated or the rotator was not matched', + ) + } + + debugLog( + `[obfuscatorx] detect: holder=${signature.holder} wrapper=${signature.wrapper} ` + + `rotate=${signature.rotate}, ${rootWrappers.length} root wrapper(s), ` + + `${aliases.length} alias(es)` + + (scopeWrappers.length + ? `, ${scopeWrappers.length} scope wrapper(s)` + : '') + + (foreignWrappers.length + ? `, ${foreignWrappers.length} foreign wrapper(s)` + : ''), + ) + + return { + status: 'resolved', + signature, + holder, + wrappers: rootWrappers, + rotators, + aliases, + undeclared, + scopeWrappers, + foreignWrappers, + notes, + } +} + +export default detectStringArray +export { weakEvidence, foldNumber, resolveWrapperNode } diff --git a/src/visitor/obfuscator/normalize-converting.js b/src/visitor/obfuscator/normalize-converting.js new file mode 100644 index 00000000..dc755645 --- /dev/null +++ b/src/visitor/obfuscator/normalize-converting.js @@ -0,0 +1,120 @@ +import generator from '@babel/generator' +import traverse from '@babel/traverse' + +import logger from '../../utility/logger.js' +import calculateConstantExp from '../calculate-constant-exp.js' +import mergeObject from '../merge-object.js' +import { createCollapsePropertyShorthand } from '../atomic/collapse-property-shorthand.js' +import { createUncomputeMember } from '../atomic/uncompute-member.js' +import { createUncomputePropertyKey } from '../atomic/uncompute-property-key.js' + +const debugLog = logger.debugLog + +/** + * Undo javascript-obfuscator's `Converting` stage: put every re-spelled literal, property name + * and object literal back into the form it was written in. + * + * That stage holds eleven transformers, and ten of them rewrite a single node in place - a + * boolean into `!![]`, a number into hexadecimal or an arithmetic tree, a property name into a + * computed string, a string into a chain of chunks. None of those rewrites is specific to this + * obfuscator, so none of the reversals lives here: they are plain visitors under `src/visitor/` + * that any plugin may compose. + * + * **This file is scheduling and nothing else**, which is the same division + * `normalize-statements.js` already draws. What is obfuscator-specific is not how to fold a + * constant - it is knowing that these particular reversals unlock each other, in which order, + * and that the group has to run to a fixpoint. + */ + +/** + * Strip the `raw` spelling from numeric literals. + * + * `NumberLiteralTransformer` is the one member of its stage that changes no AST node at all: it + * writes the literal's `raw` and leaves `value` untouched. So there is nothing to match and + * nothing to fold - discarding `extra` *is* the entire reversal, and a generator then prints the + * value in decimal. + * + * **Scoped to numbers on purpose.** String literals also carry a `raw`, and theirs holds the + * escape spelling that the `Finalizing` stage produced; that is a different transform with its + * own reversal, so this pass leaves string literals alone rather than quietly absorbing it. + */ +function stripNumericRaw(ast) { + let count = 0 + traverse(ast, { + NumericLiteral: ({ node }) => { + if (node.extra) { + delete node.extra + count++ + } + }, + }) + return count +} + +/** + * Run the group to a fixpoint. + * + * **Iteration is required rather than tidy, and it is measurable.** The reversals expose each + * other's input, because the encoder's own transforms compose: with `splitStrings` on, a + * property name is emitted as a *chain* - `o['\x66\x6c' + '\x61\x67']` - so it is not a string + * literal at all until the chain has been folded, and a member-un-computing pass looking for a + * string key finds nothing. Measured on one corpus cell (`2.19.0/objects__all-on.js`): the + * count of un-computable member reads is 33 before folding and 365 after. So a pass that ran + * once, in any order, would leave roughly nine tenths of that shape behind. + * + * **Order within a round, and what each step feeds:** + * + * 1. strip numeric `raw` - no dependencies; cheapest first + * 2. fold constant expressions - turns `!![]` into `true`, arithmetic trees into numbers, and + * chunk chains into whole strings, which is what creates the + * string keys steps 3 and 4 need + * 3. un-compute member reads - `o["foo"]` into `o.foo` + * 4. un-compute property keys - `["foo"](){}` into `foo(){}` + * 5. merge extracted objects - reassembles `var t = {}; t.a = 1; ...` into one literal, + * which is easiest once 3 and 4 have made the writes dotted + * 6. collapse shorthand - last, because it only ever acts on what 4 produced + * + * **One ordering here is a convenience, not a constraint, and it is recorded as such.** Step 5 + * accepts both a string and an identifier property, so it does not in fact require steps 3 and 4 + * to have run. It is scheduled after them because the merged output then reads dotted, not + * because the reverse order fails. Anything asserting a stricter dependency should reverse the + * two and measure, rather than inherit this comment. + * + * `maxRounds` is a runaway guard rather than a tuning knob: the loop exits as soon as a round + * changes nothing, and hitting the cap means either pathological nesting or two rewrites + * oscillating, both of which are worth reporting. + */ +export default function normalizeConverting(ast, maxRounds = 10) { + // Termination is decided by comparing the whole tree between rounds, not by counting what the + // rewrites report. Two of the six - `calculateConstantExp` and `mergeObject` - are shared + // visitors with no change signal to offer, and §3.3 forbids editing a shared visitor to add + // one. Counting only the passes that *can* report would exit a round early whenever those two + // were the only ones to fire, silently leaving their consequences unprocessed. A generated + // comparison costs one serialization per round and cannot be wrong about whether the tree + // moved. + let previous = null + let rounds = 0 + for (; rounds < maxRounds; rounds++) { + const noted = { atomic: 0 } + const bump = () => { + noted.atomic++ + } + + stripNumericRaw(ast) + traverse(ast, calculateConstantExp) + traverse(ast, createUncomputeMember(bump)) + traverse(ast, createUncomputePropertyKey(bump)) + traverse(ast, mergeObject) + traverse(ast, createCollapsePropertyShorthand(bump)) + + const current = generator(ast, { compact: true }).code + if (current === previous) { + break + } + previous = current + } + if (rounds >= maxRounds) { + debugLog(`normalize-converting: hit the ${maxRounds}-round cap`) + } + return ast +} diff --git a/src/visitor/obfuscator/normalize-statements.js b/src/visitor/obfuscator/normalize-statements.js new file mode 100644 index 00000000..b752df1f --- /dev/null +++ b/src/visitor/obfuscator/normalize-statements.js @@ -0,0 +1,116 @@ +import traverse from '@babel/traverse' + +import logger from '../../utility/logger.js' +import lintIfStatement from '../lint-if-statement.js' +import splitSequence from '../split-sequence.js' +import splitVariableDeclaration from '../split-variable-declaration.js' +import { createConvertConditionalAssign } from '../atomic/convert-conditional-assign.js' +import { createLintConditionalIf } from '../atomic/lint-conditional-if.js' +import { createLintLogicalIf } from '../atomic/lint-logical-if.js' +import { createSplitIfTestSequence } from '../atomic/split-if-test-sequence.js' + +const debugLog = logger.debugLog + +/** + * Undo javascript-obfuscator's `Simplifying` stage: put statement-level control flow that was + * packed into operators back into statements. + * + * That stage collapses a block's trailing run of statements into one comma expression, and + * rewrites an `if` whose branches collapsed that way into `&&`, `?:`, or a de-braced branch. + * Semantics are untouched; what it destroys is the statement boundary. Since every later + * matcher navigates by statement boundaries, this runs first - it is a precondition for the + * rest of the pipeline, not a cosmetic finish. + * + * **This file is scheduling and nothing else.** Every rewrite lives in a single-purpose + * visitor under `src/visitor/`, each reusable on its own and each carrying its own safety + * argument. What is obfuscator-specific is which of them run, in what order, and that they + * run to a fixpoint - so that is all this file holds. + */ + +/** + * Run the normalization group to a fixpoint. + * + * **Iteration is required, not tidiness.** The rewrites unlock each other, because the + * encoder nests its own outputs. `if (t) { x(); if (c) { a(); } else { b(); } }` is emitted as + * `t && (x(), c ? a() : b());` - a conditional inside a sequence inside a `&&`. Nothing can + * reach that conditional until the `&&` is reversed, which creates a statement; re-bracing + * then gives that statement a block; only then can the sequence split; only then is the + * conditional in statement position. Each round peels one layer, so the round count is the + * nesting depth rather than a constant. Measured on that example: three rounds. + * + * **Order within a round is what makes one round do as much as possible**, and each step + * feeds the next: + * + * 1. distribute assignments into conditionals - turns value position into statement position + * 2. re-brace `if` branches - gives 3 and 4 a statement list to insert into + * 3. split sequences in statement/return - exposes packed statements + * 4. split sequences in `if` tests - the position 3 does not cover + * 5. reverse `&&` and `?:` in statement position - the reversals themselves + * + * Step 5 last is deliberate: it is what creates the brace-less branches and fresh statement + * positions that step 2 and step 3 of the *next* round act on. + * + * `maxRounds` is a runaway guard, not a tuning knob. The loop exits as soon as a round + * reverses nothing, and a file hitting the cap means either pathological nesting or a rewrite + * oscillating - both worth knowing about, so it is reported. + */ +function normalizeStatements(ast, maxRounds = 8) { + const total = { + logical: 0, + conditional: 0, + assign: 0, + sequence: 0, + rounds: 0, + cappedOut: false, + } + + for (let round = 0; round < maxRounds; round += 1) { + let changed = 0 + + traverse( + ast, + createConvertConditionalAssign(() => (total.assign += 1)), + ) + traverse(ast, lintIfStatement) + traverse(ast, splitSequence) + traverse(ast, splitVariableDeclaration) + traverse( + ast, + createSplitIfTestSequence(() => (total.sequence += 1)), + ) + traverse( + ast, + createLintConditionalIf(() => { + total.conditional += 1 + changed += 1 + }), + ) + traverse( + ast, + createLintLogicalIf(() => { + total.logical += 1 + changed += 1 + }), + ) + + total.rounds = round + 1 + if (!changed) { + break + } + if (round === maxRounds - 1) { + total.cappedOut = true + } + } + + debugLog( + `[obfuscatorx] normalize-statements: ${total.conditional} conditional, ` + + `${total.logical} logical, ${total.assign} assign-distributed, ` + + `${total.sequence} if-test sequences, in ${total.rounds} round(s)` + + (total.cappedOut + ? ' — HIT THE ROUND CAP, output may still be packed' + : ''), + ) + return total +} + +export default normalizeStatements diff --git a/src/visitor/obfuscator/report.js b/src/visitor/obfuscator/report.js new file mode 100644 index 00000000..e7b8b836 --- /dev/null +++ b/src/visitor/obfuscator/report.js @@ -0,0 +1,167 @@ +import logger from '../../utility/logger.js' + +/** + * Turn a detector signature into an era vector and a version range. + * + * Emitted output cannot identify an exact version, only an **era** - a maximal version range over + * which some named thing about the encoder holds still. So this reports a range, derived as the + * *intersection* of the per-component verdicts, and never a single version. + * + * **It never gates a decode.** An unrecognised signature yields `unknown` for that component and a + * decode that has already resolved its entrypoint proceeds regardless; refusal is reserved for a + * layer that is ours and unreadable, which is a different question this file does not answer. + * + * **Per-component, because a verdict has to be able to be partial.** A sample built with rotation + * disabled carries no evidence on that axis at any version, so naming an era for it would be + * inventing one. Only the axes that carry evidence take part in the intersection. + */ + +/** + * Era ranges, transcribed from the encoder package's era registry, which is the only place a range + * and the commit that verified it appear together. Bounds are inclusive. + * + * Kept as data rather than as branches so a registry gaining a row is one edit here and none in the + * matcher - detection is shape-first, so the era is an output of matching rather than an input to + * it, and nothing downstream keys on these names. + */ +const ERA_RANGES = { + 'E-sa-array-declaration': ['0.25.0', '2.18.1'], + 'E-sa-array-self-replacing-fn': ['2.19.0', '5.5.0'], + 'E-sa-wrapper-var-fn-expression': ['2.9.0', '2.11.1'], + 'E-sa-wrapper-fn-declaration': ['2.12.0', '2.15.3'], + 'E-sa-wrapper-self-replacing': ['2.15.4', '2.18.1'], + 'E-sa-wrapper-array-fn-call': ['2.19.0', '4.1.1'], + 'E-sa-wrapper-flat': ['4.2.0', '5.5.0'], + 'E-sa-rotate-counter-loop': ['0.28.0', '2.9.6'], + 'E-sa-rotate-compare-loop': ['2.10.0', '2.18.1'], + 'E-sa-rotate-compare-loop-fn-arg': ['2.19.0', '5.5.0'], +} + +/** Signature string -> era ID, per component. */ +const SIGNATURE_ERAS = { + holder: { + 'var-declaration': 'E-sa-array-declaration', + 'fn-self-replacing': 'E-sa-array-self-replacing-fn', + }, + wrapper: { + 'var-function-expression/plain/reads-identifier': + 'E-sa-wrapper-var-fn-expression', + 'function-declaration/plain/reads-identifier': + 'E-sa-wrapper-fn-declaration', + 'function-declaration/self-replacing/reads-identifier': + 'E-sa-wrapper-self-replacing', + 'function-declaration/self-replacing/reads-call-hoisted': + 'E-sa-wrapper-array-fn-call', + }, + rotate: { + 'counter-loop/none': 'E-sa-rotate-counter-loop', + 'compare-loop/parseint-mul': 'E-sa-rotate-compare-loop', + 'compare-loop/parseint-div': 'E-sa-rotate-compare-loop-fn-arg', + }, +} + +/** + * The range this entry has actually verified, which is deliberately **not contiguous**: the 2.x + * eras studied in phase 1, and separately the pinned 5.5.0. Nothing between has been built or run, + * so a range landing inside the hole is reported as unverified rather than interpolated across. + */ +const COVERAGE_HOLE = ['3.0.0', '4.2.2'] + +const parts = (v) => v.split('.').map(Number) +function compareVersions(a, b) { + const [x, y] = [parts(a), parts(b)] + for (let i = 0; i < 3; i++) { + if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0) + } + return 0 +} +const maxV = (a, b) => (compareVersions(a, b) >= 0 ? a : b) +const minV = (a, b) => (compareVersions(a, b) <= 0 ? a : b) + +/** + * @param {{holder: string, wrapper: string, rotate: string}} signature + * @returns {{ + * eras: Record, + * range: {low: string, high: string}|null, + * conflict: boolean, + * inCoverageHole: boolean, + * }} + */ +export function deriveEra(signature) { + const eras = {} + for (const component of ['holder', 'wrapper', 'rotate']) { + const value = signature?.[component] + if (value === undefined || value === 'none') { + // `rotate=none` is the case this exists for: rotation is an option, so its absence is + // evidence about the options and says nothing about the version. Not unknown - *absent*. + eras[component] = null + continue + } + eras[component] = SIGNATURE_ERAS[component][value] ?? 'unknown' + } + + const known = Object.values(eras).filter((e) => e && e !== 'unknown') + if (!known.length) { + return { eras, range: null, conflict: false, inCoverageHole: false } + } + + let low = '0.0.0' + let high = '99.99.99' + for (const era of known) { + const [lo, hi] = ERA_RANGES[era] + low = maxV(low, lo) + high = minV(high, hi) + } + const conflict = compareVersions(low, high) > 0 + const inCoverageHole = + !conflict && + compareVersions(low, COVERAGE_HOLE[1]) <= 0 && + compareVersions(high, COVERAGE_HOLE[0]) >= 0 + return { + eras, + range: conflict ? null : { low, high }, + conflict, + inCoverageHole, + } +} + +/** + * Log the verdict. Reporting only - it returns nothing a caller can gate on, by design. + * + * **An empty intersection is a diagnostic, not a failure.** It means the components disagree about + * which version could have emitted them, so the sample is either not stock - a modified variant, a + * hand-edited file, a second encoder over the top - or from a version whose shape nobody has + * recorded. Either way the decode has already happened by the time this runs. + */ +export function reportEra(signature) { + const verdict = deriveEra(signature) + const vector = Object.entries(verdict.eras) + .map(([k, v]) => `${k}=${v ?? 'no-evidence'}`) + .join(' ') + + if (verdict.conflict) { + logger.error( + `[obfuscatorx] era: ${vector} — components disagree, so no version could have emitted ` + + `all of them. Not stock: a modified variant, or a version whose shape is unrecorded. ` + + `The decode above is unaffected.`, + ) + return verdict + } + if (!verdict.range) { + logger.log( + `[obfuscatorx] era: ${vector} — no component carries version evidence, so the version is ` + + `unknown. This is the expected reading for a sample with no string array.`, + ) + return verdict + } + logger.log( + `[obfuscatorx] era: ${vector} — version in [${verdict.range.low}, ${verdict.range.high}]` + + (verdict.inCoverageHole + ? ', which overlaps the 3.0.0–4.2.2 range this entry has never verified; treat as unknown ' + + 'rather than covered' + : ''), + ) + return verdict +} + +export default reportEra diff --git a/src/visitor/obfuscator/string-array.js b/src/visitor/obfuscator/string-array.js new file mode 100644 index 00000000..0c14a394 --- /dev/null +++ b/src/visitor/obfuscator/string-array.js @@ -0,0 +1,661 @@ +import ivm from 'isolated-vm' +import generator from '@babel/generator' +import traverse from '@babel/traverse' +import * as t from '@babel/types' + +import logger from '../../utility/logger.js' +import detectStringArray, { foldNumber, resolveWrapperNode } from './detect.js' + +const debugLog = logger.debugLog + +/** + * Reverse javascript-obfuscator's string array: replace every wrapper call with the string it + * returns, then delete the machinery that produced it. + * + * **The strings are recovered by running the encoder's own code, not by modelling it.** The + * subsystem is located structurally (see detect.js), its source text is captured, evaluated in a + * fresh isolate, and each call site is decoded by evaluating that call. Rotation, the index + * shift, and the `none` / `base64` / `rc4` encodings are therefore handled by the encoder's + * prelude rather than reimplemented here. + * + * The reason is generality over three axes that are shape-only, all of which a static model would + * have to absorb one at a time: + * + * - **encodings** - a swapped-alphabet base64 with a padding-ignoring decoder, and an rc4 keyed + * per item out of a key pool. Evaluating covers both at no cost; reimplementing them exactly + * is where "nearly right" becomes silently wrong output. + * - **eras** - the holder, wrapper and rotator shapes each move on their own axis, but their + * *semantics* never change. So extraction is era-dependent and decoding is era-invariant, + * which is why there are no per-era strategy files here. + * - **variants** - real samples come from *modified* obfuscators. A fork that alters the index + * arithmetic or the rotator's checksum still runs; a model keyed on the stock algorithm does + * not survive it. + * + * **One run decodes exactly one layer.** There is deliberately no internal loop over a + * re-obfuscated sample. A second encode wraps the first's whole output, so peeling is + * outermost-first and each layer is an ordinary single-encode once it is outermost - but the + * dangerous failure in that situation is a pass mutating the layer *beneath* it, and stopping at + * the boundary is what makes that boundary observable. Layers also need not come from the same + * encoder, so the loop belongs to whoever is driving the decoders, not inside one of them. What + * this pass owes that driver is the distinction its status carries. + * + * **What it does when it cannot finish, and why there are four answers rather than two:** + * + * - `decoded` - every call site resolved, the machinery is gone. + * - `absent` - no string array here. A verdict, not a failure: it is what the terminating + * round of a peel loop reports. + * - `unowned` - the subsystem is ours and readable, but a construct a *later unit* owns is + * still calling it. Folding this into a refusal would make a sample merely + * awaiting another pass indistinguishable from a matcher failure. + * - `unreadable` - ours, and we could not read it. The refusal, kept narrow. + * + * Every outcome other than `decoded` leaves the tree **completely untouched**. A half-resolved + * string array is worse than an untouched one: downstream matchers key on how a construct is + * spelled, so a partial decode manufactures two entities out of one. + */ + +/** + * Mandatory rather than defensive: a compare-loop rotator cannot terminate on an array it was not + * written for, so the timeout is how an incomplete extraction is observed at all. + */ +const EVAL_TIMEOUT_MS = 10000 + +const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/ + +const gen = (node) => generator(node, { compact: true }).code + +/** + * An argument this pass can evaluate: a string, or an arithmetic tree over numeric literals. + * + * Folding rather than demanding a `NumericLiteral` is required, not tidy: `numbersToExpressions` + * re-spells every numeric constant as an arithmetic tree, so a literal test reads the argument as + * unevaluable on exactly the high-strength samples that matter most. + */ +const isEvaluableArg = (node) => + t.isStringLiteral(node) || foldNumber(node) !== null + +/* ------------------------------------------------------------------------- * + * Extraction + * ------------------------------------------------------------------------- */ + +/** + * The holder's source, re-declared standalone. + * + * The plain-declaration form is matched as a *declarator*, so the `var` keyword has to be put + * back. That is the cheap direction of the trade: `var a = 1, ARRAY = ['…'];` is a legal spelling + * and handing back the whole declaration would delete the sibling. + */ +function holderSource(holder) { + return holder.kind === 'var-declaration' + ? gen(t.variableDeclaration('var', [holder.node])) + : gen(holder.node) +} + +function wrapperSource(wrapper) { + return wrapper.decl === 'var-function-expression' + ? gen(t.variableDeclaration('var', [wrapper.path.node])) + : gen(wrapper.path.node) +} + +/** + * Build the prelude in **dependency order** - holder, then every wrapper, then the rotator. + * + * Source order is not the constraint and following it would be wrong: renamed output routinely + * emits the rotator above both, relying on function hoisting that no longer applies once the + * pieces are lifted out of their file. What the order has to respect is what each piece needs + * from the others - the wrappers read the holder, and the rotator drives the array through the + * wrappers, so it must run last. + * + * The rotator is wrapped in a statement rather than emitted bare, so that a callee in function + * position is parenthesised. + */ +function buildPrelude(holder, wrappers, rotators, scopeWrappers = []) { + const parts = [holderSource(holder)] + for (const wrapper of wrappers) { + parts.push(wrapperSource(wrapper)) + } + // Scope wrappers sit before the rotator's invocation, which stays **last** so that termination + // still proves the extraction was complete. They are declarations, so placing them ahead of it + // runs nothing. + // + // Their order among themselves is deliberately *not* load-bearing: each is emitted as a hoisted + // function declaration, so a chain resolves whichever way round they are written. Measured by + // reversing this loop, which changes no result. They are emitted in the detector's root-ward + // order anyway, because a prelude that reads in dependency order is easier to debug when one + // does fail - but nothing depends on it, and a future change that reorders them is not a bug. + for (const scope of scopeWrappers) { + parts.push(scopeWrapperSource(scope)) + } + for (const rotator of rotators) { + parts.push(gen(t.expressionStatement(rotator.call))) + } + return parts.join(';\n') +} + +/** + * Re-declare one scope wrapper under a synthetic name, with its upper reference rewritten to the + * upper's own lifted name. + * + * **Renaming on lift is required, not hygiene.** Every wrapper is lifted out of its own lexical + * scope into one flat isolate global scope, and `identifierNamesGenerator: 'mangled'` reuses short + * names across non-overlapping scopes - so two wrappers from sibling scopes can arrive named `a`, + * and the second definition would silently win. The same collision is why the root wrapper's call + * sites are evaluated through `wrapper.selfName` rather than by evaluating alias declarations. + * + * The node is cloned before the callee is rewritten: the real tree must not be touched until the + * whole decode has committed, or a later failure leaves a half-rewritten program behind. + */ +function scopeWrapperSource(scope) { + const fn = t.cloneNode(scope.node, true) + const call = fn.body.body[0].argument + call.callee = t.identifier(scope.upperLiftedName) + return gen( + t.functionDeclaration(t.identifier(scope.liftedName), fn.params, fn.body), + ) +} + +/* ------------------------------------------------------------------------- * + * Call sites + * ------------------------------------------------------------------------- */ + +/** + * Every reference to a root wrapper, classified. + * + * **A reference inside the machinery is not a use site.** The encoder injects scope aliases into + * every lexical scope, the rotator's own body and the wrapper's inner closure included, and the + * decode bodies hang a memo cache off the wrapper object. All of those look like ordinary uses + * and all of them vanish with the machinery around them; decoding them would be work thrown away, + * and counting them as blockers would refuse on samples that are entirely decodable. + */ +function collectSites(ast, wrapperNodes, aliasNodes, insideMachinery) { + const sites = [] + const opaque = [] + const strayRefs = [] + + traverse(ast, { + Identifier(path) { + // A declaration id is not a reference. Counting one is the trap that makes a fail-closed + // matcher kill every application in scope. + if (!path.isReferencedIdentifier()) { + return + } + const wrapper = resolveWrapperNode(path, path.node.name, wrapperNodes) + if (!wrapper || insideMachinery(path)) { + return + } + const parent = path.parent + if (t.isCallExpression(parent) && parent.callee === path.node) { + if (parent.arguments.every(isEvaluableArg)) { + sites.push({ path: path.parentPath, wrapper }) + } else { + // **This has no producer below javascript-obfuscator 3.2.0, and a known one from there + // on.** A 2.x use site is constant by construction: every argument - the real index, the + // fake padding, the rc4 key - is built by the encoder from a literal factory, and the + // only non-constant argument it can emit is a scope wrapper forwarding `param - N`, + // which is machinery and never reaches here. From 3.2.0 `stringArrayCallsTransform` + // (StringArrayControlFlowTransformer) moves those literals into a control-flow storage + // and rewrites the site to `storage.key` - a member expression, which is exactly what + // `isEvaluableArg` refuses. + // + // So this is a gate waiting for its input, not vestigial: do not delete it as + // unreachable. `unowned` is the right answer for it too, since a storage argument is + // precisely "readable, and a construct another unit owns is calling in". + opaque.push(gen(parent)) + } + return + } + // The alias declarations this pass already knows about: `var a = W`. The reference is the + // alias being defined, not a use of it. + if ( + t.isVariableDeclarator(parent) && + parent.init === path.node && + aliasNodes.has(parent) + ) { + return + } + strayRefs.push(gen(parent)) + }, + }) + + return { sites, opaque, strayRefs } +} + +/** + * Is this call site in computed-member-key position? + * + * The check it feeds is one of only two that can catch a *missed rotator* with no expected output + * to compare against - see `decodeStringArray`. + */ +function isComputedKey(path) { + const parent = path.parent + return ( + (t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) && + parent.computed && + parent.property === path.node + ) +} + +/* ------------------------------------------------------------------------- * + * Removal + * ------------------------------------------------------------------------- */ + +/** + * Remove one declarator, keeping whatever shares its declaration. + * + * This exists for the MULTI-declarator case and nothing else. The encoder emits wrapper aliases as + * extra declarators inside one `var`, alongside the program's own variables — `var a = W, b = W, + * foo = a(0x109);` — so removing the declaration would take `foo` with it. + * + * Babel already owns the other direction: its removal hook deletes the parent VariableDeclaration + * when the declarator being removed is the only one, so after `path.remove()` the declaration is + * either gone or still has at least one declarator left. Never write a `declarations.length === 0` + * branch here — it cannot be reached, and it reads as though this function handles the empty case + * when Babel does. + */ +function removeDeclarator(path) { + path.remove() +} + +/** + * Remove an expression that was evaluated for its effect. + * + * Written for the rotator, and the care is for one spelling: adjacent-statement merging fuses the + * rotator's IIFE with whatever statement follows it, so on a sample that also enables a timer it + * arrives as `(function (a, b) { … })(A, 0xb89ba), setInterval(…)`. Removing the statement would + * take the timer with it. + */ +function removeEffectExpression(path) { + let target = path + while (target.parentPath && target.parentPath.isUnaryExpression()) { + target = target.parentPath + } + const parent = target.parentPath + if (parent && parent.isExpressionStatement()) { + parent.remove() + return + } + target.remove() + if (parent && !parent.removed && parent.isSequenceExpression()) { + const remaining = parent.node.expressions + if (remaining.length === 1) { + parent.replaceWith(remaining[0]) + } else if (remaining.length === 0) { + parent.remove() + } + } +} + +/** Find the live path for each of a set of nodes, in one pass. */ +function pathsFor(ast, nodes) { + const found = new Map() + if (!nodes.size) { + return found + } + traverse(ast, { + enter(path) { + if (nodes.has(path.node)) { + found.set(path.node, path) + } + }, + }) + return found +} + +/* ------------------------------------------------------------------------- * + * The pass + * ------------------------------------------------------------------------- */ + +function result(status, detected, extra = {}) { + return { + status, + signature: detected.signature, + replaced: 0, + removed: { holder: 0, wrappers: 0, rotators: 0, aliases: 0 }, + notes: detected.notes, + ...extra, + } +} + +/** + * @param {import('@babel/types').File} ast mutated in place, and only on `decoded` + * @param {{ timeout?: number }} [options] + * @returns {{ + * status: 'decoded' | 'absent' | 'unowned' | 'unreadable', + * signature: { holder: string, wrapper: string, rotate: string }, + * replaced: number, + * removed: { holder: number, wrappers: number, rotators: number, aliases: number }, + * notes: string[], + * }} + */ +function decodeStringArray(ast, options = {}) { + const timeout = options.timeout ?? EVAL_TIMEOUT_MS + const detected = detectStringArray(ast) + const notes = detected.notes + + if (detected.status !== 'resolved') { + return result(detected.status, detected) + } + + // **A gate, not a note.** An alias assigned without a declaration is a global, so it has no + // binding and its call sites cannot be enumerated. Deleting the machinery with one of them + // still live is fail-open corruption - the program breaks - where declining leaves residue + // that can be seen and counted. A real sample needed exactly this edited by hand before any + // tool could touch it. + if (detected.undeclared.length) { + notes.push( + `refusing: ${detected.undeclared.length} wrapper alias(es) have no binding, so their ` + + `call sites cannot be found and the machinery cannot be safely removed`, + ) + return result('unreadable', detected) + } + + // Readable, but a construct a later unit owns is still calling in. Reported as its own outcome + // so that "awaiting another pass" and "my matcher failed" stay different answers. + if (detected.foreignWrappers.length) { + notes.push( + `leaving the subsystem in place: ${detected.foreignWrappers.length} function(s) reach the ` + + `string-array machinery with non-constant arguments and are not resolvable scope wrappers`, + ) + return result('unowned', detected) + } + + const holder = detected.holder + const wrappers = detected.wrappers + // Lifted names are assigned here rather than in the detector: they exist only for the isolate, + // and the detector's job is to hand back handles, not to pick identifiers for an evaluation + // strategy it knows nothing about. Position in the array is the topological order the detector + // established, so an upper's name is always already assigned when its dependant is reached. + const scopeWrappers = detected.scopeWrappers ?? [] + const liftedByNode = new Map() + scopeWrappers.forEach((scope, index) => { + scope.liftedName = `__sw${index}` + liftedByNode.set(scope.node, scope) + }) + for (const scope of scopeWrappers) { + const upper = liftedByNode.get(scope.upper) + scope.upperLiftedName = upper + ? upper.liftedName + : wrappers.find((wrapper) => wrapper.node === scope.upper)?.selfName + if (!scope.upperLiftedName) { + notes.push( + `refusing: scope wrapper '${scope.name}' forwards to a wrapper that did not resolve to a ` + + `lifted name, so the prelude cannot be built`, + ) + return result('unreadable', detected) + } + } + // Only rotators that rotate *this* holder. One that does not is not ours - a second layer's, + // most likely - and running it would rotate an array it was never written for. + const rotators = detected.rotators.filter( + (rotator) => + rotator.argName === holder.name || rotator.argName === holder.arrayName, + ) + if (rotators.length !== detected.rotators.length) { + notes.push( + 'refusing: a rotator is present that does not rotate this string array', + ) + return result('unreadable', detected) + } + + // Scope wrappers join both sets. In `wrapperNodes` they become resolvable call targets, so a + // site naming one is collected like any other; in `machinery` they become part of what is + // deleted, so their own forwarding calls stop reading as use sites. + const wrapperNodes = new Set([ + ...wrappers.map((wrapper) => wrapper.node), + ...scopeWrappers.map((scope) => scope.node), + ]) + const wrapperByNode = new Map([ + ...wrappers.map((wrapper) => [ + wrapper.node, + { callName: wrapper.selfName }, + ]), + ...scopeWrappers.map((scope) => [ + scope.node, + { callName: scope.liftedName }, + ]), + ]) + const machinery = new Set([ + holder.node, + ...wrappers.map((wrapper) => wrapper.node), + ...scopeWrappers.map((scope) => scope.node), + ...rotators.map((rotator) => rotator.call), + ]) + const insideMachinery = (path) => { + for (let cursor = path.parentPath; cursor; cursor = cursor.parentPath) { + if (machinery.has(cursor.node)) { + return true + } + } + return false + } + + // **Nothing outside the machinery may read the array itself.** The wrapper's call sites are + // enumerated exhaustively below, but the *holder* is a separate binding and its references were + // never counted - so a program that indexes the array directly would have had it deleted out + // from under it. That is the fail-open direction: the sample breaks rather than leaving residue. + // Checked here because it is only answerable before anything is replaced. + const holderBinding = holder.path.isFunctionDeclaration() + ? holder.path.parentPath.scope.getBinding(holder.name) + : holder.path.scope.getBinding(holder.name) + const outsideHolderRefs = (holderBinding?.referencePaths ?? []).filter( + (ref) => !insideMachinery(ref), + ) + if (!holderBinding || outsideHolderRefs.length) { + notes.push( + holderBinding + ? `refusing: the string array is read from ${outsideHolderRefs.length} place(s) outside ` + + `the machinery, e.g. ${gen(outsideHolderRefs[0].parent)}` + : 'refusing: the string array holder has no binding to check its readers through', + ) + return result('unreadable', detected) + } + + const aliasNodes = new Set(detected.aliases.map((alias) => alias.path.node)) + const { sites, opaque, strayRefs } = collectSites( + ast, + wrapperNodes, + aliasNodes, + insideMachinery, + ) + if (opaque.length) { + notes.push( + `leaving the subsystem in place: ${opaque.length} call site(s) take arguments that ` + + `cannot be evaluated, e.g. ${opaque[0]}`, + ) + return result('unowned', detected) + } + if (strayRefs.length) { + notes.push( + `refusing: ${strayRefs.length} reference(s) to the root wrapper are neither calls nor ` + + `known aliases, e.g. ${strayRefs[0]}`, + ) + return result('unreadable', detected) + } + + // ---- evaluate ---------------------------------------------------------- + // + // **A fresh isolate per decode.** A module-scope one is shared by every decode in the process, + // so the second sample evaluates its machinery into a context still holding the first's + // bindings: a name that should be missing resolves, and the cell passes for the wrong reason. + const isolate = new ivm.Isolate({ memoryLimit: 128 }) + let failure = null + try { + const context = isolate.createContextSync() + const prelude = buildPrelude(holder, wrappers, rotators, scopeWrappers) + try { + // **The timeout is a correctness instrument, not a safety net.** The compare-loop rotator + // searches until a checksum over the array's own contents matches, so it *cannot* terminate + // on an array it was not written for. Termination is therefore proof that the extraction + // was complete, and a timeout means it was not - which is the one signal that catches a + // missed component on a sample with no expected output to compare against. + context.evalSync(prelude, { timeout }) + } catch (e) { + failure = + `the extracted machinery did not evaluate (${e.message}); on a compare-loop ` + + `rotator this means the extraction was incomplete rather than that the sample is hostile` + } + + if (!failure) { + // Cached because the same index recurs across a program, and the wrappers are pure with + // respect to the array once the rotation has run. + const cache = new Map() + for (const site of sites) { + const wrapper = wrapperByNode.get(site.wrapper) + // **The callee is rewritten to the wrapper's own name before evaluating**, rather than + // the alias declarations being evaluated into the isolate the way one could. Renamed + // output reuses short names across non-overlapping scopes, so evaluating alias + // declarations can collide two distinct bindings onto one name. Resolution already knows + // which wrapper this site reached, so nothing is guessed. + const args = site.path.node.arguments.map(gen).join(',') + const call = `${wrapper.callName}(${args})` + let value = cache.get(call) + if (value === undefined) { + try { + value = context.evalSync(call, { timeout }) + } catch (e) { + failure = `call site ${gen(site.path.node)} did not evaluate (${e.message})` + break + } + if (typeof value !== 'string') { + failure = `call site ${gen(site.path.node)} returned ${typeof value}, not a string` + break + } + cache.set(call, value) + } + site.value = value + } + } + } finally { + isolate.dispose() + } + + if (failure) { + notes.push(`refusing: ${failure}`) + return result('unreadable', detected) + } + + // **The second rotator-miss guard, and the general one.** A missed rotator does not throw: it + // returns real strings from an unrotated array, so the output parses, runs, and reads clean on + // every residue axis while being wrong. What it cannot do is keep a property name in a computed + // member key valid, because the encoder put a real one there. Under a per-item-keyed encoding + // the same miss also produces non-ASCII bytes, but that tell is encoding-specific and this one + // is not. + // + // Deliberately strict: it is a guard, and loosening one before it has ever failed is how a + // guard stops guarding. + const keyViolations = sites.filter( + (site) => isComputedKey(site.path) && !IDENTIFIER_RE.test(site.value), + ) + if (keyViolations.length) { + notes.push( + `refusing: ${keyViolations.length} decoded string(s) in computed-member-key position are ` + + `not valid identifiers, e.g. ${JSON.stringify(keyViolations[0].value)} - the usual ` + + `cause is a component of the machinery that was not extracted`, + ) + return result('unreadable', detected) + } + + // ---- mutate ------------------------------------------------------------ + // + // Nothing above this line has touched the tree. Every `(call site, value)` pair is resolved + // first because the alternative cannot satisfy all-or-nothing: editing while sites are still + // being resolved leaves a half-decoded array behind on any later failure. + const rotatorPaths = pathsFor( + ast, + new Set(rotators.map((rotator) => rotator.call)), + ) + // Taken while the holder's path is still attached; the Scope object outlives the removals. + const programScope = holder.path.scope.getProgramParent() + + for (const site of sites) { + site.path.replaceWith(t.stringLiteral(site.value)) + } + + let removedAliases = 0 + for (const alias of detected.aliases) { + // Aliases the encoder injected *into* the machinery go with it; removing them separately + // would only detach paths the machinery removal still has to walk. + if (alias.path.removed || insideMachinery(alias.path)) { + continue + } + removeDeclarator(alias.path) + removedAliases += 1 + } + + let removedRotators = 0 + for (const rotator of rotators) { + const path = rotatorPaths.get(rotator.call) + if (path && !path.removed) { + removeEffectExpression(path) + removedRotators += 1 + } + } + + // Ahead of the root wrappers only for readability - what actually keeps this safe is that every + // call site was replaced above, so nothing references any of them by the time they go, and the + // `removed` guard covers a wrapper that a containing one already took with it. + let removedScopeWrappers = 0 + for (const scope of [...scopeWrappers].reverse()) { + if (scope.path.removed) { + continue + } + if (scope.decl === 'var-function-expression') { + removeDeclarator(scope.path) + } else { + scope.path.remove() + } + removedScopeWrappers += 1 + } + + let removedWrappers = 0 + for (const wrapper of wrappers) { + if (wrapper.path.removed) { + continue + } + if (wrapper.decl === 'var-function-expression') { + removeDeclarator(wrapper.path) + } else { + wrapper.path.remove() + } + removedWrappers += 1 + } + + let removedHolder = 0 + if (!holder.path.removed) { + if (holder.kind === 'var-declaration') { + removeDeclarator(holder.path) + } else { + holder.path.remove() + } + removedHolder = 1 + } + + // Crawl from the **program** scope, not from any one removal site. This pass deletes bindings in + // several scopes at once and replaces references in others, so crawling locally would leave the + // enclosing scopes' reference counts stale for whatever runs next - a cleanup sweep, typically, + // which is exactly the kind of pass that decides what to delete from a count. + programScope.crawl() + + debugLog( + `[obfuscatorx] string-array: ${sites.length} call site(s) decoded, removed ` + + `${removedHolder} holder, ${removedWrappers} wrapper(s), ` + + `${removedScopeWrappers} scope wrapper(s), ${removedRotators} rotator(s), ` + + `${removedAliases} alias(es)`, + ) + + return { + status: 'decoded', + signature: detected.signature, + replaced: sites.length, + removed: { + holder: removedHolder, + wrappers: removedWrappers, + scopeWrappers: removedScopeWrappers, + rotators: removedRotators, + aliases: removedAliases, + }, + notes, + } +} + +export default decodeStringArray diff --git a/src/visitor/obfuscator/unflatten-switch-dispatch.js b/src/visitor/obfuscator/unflatten-switch-dispatch.js new file mode 100644 index 00000000..735a9b90 --- /dev/null +++ b/src/visitor/obfuscator/unflatten-switch-dispatch.js @@ -0,0 +1,264 @@ +import * as t from '@babel/types' + +/** + * Undo javascript-obfuscator's block-statement control-flow flattening: a block whose statements + * have been shuffled into a `switch`, driven in the original order by a `"|"`-joined string of + * case indexes. + * + * { + * const C = '2|0|3|1|4'['split']('|'); + * let I = 0; + * while (true) { + * switch (C[I++]) { + * case '0': ; continue; + * ... + * case '4': return ; + * } + * break; + * } + * } + * + * **The reversal is a permutation read, and this pass is written as one.** The controller string + * holds, for each *original* statement position, the index of the case that now carries it. So + * reading it left to right and indexing the case list recovers the original order directly, in one + * step, with no scanning. + * + * A copy of the shared `remove-control-flow-ob.js` was rejected rather than adapted. That visitor + * reaches the same output on this encoder - it decodes every live flattened block in the frozen + * corpus - but it gets there by *walking forward from each index until it meets a `continue`*, a + * generality this encoder never needs, and three of its behaviours off that path are worse than + * declining. Its own doc records them. This file exists because a shared visitor cannot be + * narrowed without changing what two other plugins get, not because its output here was wrong. + * + * **Every check happens before any mutation.** The pass either rewrites a block completely or + * leaves it exactly as it found it; there is no point at which it has removed a declaration and + * then discovers it cannot continue. That is the difference that makes a decline safe, and it is + * why the invariants are gathered into `match()` rather than tested as it goes. + */ + +/** `while (true)`, and nothing else. */ +function isAlwaysTrueTest(node) { + return t.isBooleanLiteral(node, { value: true }) +} + +/** + * `C[I++]` - a computed read of one identifier indexed by the suffix increment of another. + * + * Checked in full before anything is read off it. The shared visitor checks only that the + * discriminant is a `MemberExpression` and then reads `property.argument.name`, which throws on a + * discriminant like `C[I]` and takes the whole decode down with it. + */ +function readDispatch(node) { + if (!t.isMemberExpression(node) || !node.computed) return null + if (!t.isIdentifier(node.object)) return null + const p = node.property + if (!t.isUpdateExpression(p) || p.operator !== '++' || p.prefix) return null + if (!t.isIdentifier(p.argument)) return null + return { controller: node.object.name, index: p.argument.name } +} + +/** + * A case that contributes no statement at all. Distinct from `null`, which is this function's + * reject signal - the two must not collapse, because one means "skip this slot" and the other + * means "do not touch this block". + */ +const EMPTY_CASE = Symbol('empty-case') + +/** The three consequent shapes this encoder emits, and no others. */ +function readConsequent(cs) { + if (cs.length === 2 && t.isContinueStatement(cs[1]) && !cs[1].label) + return cs[0] + if (cs.length === 1 && t.isReturnStatement(cs[0])) return cs[0] + // A bare `continue` is an EMPTY case, and the encoder does emit one: control-flow flattening + // runs at stage 4 and puts a function's leading directive into a case like any other statement, + // then `DirectivePlacementTransformer` re-emits that directive at the top of the scope during + // `Finalizing` - leaving the case it came from with nothing in it. Any function whose body opens + // with `'use strict'` and gets flattened produces this, which is a large slice of real-world + // input. Omitting the slot is the whole reversal: the order string is a permutation, so the case + // is visited exactly once, and executing it does nothing but return to the loop. + if (cs.length === 1 && t.isContinueStatement(cs[0]) && !cs[0].label) + return EMPTY_CASE + return null +} + +/** + * Resolve a name to the declarator that binds it: a declarator in a **preceding sibling + * declaration** of the loop, in the same block. + * + * Scanning previous siblings handles `simplify`'s fused `var C = …, I = 0;` for free, since it + * walks each declaration's declarator list. Resolving by *binding* instead is unsound, and a + * hand-built case caught it: + * + * function f() { if (x) { var C = '1|0'.split('|'); var I = 0; } + * while (true) { switch (C[I++]) { … } break; } } + * + * `var` hoists, so the binding resolves - but the *initialisation* is conditional. When `x` is + * falsy `C` is `undefined`, the switch matches nothing, and the block does nothing. Rewriting it + * to the linear body would run statements the original never runs. Requiring the declaration to + * precede the loop as a sibling is a cheap sufficient condition for the initialisation dominating + * the dispatch, and it is exactly what the shared visitor's previous-sibling scan enforced by + * accident. + * + * **It reads the current tree, not the scope's cached bindings, and that is not a style choice.** + * `scope.getBinding()` hands back a path recorded at the last crawl, and the passes that must run + * before this one - constant folding, branch pruning, the storage inlining - replace nodes + * wholesale. The binding then points at a detached node that still *prints* identically to the one + * in the tree, so an identity comparison against it silently fails and every real block declines. + * Measured: resolving by binding rejected all 108 live corpus blocks while accepting hand-built + * ones, because only the hand-built trees had never been rewritten. + */ +function resolveDeclarator(path, name) { + for (const sibling of path.getAllPrevSiblings()) { + if (!sibling.isVariableDeclaration()) continue + for (let i = 0; i < sibling.node.declarations.length; i++) { + const declarator = sibling.get(`declarations.${i}`) + if (t.isIdentifier(declarator.node.id, { name })) return declarator + } + } + return null +} + +/** + * Is every mention of `name` inside `path`? + * + * Asked by walking the enclosing statement list rather than by reading `binding.referencePaths`, + * for the same staleness reason as above. Cheap, since the search is bounded by the block the loop + * sits in. + */ +function usedOnlyInside(path, name) { + let outside = 0 + const parent = path.parentPath + parent.traverse({ + Identifier(id) { + if (id.node.name !== name) return + if (id.findParent((q) => q === path)) return + // the declarator's own id is a binding site, not a use + if ( + id.parentPath.isVariableDeclarator() && + id.parentPath.node.id === id.node + ) + return + outside++ + }, + }) + return outside === 0 +} + +/** `'2|0|3'['split']('|')` or `'2|0|3'.split('|')`, after constant folding has run. */ +function readOrderString(init) { + if (!t.isCallExpression(init) || init.arguments.length !== 1) return null + if (!t.isStringLiteral(init.arguments[0], { value: '|' })) return null + const callee = init.callee + if (!t.isMemberExpression(callee)) return null + const prop = callee.property + const name = t.isStringLiteral(prop) + ? prop.value + : t.isIdentifier(prop) + ? prop.name + : null + if (name !== 'split') return null + if (!t.isStringLiteral(callee.object)) return null + return callee.object.value +} + +/** + * Decide whether this `while` is a flattened block, and gather everything the rewrite needs. + * + * Returns `null` for anything that is not exactly the emitted shape. The strictness is the point: + * an ordinary `while (!done) { switch (state) { ... } break; }` is a perfectly common thing to + * write, and a looser gate rewrites it into straight-line code that runs once. + */ +function match(path) { + const node = path.node + if (!isAlwaysTrueTest(node.test)) return null + if (!t.isBlockStatement(node.body) || node.body.body.length !== 2) return null + const [head, tail] = node.body.body + if (!t.isSwitchStatement(head)) return null + if (!t.isBreakStatement(tail) || tail.label) return null + + const dispatch = readDispatch(head.discriminant) + if (!dispatch) return null + + const cases = head.cases + if (!cases.length) return null + + // Case tests are `String(i)` in ascending order, and there is no `default`. A `default` would + // mean an execution path the order string does not describe. + for (let i = 0; i < cases.length; i++) { + if (!t.isStringLiteral(cases[i].test, { value: String(i) })) return null + } + + // Every consequent is one of the two emitted shapes. Anything else - a bare fallthrough, an + // extra statement, a labelled `continue` - means the tree is not what this pass assumes, and + // walking forward through it is what duplicates statements. + const statements = cases.map((c) => readConsequent(c.consequent)) + if (statements.some((s) => s === null)) return null + + const controllerDecl = resolveDeclarator(path, dispatch.controller) + const indexDecl = resolveDeclarator(path, dispatch.index) + if (!controllerDecl || !indexDecl) return null + + const order = readOrderString(controllerDecl.node.init) + if (order === null) return null + + const indexInit = indexDecl.node.init + if (!t.isNumericLiteral(indexInit, { value: 0 })) return null + + // The order is a permutation of the case indexes: same length, every index once. A shorter or + // longer order string, or a repeated index, means the two halves disagree about the block. + const parts = order.split('|') + if (parts.length !== cases.length) return null + const seen = new Set() + for (const part of parts) { + if (!/^\d+$/.test(part)) return null + const idx = Number(part) + if (idx >= cases.length || seen.has(idx)) return null + seen.add(idx) + } + + // Neither control variable may be touched anywhere but inside this loop, or removing its + // declaration changes what the surrounding code can see. + // + // Stated as containment rather than as a count, because the index is *necessarily* mutated - + // `I++` is the dispatch - so a "no writes" gate can never pass, and a "one reference" gate + // depends on whether Babel books that increment as a read, a write, or both. Asking whether + // every use sits under the node being replaced is the property actually needed, and it does not + // depend on that bookkeeping. + if (!usedOnlyInside(path, dispatch.controller)) return null + if (!usedOnlyInside(path, dispatch.index)) return null + + return { statements, parts, controllerDecl, indexDecl } +} + +/** + * @param {(info: object) => void} [onChange] notified once per rewritten block, so a caller can + * run a pipeline to a fixpoint without re-serializing the tree. + */ +export function createUnflattenSwitchDispatch(onChange) { + return { + WhileStatement: { + exit(path) { + const found = match(path) + if (!found) return + const { statements, parts, controllerDecl, indexDecl } = found + + // Read the controller left to right: position k names the case holding original + // statement k. Empty cases drop out here rather than at the gate, so the permutation + // check above still sees every index. + const rebuilt = parts + .map((part) => statements[Number(part)]) + .filter((s) => s !== EMPTY_CASE) + + controllerDecl.remove() + indexDecl.remove() + // Every case empty is a block that does nothing. `replaceWithMultiple([])` is not a + // removal, so ask for one explicitly. + if (rebuilt.length === 0) path.remove() + else path.replaceWithMultiple(rebuilt) + if (onChange) onChange({ count: rebuilt.length }) + }, + }, + } +} + +export default createUnflattenSwitchDispatch() diff --git a/src/visitor/obfuscator/unlock-env.js b/src/visitor/obfuscator/unlock-env.js new file mode 100644 index 00000000..369dfab8 --- /dev/null +++ b/src/visitor/obfuscator/unlock-env.js @@ -0,0 +1,361 @@ +import traverse from '@babel/traverse' +import * as t from '@babel/types' + +import logger from '../../utility/logger.js' + +const debugLog = logger.debugLog + +/** + * Strip javascript-obfuscator's custom code helpers: the self-defending guard, the console-output + * disabler, the debug-protection trap, and the calls controller the three of them run through. + * + * The encoder injects these at its *second* stage, so by the time anything here sees them they + * have been through dead-code injection, control-flow flattening, literal re-spelling, renaming + * and the string array. This pass therefore assumes those have already been reversed: it matches + * decoded shapes, and it is the last thing in the pipeline rather than the first. + * + * Each protection is emitted as two pieces in *different scopes* - a definition and a trigger - + * plus a per-group calls controller: + * + * var C = (function () { // the calls controller, one PER GROUP + * var first = true; + * return function (context, fn) { + * var r = first ? function () { if (fn) { ... } } : function () {}; + * first = false; + * return r; + * }; + * })(); + * + * var G = C(this, function () { ... }); // the definition + * G(); // the trigger + * + * Debug protection is the exception: its guard is invoked where it is built, inside an IIFE with + * no name bound to it, and it additionally emits a top-level `function D(ret) { ... }` and an + * optional `setInterval` firing it. + * + * **Deleting the controller with its guard is only safe because of an encoder property**, not a + * decoder one: each helper group builds its *own* controller rather than sharing one, so a + * controller never has a second guard depending on it. This pass verifies that per controller + * instead of assuming it, and declines when it does not hold - which is what would happen against + * a variant encoder that shared them. + * + * **The console-output guard references its own controller from inside its callback** + * (`C.constructor.prototype.bind(C)`, `C.bind(C)`). So a liveness test on the controller must not + * count references that live inside the guard being removed. Here that is explicit: references are + * partitioned before anything is deleted, rather than relying on deleting the guard first and + * re-crawling. + * + * **Match completely, then mutate.** Every gate is checked before the first removal, so a guard is + * either taken out whole or left exactly as found. Declining costs legible residue that a census + * counts; a half-removed guard leaves a program referencing a binding that no longer exists. + */ + +/** `function (...) {}` or `(...) => {}`, with a block body. */ +function isFnWithBlock(node) { + return ( + (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) && t.isBlockStatement(node.body) + ) +} + +/** + * A member key in the two spellings that reach this point: `o.k` and `o['k']`. + * + * Reading only the first is the trap that makes a matcher accept hand-built cases and reject every + * real one - our own `Converting` reversal un-computes most keys, but not all of them, so both + * spellings are live in this pass's input. + */ +function memberKey(node) { + if (!t.isMemberExpression(node)) return null + if (!node.computed && t.isIdentifier(node.property)) return node.property.name + if (node.computed && t.isStringLiteral(node.property)) return node.property.value + return null +} + +/** + * Count nodes under `node` matching `pred`. + * + * A plain walk rather than a Babel traversal: the subjects here are block statements and function + * bodies, which `traverse` cannot be pointed at without a path or a synthetic program wrapper, and + * wrapping is what makes such a helper throw on the first node type it cannot convert. + */ +function countNodes(node, pred) { + let n = 0 + const walk = (x) => { + if (!x || typeof x.type !== 'string') return + if (pred(x)) n++ + for (const key of t.VISITOR_KEYS[x.type] || []) { + const v = x[key] + if (Array.isArray(v)) v.forEach(walk) + else walk(v) + } + } + walk(node) + return n +} + +/** + * The calls controller, matched on shape: an immediately-invoked function taking no arguments, + * whose body returns a two-parameter function that chooses between two function expressions. + * + * Keyed on the choice rather than on the `firstCall` flag, because the flag is a renamed local and + * the conditional is the part that carries the meaning - the wrapper runs its target once. + */ +function isCallsControllerInit(node) { + if (!t.isCallExpression(node) || node.arguments.length || !isFnWithBlock(node.callee)) return false + return ( + countNodes(node.callee.body, (n) => + isFnWithBlock(n) && + n.params.length === 2 && + countNodes(n.body, (c) => + t.isConditionalExpression(c) && isFnWithBlock(c.consequent) && isFnWithBlock(c.alternate)) > 0) > 0 + ) +} + +/** + * Which protection a guard callback implements, or `null` for a shape this pass does not know. + * + * Returning `null` is a decline, never a default: an unrecognised callback is left in place so a + * residue census still counts it. The alternative - treating "matched the wrapper" as enough - + * would delete arbitrary code that happens to be called as `C(this, fn)`. + */ +function classifyGuard(body) { + const searches = countNodes(body, (n) => t.isCallExpression(n) && memberKey(n.callee) === 'search') + if (searches >= 2) return 'self-defending' + + const regexps = countNodes(body, (n) => t.isNewExpression(n) && t.isIdentifier(n.callee, { name: 'RegExp' })) + if (regexps >= 2) return 'debug-protection-call' + + const methodList = countNodes(body, (n) => { + if (!t.isArrayExpression(n) || n.elements.length < 5) return false + const strings = n.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value) + return strings.length >= 5 && strings.includes('log') && strings.includes('warn') + }) + if (methodList > 0) return 'console-output' + + // The era below `E-selfdef-search` builds its regexp through `constructor` rather than `RegExp`, + // and is recognised by the nested function it declares and immediately calls. Checked last + // because it is the loosest of the four. + const nested = countNodes(body, (n) => isFnWithBlock(n) || t.isFunctionDeclaration(n)) + if (nested > 0) return 'self-defending' + + return null +} + +/** Is `path` inside `ancestor`? Asked of the live tree, never of cached positions. */ +function isInside(path, ancestorNode) { + return path.findParent((p) => p.node === ancestorNode) !== null +} + +/** + * The outermost statement that exists only to hold this guard. + * + * Debug protection's guard is invoked where it is built, inside an IIFE that wraps nothing else: + * + * (function () { C(this, function () { … })(); })(); + * + * Removing the inner statement leaves `(function () {})();` behind - a statement with no effect + * that no census keyed on the *encoder's* shapes can see, because the encoder never emits it. We + * do. So the removal target is computed by ascending through every wrapper whose body holds this + * statement and nothing else, and the ascent is written as a loop because nesting depth is not + * something to assume. + */ +/** + * What to delete for a single effect. + * + * The encoder's adjacent-statement merging fuses neighbouring statements into one sequence + * expression, and it does not care whose they are - a removal target of ours can end up sharing a + * statement with the program's own calls. Deleting the statement then deletes those too, which is + * corruption rather than residue and is silent: the program runs and simply stops producing + * output. So delete the sequence *element* when there is one, and the statement otherwise. + */ +function effectPath(callPath) { + return callPath.parentPath && callPath.parentPath.isSequenceExpression() + ? callPath + : callPath.getStatementParent() +} + +function outermostWrapperStatement(guardCall) { + // A fused guard yields its own element here, not a statement; the loop below then declines to + // walk outward on its first test, which is correct - there is no wrapper to unwrap. + let stmt = effectPath(guardCall) + for (;;) { + const block = stmt.parentPath + if (!block || !block.isBlockStatement()) break + if (block.node.body.length !== 1 || block.node.body[0] !== stmt.node) break + const fn = block.parentPath + if (!fn || !isFnWithBlock(fn.node) || fn.node.params.length) break + const call = fn.parentPath + if (!call || !call.isCallExpression() || call.node.callee !== fn.node || call.node.arguments.length) break + const outer = call.getStatementParent() + if (!outer || !outer.isExpressionStatement()) break + stmt = outer + } + return stmt +} + +/** + * Remove one guard and the controller it runs through, or leave both untouched. + * + * Returns the protection's name when it removed something, `null` when it declined. + */ +function stripGuard(controllerPath, removed) { + const id = controllerPath.node.id + if (!t.isIdentifier(id)) return null + const binding = controllerPath.scope.getBinding(id.name) + if (!binding) return null + + // Partition the controller's references BEFORE touching anything: the calls that build a guard, + // and everything else. The console-output callback puts several of the latter inside the former. + const guardCalls = [] + const others = [] + for (const ref of binding.referencePaths) { + const parent = ref.parentPath + if ( + parent && parent.isCallExpression() && parent.node.callee === ref.node && + parent.node.arguments.length === 2 && + t.isThisExpression(parent.node.arguments[0]) && isFnWithBlock(parent.node.arguments[1]) + ) { + guardCalls.push(parent) + } else { + others.push(ref) + } + } + if (guardCalls.length !== 1) { + debugLog(`unlock-env: declining ${id.name}, ${guardCalls.length} guards on one controller`) + return null + } + const guardCall = guardCalls[0] + const callback = guardCall.node.arguments[1] + const kind = classifyGuard(callback.body) + if (!kind) { + debugLog(`unlock-env: declining ${id.name}, unrecognised guard callback`) + return null + } + // Every remaining reference must be inside the callback we are about to delete. One that is not + // means something else uses this controller, and deleting it would break that caller. + if (others.some((ref) => !isInside(ref, callback))) { + debugLog(`unlock-env: declining ${id.name}, controller referenced outside its guard`) + return null + } + + // Two definition shapes. `var G = C(this, fn); G()` binds a name, and the trigger is a separate + // statement; debug protection's call form invokes the guard where it is built and binds nothing. + const declarator = guardCall.parentPath.isVariableDeclarator() ? guardCall.parentPath : null + let triggers = [] + if (declarator) { + if (!t.isIdentifier(declarator.node.id)) return null + const guardBinding = declarator.scope.getBinding(declarator.node.id.name) + if (!guardBinding) return null + for (const ref of guardBinding.referencePaths) { + const call = ref.parentPath + if (call && call.isCallExpression() && call.node.callee === ref.node && !call.node.arguments.length) { + triggers.push(call.parentPath.isExpressionStatement() ? call.parentPath : call) + } else if (!isInside(ref, callback)) { + debugLog(`unlock-env: declining ${id.name}, guard referenced outside its own trigger`) + return null + } + } + } + + // --- past every gate; only now does anything move --- + for (const trigger of triggers) trigger.remove() + if (declarator) { + declarator.remove() + } else { + // the guard is invoked in place: remove the whole wrapper that exists only to hold it + outermostWrapperStatement(guardCall).remove() + } + controllerPath.remove() + removed.push(kind) + return kind +} + +/** + * The debug-protection function and whatever fires it. + * + * Matched on its two-statement body - a nested function declaration and a `try`/`catch` - which is + * the shape the encoder's template guarantees and which no program body reaches by accident. + * Removed after the guards, because the guard callback that calls this function is one of its + * references and must be gone before the rest can be read. + */ +function stripDebugProtectionFunction(path, removed) { + const { id, params, body } = path.node + if (!t.isIdentifier(id) || params.length !== 1 || body.body.length !== 2) return + if (!t.isFunctionDeclaration(body.body[0]) || !t.isTryStatement(body.body[1])) return + + const binding = path.scope.getBinding(id.name) + if (!binding) return + + // Each remaining reference must be an interval firing it. Anything else and the function is + // still doing work we do not understand, so it stays. + const intervals = [] + for (const ref of binding.referencePaths) { + const fnParent = ref.getFunctionParent() + const call = fnParent && fnParent.parentPath + if (call && call.isCallExpression() && t.isIdentifier(call.node.callee, { name: 'setInterval' })) { + intervals.push(effectPath(call)) + continue + } + if (ref.parentPath.isCallExpression() && + t.isIdentifier(ref.parentPath.node.callee, { name: 'setInterval' })) { + intervals.push(effectPath(ref.parentPath)) + continue + } + debugLog(`unlock-env: declining ${id.name}, debug-protection referenced outside an interval`) + return + } + + for (const interval of intervals) if (!interval.removed) interval.remove() + path.remove() + removed.push('debug-protection') +} + +/** + * Strip every custom code helper the sample carries. Returns the list of protections removed, so a + * caller can report what it did rather than inferring it from a diff. + */ +export default function unlockEnv(ast) { + const removed = [] + + // This pass's gates read `binding.referencePaths`, so it depends on those references still + // pointing into the live tree. That is now guaranteed by the pass that breaks it rather than + // defended here: `prune-if-branch` detaches subtrees and crawls on its way out, so no stale + // reference reaches this point. This pass previously opened with a `traverse.cache.clear()` to + // repair it, which was a symptom patch at the consumer - removed once the producer was fixed, + // and its removal verified byte-identical over the corpus. + // + // Guards first. Each one's callback holds references to the debug-protection function and to its + // own controller, so removing guards is what makes the remaining references readable. + traverse(ast, { + VariableDeclarator(path) { + if (!isCallsControllerInit(path.node.init)) return + stripGuard(path, removed) + }, + }) + + // Re-crawl before reading any binding again. The guard removals above detached nodes, and a + // binding records its references as of the last crawl - so without this the debug-protection + // function still lists the reference that lived inside the guard callback we just deleted, that + // reference matches none of the accepted shapes, and the pass declines on every sample that has + // one. This is the stale-scope trap, and the tell was exactly the one on record - a matcher that + // accepts a freshly parsed tree and rejects every tree a pass has touched. + // + // The crawl is the whole remedy here, because the only nodes detached before this point are the + // ones this pass's own first phase removed, and a crawl repairs what it detached. It used to be + // half of one: a `traverse.cache.clear()` opened this function to absorb staleness inherited + // from `prune-if-branch`, which is now repaired at that producer instead. + traverse(ast, { + Program(path) { + path.scope.crawl() + }, + }) + + traverse(ast, { + FunctionDeclaration(path) { + stripDebugProtectionFunction(path, removed) + }, + }) + + if (removed.length) debugLog(`unlock-env: removed ${removed.join(', ')}`) + return ast +} diff --git a/src/visitor/prune-if-branch.js b/src/visitor/prune-if-branch.js index 620540f5..1bd2a3d9 100644 --- a/src/visitor/prune-if-branch.js +++ b/src/visitor/prune-if-branch.js @@ -37,8 +37,18 @@ function replaceWithBranch(path, branch) { path.replaceWithMultiple(branch.body) } +/** + * Whether this traversal detached anything, so the exit handler knows if a crawl is owed. + * + * Module-level because a visitor object carries no per-run state. Safe because every consumer + * invokes this as a standalone, synchronous `traverse(ast, pruneIfBranch)` and `Program.enter` + * resets it at the start of each run. + */ +let detachedSomething = false + function pruneIfBranch(path) { function clear(path, toggle) { + detachedSomething = true // 判定成立 if (toggle) { replaceWithBranch(path, path.node.consequent) @@ -71,9 +81,30 @@ function pruneIfBranch(path) { /** * Prune the branch if the test is constant * - * The code must be reloaded to update the references + * Removing a branch detaches its subtree, and Babel leaves every *other* binding's + * `referencePaths` pointing into it. Such a reference reports `removed === false` and its cached + * parent chain still reaches a Program, so only node reachability can see the difference — which + * is why this went unnoticed long enough to be documented as a caller's problem rather than + * repaired here. It is not a caller's problem. Of the four consuming plugins, three re-parse the + * whole program immediately after calling this visitor (`// 刷新代码`) — the same repair written + * out three times — and the fourth does not re-parse at all, so it carried the exposure with + * nothing answering for it. A fifth consumer, composing on a single AST, inherited stale bindings + * and emitted code that threw. + * + * So the invariant is restored where it is broken: one `scope.crawl()` on the way out, and only + * when something was actually detached. A crawl cannot change the tree — it rebuilds cached scope + * information — so this is invisible to output and costs nothing on a traversal that pruned + * nothing. */ export default { + Program: { + enter() { + detachedSomething = false + }, + exit(path) { + if (detachedSomething) path.scope.crawl() + }, + }, IfStatement: pruneIfBranch, ConditionalExpression: pruneIfBranch, } diff --git a/src/visitor/split-assignment.js b/src/visitor/split-assignment.js index ebbe171c..6390ceae 100644 --- a/src/visitor/split-assignment.js +++ b/src/visitor/split-assignment.js @@ -53,17 +53,26 @@ function getInsertPath(path) { return insertPath } +/** + * Whether this traversal split anything, so the exit handler knows if a crawl is owed. Module-level + * because a visitor object carries no per-run state; `Program.enter` resets it per run. + */ +let splitSomething = false + function procAssignment(path) { const insertPath = getInsertPath(path) if (!insertPath) { return } insertPath.insertBefore(t.expressionStatement(path.node)) - path.replaceWith(path.node.left) - // Crawl from the program scope: a moved assignment can reference bindings in - // an enclosing scope, so crawling only insertPath.scope would leave those - // outer bindings with stale reference counts. - insertPath.scope.getProgramParent().crawl() + // Clone the target rather than re-using it. `path.node` has just been re-homed into the inserted + // statement, so `path.node.left` is already live there; handing that same node object back here + // would leave one node reachable at two positions - measured as two such nodes on one real + // sample. That is not a bookkeeping wart a crawl can repair, because the tree really does hold it + // twice: a later pass replacing both occurrences finds the second one's parent slot already + // rewritten, resyncs to a null key, and throws inside Babel's validator. + path.replaceWith(t.cloneNode(path.node.left, true)) + splitSomething = true } /** @@ -71,7 +80,32 @@ function procAssignment(path) { * * - In the test of IfStatement * - In the VariableDeclaration + * + * **The crawl restores an invariant this rewrite breaks**: after a pass, the scope information + * Babel has cached should equal what a fresh parse of that pass's own output would produce. The + * moved assignment is re-homed by `insertBefore`, and re-homing a subtree registers its references + * a second time - so a binding can end up listing the same live node twice, with nothing detached. + * A later consumer that gates a deletion on "have I resolved every reference to this binding" then + * passes that check while a live reference goes unhandled. + * + * **It must be program-scoped**, because a moved assignment can reference bindings in an enclosing + * scope and crawling only `insertPath.scope` would leave those outer bindings inconsistent - the + * defect this file was fixed for once already. + * + * **Once per traversal rather than once per split**, which is the only thing that changed since: + * the previous form crawled the whole program on every rewrite, so a sample with many splits paid + * for the entire program each time. Deferring is safe because nothing in this pass reads scope + * state - `getInsertPath` walks `parentPath` and tests node types and keys, never a binding - so no + * later invocation in the same traversal depends on the crawl having already run. */ export default { + Program: { + enter() { + splitSomething = false + }, + exit(path) { + if (splitSomething) path.scope.crawl() + }, + }, AssignmentExpression: procAssignment, } diff --git a/src/visitor/split-sequence.js b/src/visitor/split-sequence.js index 5ffdad73..64d19dd9 100644 --- a/src/visitor/split-sequence.js +++ b/src/visitor/split-sequence.js @@ -1,5 +1,11 @@ import * as t from '@babel/types' +/** + * Whether this traversal split anything, so the exit handler knows if a crawl is owed. Module-level + * because a visitor object carries no per-run state; `Program.enter` resets it per run. + */ +let splitSomething = false + function doSplit(insertPath, path) { const expressions = path.node.expressions const lastExpression = expressions.pop() @@ -7,7 +13,7 @@ function doSplit(insertPath, path) { insertPath.insertBefore(t.expressionStatement(expressions.shift())) } path.replaceWith(lastExpression) - insertPath.scope.crawl() + splitSomething = true } function splitSequence(path) { @@ -47,7 +53,29 @@ function splitSequence(path) { * - VariableDeclarator * - ReturnStatement * - ExpressionStatement + * + * **The crawl restores an invariant this rewrite breaks**: after a pass, the scope information + * Babel has cached should equal what a fresh parse of that pass's own output would produce. + * `insertBefore` and `replaceWith` are both handed expressions lifted out of the existing sequence, + * and re-homing a subtree registers its references a second time - so `binding.referencePaths` + * lists the same live node twice and `binding.references` counts it twice, with nothing detached. + * Measured on one real sample: 32 duplicate entries, 1480 recorded references against 1448 real + * ones. A later consumer that gates a deletion on "have I resolved every reference" then passes + * that check while a live reference goes unhandled, and deletes a declaration still in use. + * + * It replaces a `scope.crawl()` that used to run inside `doSplit`, which was wrong twice over: it + * fired once per split rather than once per traversal, and it was scoped to the insertion point's + * scope rather than the program's - and crawling a narrower scope *adds* duplicates, by appending + * to outer-scope bindings that already hold those references. */ export default { + Program: { + enter() { + splitSomething = false + }, + exit(path) { + if (splitSomething) path.scope.crawl() + }, + }, SequenceExpression: splitSequence, } diff --git a/src/visitor/split-variable-declaration.js b/src/visitor/split-variable-declaration.js index 1d61a8b4..bbe42b49 100644 --- a/src/visitor/split-variable-declaration.js +++ b/src/visitor/split-variable-declaration.js @@ -1,5 +1,11 @@ import * as t from '@babel/types' +/** + * Whether this traversal split anything, so the exit handler knows if a crawl is owed. Module-level + * because a visitor object carries no per-run state; `Program.enter` resets it per run. + */ +let splitSomething = false + function splitVariableDeclaration(path) { // The scope of a for statement is its body if (path.parentPath.isFor()) { @@ -18,14 +24,38 @@ function splitVariableDeclaration(path) { path.insertBefore(t.variableDeclaration(kind, [item])) } path.remove() - path.scope.crawl() + splitSomething = true } /** * Split the VariableDeclaration if it has more than one VariableDeclarator * * This operation will only be performed when its container is an array + * + * **The crawl restores an invariant this rewrite breaks**: after a pass, the scope information + * Babel has cached should equal what a fresh parse of that pass's own output would produce. Each + * declarator is re-homed into a new declaration by `insertBefore`, and re-homing a subtree + * registers its references a second time, with nothing detached - the same live node listed twice. + * + * **This replaces a `path.scope.crawl()` that used to run here and made things worse.** All three + * options were measured on one real sample against a 1448-reference baseline: the old + * `path.scope.crawl()` gave **37 duplicates** and 1485 references, because crawling a scope + * narrower than the program appends to outer-scope bindings that already hold those references; + * removing the crawl entirely **lost** references, reporting 1096; and one program-scoped crawl on + * the way out gives exactly 1448 with none duplicated. Note that `path` has been removed by then, + * so its own `.scope` is not the right thing to ask - `Program.exit` is. + * + * Gated because a crawl is only owed when something moved, and a crawl cannot change the tree - so + * this is invisible to output and free on a traversal that split nothing. */ export default { + Program: { + enter() { + splitSomething = false + }, + exit(path) { + if (splitSomething) path.scope.crawl() + }, + }, VariableDeclaration: splitVariableDeclaration, } diff --git a/test/helper.js b/test/helper.js index 44e7fba2..efdf1ae4 100644 --- a/test/helper.js +++ b/test/helper.js @@ -3,6 +3,81 @@ import { expect } from 'vitest' import { parse } from '@babel/parser' import generate from '@babel/generator' import traverse from '@babel/traverse' +import * as t from '@babel/types' + +// Every node reachable from the Program right now. +function liveNodes(ast) { + const live = new Set() + const walk = (node) => { + if (!node || typeof node.type !== 'string' || live.has(node)) return + live.add(node) + for (const key of t.VISITOR_KEYS[node.type] || []) { + const value = node[key] + if (Array.isArray(value)) value.forEach(walk) + else walk(value) + } + } + walk(ast.program || ast) + return live +} + +// Node objects reachable from the Program at more than one position. +// +// An AST is a tree, not a DAG, and a pass breaks that by re-homing a node and then handing one of +// its own subtrees back to another path API - `insertBefore(path.node)` followed by +// `replaceWith(path.node.left)` puts that `left` in two places at once. The text still generates +// correctly, so nothing about the output betrays it. +// +// **No crawl repairs this**, which is what separates it from `detachedReferences` above: a crawl +// records the node twice because it genuinely is reachable twice. The damage is deferred to +// whichever later pass resolves both occurrences - replacing the second finds its parent slot +// already holding what the first replacement put there, the path resyncs to a null key, and +// Babel's validator throws. Clone the subtree rather than re-using it. +export function aliasedNodes(ast) { + const seen = new Set() + const aliased = [] + const walk = (node) => { + if (!node || typeof node.type !== 'string') return + if (seen.has(node)) { + aliased.push(node.type) + return + } + seen.add(node) + for (const key of t.VISITOR_KEYS[node.type] || []) { + const value = node[key] + if (Array.isArray(value)) value.forEach(walk) + else walk(value) + } + } + walk(ast.program || ast) + return aliased.sort() +} + +// Bindings still pointing at nodes that are no longer in the tree. +// +// A pass that removes nodes leaves the bindings of *other* names holding references into the +// subtree it detached, and every later pass reading `binding.referencePaths` then decides against a +// program that no longer exists. Measured on a real pipeline, the first removing pass detached 90 +// of 320 references at a stroke and no later pass cleared them by itself. +// +// Reachability is the only check that sees it, which is why this walks the tree rather than asking +// the path. A stale reference reports `removed === false`, and `ref.find((p) => p.isProgram())` is +// truthy, because the cached parent chain still links it to a Program path object. +export function detachedReferences(ast) { + const live = liveNodes(ast) + const stale = [] + traverse(ast, { + Scopable(path) { + const { bindings } = path.scope + for (const name of Object.keys(bindings).sort()) { + for (const ref of bindings[name].referencePaths) { + if (!live.has(ref.node)) stale.push(`${name}: ${ref.node.type}`) + } + } + }, + }) + return stale.sort() +} // Snapshot every binding's reference bookkeeping, reading scope as Babel has // it cached — i.e. the state the visitor left behind. Babel does not re-crawl @@ -28,24 +103,107 @@ function referenceState(ast) { return state } +/** + * Assert the half of a pass's contract that its output text cannot show. + * + * A pass rewrites the tree *and* the derived state Babel keeps beside it. Leave that inconsistent + * and the emitted text is perfect byte for byte, while every later pass consulting it decides + * against a program that no longer exists. Three independent checks, because they fail differently + * and no one of them sees the others: + * + * - `detachedReferences` - a binding still points at a node the tree no longer holds; + * - `aliasedNodes` - one node object sits at two positions, which no crawl repairs; + * - `referenceState` - the cached reference bookkeeping disagrees with a fresh parse of this + * pass's own output, which is what an inflated or lost reference count looks like. + * + * **Exported so a runner that cannot use the helpers below still inherits the audit.** Passes here + * come in two shapes - a Babel visitor object, and a function taking the AST - and `getVisitorResult` + * only accepts the first. A test for a function-shaped pass therefore had to roll its own runner, + * and every such runner silently opted out of all three checks. That is how four visitors shipped + * inflated reference counts: not because the oracle was missing, but because it was only reachable + * from a helper their pass could not be passed to. + * + * `cmpCode` is the expected output. **Omit it and the comparison is made against a fresh parse of + * the tree's own generated output instead**, which is the invariant stated literally and needs no + * golden: after a pass, its derived state should equal what a fresh parse of its own output would + * produce. That matters where a golden cannot honestly be reviewed - a fixture harvested from real + * obfuscated output is thousands of bytes of generated identifiers, and committing a `.fix.js` + * nobody can read only makes exact string equality look authoritative. Self-comparison pins the + * claim that is actually being made about such a case, and no more. + */ +export function expectConsistentState(ast, cmpCode, parseOptions = {}) { + expect(detachedReferences(ast)).toEqual([]) + expect(aliasedNodes(ast)).toEqual([]) + const expectedSource = cmpCode === undefined ? generate(ast).code : cmpCode + expect(referenceState(ast)).toEqual( + referenceState(parse(expectedSource, parseOptions)), + ) +} + export function getVisitorResult(visitor, fix, input) { const sourceCode = fs.readFileSync(input + '.js', { encoding: 'utf-8' }) const ast = parse(sourceCode) traverse(ast, visitor) + // The state audit applies to no-op cases too, and deliberately: "output unchanged" is not + // "nothing happened", so a visitor that removes and rebuilds can leave stale references while + // the text round-trips. Only the reference-state comparison is fix-only - a case that does not + // mutate has nothing to compare against. + // + // This catches what the visitor inflicts on ITSELF. Staleness inherited from an earlier pass + // cannot arise here — one visitor, one fresh parse — so it is getPipelineResult that covers the + // class a real pipeline has. if (fix) { const cmpCode = fs.readFileSync(input + '.fix.js', { encoding: 'utf-8' }) expect(generate(ast).code).toBe(cmpCode) - // Reference integrity (fix cases only): the transformed AST's scope must - // match a fresh parse of the expected output. A missing or mis-scoped - // crawl() leaves stale reference counts that this catches even though the - // generated text is identical. No-op (fix === false) cases don't mutate - // the tree, so the check would be redundant there. - expect(referenceState(ast)).toEqual(referenceState(parse(cmpCode))) + expectConsistentState(ast, cmpCode) } else { expect(generate(ast).code).toBe(sourceCode) + expectConsistentState(ast) } } +/** + * Run several passes on ONE AST, the way the pipeline does, and check the result. + * + * `getVisitorResult` parses fresh and runs a single visitor, so no earlier pass has detached + * anything and inherited staleness is unreachable by construction. That is the class that actually + * bites: a pass certified alone, and against fixtures that were built by running the earlier passes + * and then *writing the result to disk*, is certified across a re-parse — and re-parsing rebuilds + * every path from text, silently repairing the one state a real pipeline carries forward. One pass + * here read clean on every residue axis and on runtime equivalence over a whole corpus that way, + * while being wrong on 108 cells the moment the same pipeline ran on a single AST. + * + * So the fixture input must be what the EARLIER passes leave in memory, never a pre-baked file. + * + * `passes` are applied in order; each is either a Babel visitor object or a function taking the + * AST, since the passes in this project come in both shapes. + */ +export function getPipelineResult(passes, fix, input) { + const sourceCode = fs.readFileSync(input + '.js', { encoding: 'utf-8' }) + const ast = parse(sourceCode, { + allowReturnOutsideFunction: true, + errorRecovery: true, + }) + for (const pass of passes) { + if (typeof pass === 'function') pass(ast) + else traverse(ast, pass) + } + const cmpCode = fix + ? fs.readFileSync(input + '.fix.js', { encoding: 'utf-8' }) + : sourceCode + expect(generate(ast).code).toBe(cmpCode) + // The same audit `getVisitorResult` runs, and it matters more here, not less: this helper exists + // to model a real pipeline, and staleness inherited from an earlier pass is the class it was + // built to expose. Checking only the output text left that class unmeasured - four visitors + // shipped inflated reference counts past this helper, and the defect surfaced instead as a + // deleted declaration in a corpus sweep. + expectConsistentState(ast, fix ? cmpCode : undefined, { + allowReturnOutsideFunction: true, + errorRecovery: true, + }) + return ast +} + export function getPluginResult(plugin, fix, input) { const sourceCode = fs.readFileSync(input + '.js', { encoding: 'utf-8' }) const out = plugin(sourceCode) diff --git a/test/helper.test.js b/test/helper.test.js new file mode 100644 index 00000000..96202432 --- /dev/null +++ b/test/helper.test.js @@ -0,0 +1,35 @@ +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { detachedReferences } from './helper.js' + +/** + * The detector guards every visitor test, so it has to be shown capable of failing. A check that + * only ever reports zero is indistinguishable from one whose population is empty by construction, + * and that is the exact failure it exists to catch elsewhere. + */ + +test('detachedReferences is empty on a freshly parsed tree', () => { + const ast = parse('var a = 1;\nfunction f() {\n return a;\n}\nf();') + expect(detachedReferences(ast)).toEqual([]) +}) + +test('detachedReferences reports a reference detached by a node removal', () => { + const ast = parse('var a = 1;\nfunction f() {\n return a;\n}\nf();') + + // Cache the bindings first: this is the state a pass inherits from the one before it. + traverse(ast, { + Program(path) { + path.scope.crawl() + }, + }) + + // Detach the subtree holding the only reference to `a`, without telling Babel. + traverse(ast, { + FunctionDeclaration(path) { + path.node.body.body = [] + }, + }) + + expect(detachedReferences(ast)).toEqual(['a: Identifier']) +}) diff --git a/test/obfuscatorx/2.19.0-all-on-objects.fix.js b/test/obfuscatorx/2.19.0-all-on-objects.fix.js new file mode 100644 index 00000000..1903c169 --- /dev/null +++ b/test/obfuscatorx/2.19.0-all-on-objects.fix.js @@ -0,0 +1,20 @@ +var _0x49c7d0 = { + flag: true +}; +var _0x2be2b2 = { + name: "widget", + size: 3, + nested: _0x49c7d0 +}; +function _0xc8adcc(_0x519e8c) { + var _0x1d414f = _0x519e8c; + return function () { + _0x1d414f += 1; + return _0x1d414f; + }; +} +var _0x1d089d = _0xc8adcc(_0x2be2b2.size); +var _0x5779ed = "name"; +console.log("console-channel"); +process.stdout.write(_0x2be2b2[_0x5779ed] + " " + _0x2be2b2.nested.flag + " " + _0x2be2b2.size + "\n"); +process.stdout.write(_0x1d089d() + " " + _0x1d089d() + " " + Object.keys(_0x2be2b2).join(",") + "\n"); \ No newline at end of file diff --git a/test/obfuscatorx/2.19.0-all-on-objects.js b/test/obfuscatorx/2.19.0-all-on-objects.js new file mode 100644 index 00000000..30862858 --- /dev/null +++ b/test/obfuscatorx/2.19.0-all-on-objects.js @@ -0,0 +1 @@ +function _0xb602(_0x418cdb,_0x433fa8){var _0x256fdb=_0x1096();return _0xb602=function(_0x47bc8a,_0x403186){_0x47bc8a=_0x47bc8a-(-0x2157+0xa1d*0x2+0xe43);var _0x4eaba0=_0x256fdb[_0x47bc8a];if(_0xb602['\x55\x46\x64\x61\x4b\x53']===undefined){var _0x42c0a3=function(_0x24738c){var _0x1929ba='\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d';var _0x3ec19e='',_0x364ce6='';for(var _0x43c312=-0x1f93+-0x895+0x2828,_0x4e695b,_0x42b50,_0x1bc77a=0x2e9*-0x1+0x230b*0x1+-0xab6*0x3;_0x42b50=_0x24738c['\x63\x68\x61\x72\x41\x74'](_0x1bc77a++);~_0x42b50&&(_0x4e695b=_0x43c312%(-0x1591*-0x1+0x6*0x20+-0x164d*0x1)?_0x4e695b*(0x13*-0x11+0xaab+-0x928)+_0x42b50:_0x42b50,_0x43c312++%(0x1dd7*-0x1+-0x204f+0x3e2a))?_0x3ec19e+=String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](-0x7a+-0x742+-0x2e9*-0x3&_0x4e695b>>(-(-0x509*-0x7+-0x237e*-0x1+-0x3b9*0x13)*_0x43c312&0x1ca8+-0x189*0x14+0x212)):-0x13f7*-0x1+0x1b41+-0xbce*0x4){_0x42b50=_0x1929ba['\x69\x6e\x64\x65\x78\x4f\x66'](_0x42b50);}for(var _0x2a6a1c=-0x1471+-0x1d04+0xb*0x47f,_0x3a37c8=_0x3ec19e['\x6c\x65\x6e\x67\x74\x68'];_0x2a6a1c<_0x3a37c8;_0x2a6a1c++){_0x364ce6+='\x25'+('\x30\x30'+_0x3ec19e['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x2a6a1c)['\x74\x6f\x53\x74\x72\x69\x6e\x67'](0x1436+-0x2c2*-0x1+-0x16e8))['\x73\x6c\x69\x63\x65'](-(-0x1509+-0x34c+0x1857));}return decodeURIComponent(_0x364ce6);};_0xb602['\x77\x63\x69\x71\x47\x74']=_0x42c0a3,_0x418cdb=arguments,_0xb602['\x55\x46\x64\x61\x4b\x53']=!![];}var _0x106b82=_0x256fdb[0x3*0xb49+-0x107f*0x2+0x11*-0xd],_0x4837dd=_0x47bc8a+_0x106b82,_0x66126b=_0x418cdb[_0x4837dd];return!_0x66126b?(_0x4eaba0=_0xb602['\x77\x63\x69\x71\x47\x74'](_0x4eaba0),_0x418cdb[_0x4837dd]=_0x4eaba0):_0x4eaba0=_0x66126b,_0x4eaba0;},_0xb602(_0x418cdb,_0x433fa8);}(function(_0x490a46,_0x13c98d){function _0x123e78(_0x4bd219,_0x3ffbf5,_0x19948b,_0x38b09b,_0x28d1db){return _0x4265(_0x3ffbf5- -0x53,_0x38b09b);}function _0x41f479(_0x2c09b9,_0x550201,_0x1b3de9,_0x66e24a,_0x3317c6){return _0xb602(_0x2c09b9- -'\x30\x78\x33\x30\x62',_0x3317c6);}function _0x40eee4(_0x5813e2,_0x4aa943,_0x51baca,_0x539378,_0x2294ad){return _0x2c22(_0x539378- -'\x30\x78\x64\x63',_0x5813e2);}function _0x4b03b0(_0x365f91,_0xd515b6,_0x567030,_0x26fb04,_0x118cc6){return _0x4265(_0x365f91-'\x30\x78\x33\x63\x66',_0x118cc6);}var _0x524300=_0x490a46();function _0x35f6b5(_0x1676f5,_0x317a19,_0xc60cb0,_0x117806,_0xa2ff4b){return _0x2c22(_0x1676f5-'\x30\x78\x31\x63\x33',_0xc60cb0);}function _0x22f21b(_0x4ae414,_0x581e92,_0x1a1447,_0x22f872,_0x19b8df){return _0x4265(_0x22f872-0x23a,_0x4ae414);}function _0xb5bb06(_0x42e364,_0x5530e3,_0x3e7919,_0x302189,_0x408594){return _0x2c22(_0x3e7919- -'\x30\x78\x33\x31\x37',_0x5530e3);}function _0x1d357c(_0x441ef5,_0x1d0db9,_0x509e25,_0x4cd429,_0x37b359){return _0x4265(_0x509e25- -0x38a,_0x37b359);}function _0x93bba7(_0x3b6e36,_0xa5ea68,_0x597cbd,_0x2364a3,_0x1a9de4){return _0x2c22(_0x3b6e36- -0x296,_0x2364a3);}function _0x38a976(_0xcea198,_0x328984,_0x26b591,_0x14ce2f,_0x310343){return _0x4265(_0x26b591- -'\x30\x78\x32\x64\x36',_0xcea198);}function _0x3cb65d(_0x233083,_0x2ad371,_0x19a6bc,_0x12d711,_0x46910a){return _0xb602(_0x46910a- -0x3d1,_0x2ad371);}while(!![]){try{var _0x1c62fd=parseInt(_0x4b03b0(0x4fc,'\x30\x78\x34\x66\x36',0x4f6,0x500,'\x46\x61\x62\x5e'))/(-0x390*-0x2+-0x9e5*-0x3+-0x2a1*0xe)+parseInt(_0x4b03b0('\x30\x78\x34\x66\x61',0x4fa,'\x30\x78\x35\x30\x32',0x503,'\x6a\x46\x21\x4b'))/(-0x9*0x6c+0x1*0xcc7+-0x8f9*0x1)*(-parseInt(_0x38a976('\x6a\x46\x21\x4b',-0x19f,-0x1a5,-'\x30\x78\x31\x39\x64',-'\x30\x78\x31\x39\x66'))/(-0x2223+0x1*0xe89+0x139d))+-parseInt(_0x35f6b5('\x30\x78\x32\x66\x33',0x2f2,0x2f6,0x2fb,0x2eb))/(0x1*0x13dc+0xabd+0x1e95*-0x1)+-parseInt(_0x4b03b0(0x508,'\x30\x78\x35\x30\x32',0x50a,0x512,'\x52\x4e\x51\x6d'))/(-0x2034+-0x1609*0x1+-0xf*-0x39e)*(-parseInt(_0xb5bb06(-0x1da,-0x1db,-'\x30\x78\x31\x65\x30',-0x1da,-0x1dc))/(-0x13af*0x1+-0x1a3f+0x2df4))+-parseInt(_0x3cb65d(-'\x30\x78\x32\x39\x65',-0x292,-0x293,-0x290,-0x299))/(0x7be+-0xa*0x30+-0x5d7)*(parseInt(_0x3cb65d(-'\x30\x78\x32\x61\x35',-0x2a4,-'\x30\x78\x32\x39\x61',-'\x30\x78\x32\x61\x36',-'\x30\x78\x32\x39\x66'))/(0x219*-0x7+-0x220f+0x30c6))+parseInt(_0x40eee4(0x52,'\x30\x78\x35\x37','\x30\x78\x35\x37',0x59,'\x30\x78\x35\x64'))/(0x5e*-0xb+-0x31*0x16+0x849)+-parseInt(_0xb5bb06(-0x1dc,-'\x30\x78\x31\x64\x38',-'\x30\x78\x31\x65\x31',-'\x30\x78\x31\x64\x61',-0x1e0))/(-0x1a0e+-0xc46*0x1+0x265e)*(-parseInt(_0x22f21b('\x47\x63\x57\x65','\x30\x78\x33\x35\x62',0x368,'\x30\x78\x33\x36\x31',0x361))/(-0x26dc+-0x1a21+0x4108));if(_0x1c62fd===_0x13c98d)break;else _0x524300['push'](_0x524300['shift']());}catch(_0x40a744){_0x524300['push'](_0x524300['shift']());}}}(_0x1096,0x3*0x1ad99+-0x1*0x49de+-0x21db0));var _0x49c7d0={};_0x49c7d0['\x66\x6c'+'\x61\x67']=!![];var _0x45c94f={};function _0x2c22(_0x926360,_0x253667){var _0x25e330=_0x1096();return _0x2c22=function(_0x1ce159,_0x1096d3){_0x1ce159=_0x1ce159-(-0x2157+0xa1d*0x2+0xe43);var _0x2c22e1=_0x25e330[_0x1ce159];return _0x2c22e1;},_0x2c22(_0x926360,_0x253667);}_0x45c94f['\x6e\x61'+'\x6d\x65']='\x77\x69'+'\x64\x67'+'\x65\x74',_0x45c94f['\x73\x69'+'\x7a\x65']=0x3,_0x45c94f['\x6e\x65'+'\x73\x74'+'\x65\x64']=_0x49c7d0;var _0x2be2b2=_0x45c94f;function _0xc8adcc(_0x519e8c){var _0x1fd9c8={'\x6e\x46\x46\x77\x7a':function(_0x1271c4,_0x4d8a94){return _0x1271c4(_0x4d8a94);},'\x7a\x42\x4a\x79\x57':function(_0x507be2,_0x354d30){return _0x507be2+_0x354d30;},'\x57\x4f\x79\x71\x52':function(_0x390d27,_0x3c7396){return _0x390d27+_0x3c7396;},'\x62\x73\x7a\x56\x76':'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x28'+'\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x28\x29'+'\x20','\x70\x54\x68\x6e\x5a':'\x7b\x7d'+'\x2e\x63'+'\x6f\x6e'+'\x73\x74'+'\x72\x75'+'\x63\x74'+'\x6f\x72'+'\x28\x22'+'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x74'+'\x68\x69'+'\x73\x22'+'\x29\x28'+'\x20\x29','\x41\x42\x6c\x73\x6a':function(_0x4608d9){return _0x4608d9();},'\x6f\x75\x47\x65\x72':'\x77\x68'+'\x69\x6c'+'\x65\x20'+'\x28\x74'+'\x72\x75'+'\x65\x29'+'\x20\x7b'+'\x7d','\x62\x67\x57\x4e\x61':'\x63\x6f'+'\x75\x6e'+'\x74\x65'+'\x72','\x61\x55\x53\x55\x72':function(_0x169ff3,_0x3e2341){return _0x169ff3===_0x3e2341;},'\x63\x62\x53\x72\x65':'\x6f\x61'+'\x6e\x50'+'\x50','\x53\x69\x48\x61\x7a':'\x6f\x74'+'\x55\x6e'+'\x45','\x43\x67\x41\x44\x63':'\x74\x50'+'\x64\x75'+'\x66','\x67\x78\x68\x6b\x70':'\x53\x72'+'\x4e\x61'+'\x61','\x4d\x7a\x46\x42\x4a':'\x47\x68'+'\x4b\x6c'+'\x4f','\x61\x4c\x53\x44\x48':function(_0x49f34c,_0x2e78e1){return _0x49f34c!==_0x2e78e1;},'\x57\x70\x49\x47\x73':'\x61\x55'+'\x61\x56'+'\x66','\x55\x58\x72\x4f\x6f':'\x41\x4f'+'\x68\x78'+'\x49','\x73\x59\x53\x69\x5a':'\x4f\x65'+'\x63\x50'+'\x72','\x6b\x4c\x47\x61\x79':'\x73\x61'+'\x57\x4b'+'\x59','\x67\x53\x4b\x79\x4a':'\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x20\x2a'+'\x5c\x28'+'\x20\x2a'+'\x5c\x29','\x5a\x56\x51\x55\x6a':'\x5c\x2b'+'\x5c\x2b'+'\x20\x2a'+'\x28\x3f'+'\x3a\x5b'+'\x61\x2d'+'\x7a\x41'+'\x2d\x5a'+'\x5f\x24'+'\x5d\x5b'+'\x30\x2d'+'\x39\x61'+'\x2d\x7a'+'\x41\x2d'+'\x5a\x5f'+'\x24\x5d'+'\x2a\x29','\x43\x58\x7a\x64\x54':function(_0xde3bb7,_0xd5d85){return _0xde3bb7(_0xd5d85);},'\x4d\x55\x41\x65\x52':'\x69\x6e'+'\x69\x74','\x47\x6a\x42\x6f\x64':function(_0x3c613a,_0x37873e){return _0x3c613a+_0x37873e;},'\x4d\x53\x74\x42\x72':'\x63\x68'+'\x61\x69'+'\x6e','\x4f\x54\x55\x6b\x4a':'\x69\x6e'+'\x70\x75'+'\x74','\x6c\x67\x74\x6e\x61':function(_0x9a5229,_0x33803c){return _0x9a5229===_0x33803c;},'\x4e\x75\x58\x44\x56':'\x4d\x7a'+'\x72\x4e'+'\x43','\x49\x70\x69\x6c\x46':'\x51\x42'+'\x6d\x44'+'\x49','\x6c\x69\x6c\x52\x4d':function(_0x2031be,_0x56e421){return _0x2031be===_0x56e421;},'\x43\x76\x48\x70\x4b':'\x59\x62'+'\x73\x4c'+'\x70','\x6a\x78\x65\x5a\x68':'\x44\x66'+'\x6a\x64'+'\x78','\x70\x55\x78\x4f\x70':'\x33\x7c'+'\x34\x7c'+'\x31\x7c'+'\x30\x7c'+'\x32\x7c'+'\x35','\x65\x68\x62\x79\x44':'\x6a\x4f'+'\x73\x70'+'\x72','\x7a\x70\x46\x5a\x48':'\x44\x69'+'\x6c\x65'+'\x54','\x73\x49\x69\x75\x53':function(_0x1b9967,_0x12ef5b,_0x213a31){return _0x1b9967(_0x12ef5b,_0x213a31);},'\x7a\x4e\x42\x6f\x50':function(_0x2c8cea,_0x181c9a){return _0x2c8cea+_0x181c9a;},'\x66\x58\x49\x46\x71':function(_0x5c44f3,_0x4b72c6){return _0x5c44f3===_0x4b72c6;},'\x77\x64\x41\x42\x4b':'\x53\x70'+'\x48\x76'+'\x78','\x64\x6c\x54\x65\x53':function(_0x4ae9ed,_0x1e2ab0){return _0x4ae9ed===_0x1e2ab0;},'\x4f\x46\x61\x65\x50':'\x41\x50'+'\x6b\x7a'+'\x66','\x67\x56\x65\x74\x5a':'\x75\x79'+'\x63\x73'+'\x4b','\x4a\x54\x55\x47\x72':function(_0x17be37,_0x3c171b){return _0x17be37+_0x3c171b;},'\x79\x55\x48\x76\x66':'\x64\x65'+'\x62\x75','\x49\x63\x48\x61\x72':'\x67\x67'+'\x65\x72','\x75\x42\x72\x7a\x4d':'\x73\x74'+'\x61\x74'+'\x65\x4f'+'\x62\x6a'+'\x65\x63'+'\x74','\x55\x6f\x56\x4a\x71':function(_0xafda7,_0x2b06d2){return _0xafda7===_0x2b06d2;},'\x6c\x64\x4b\x46\x62':'\x54\x4b'+'\x69\x5a'+'\x43','\x48\x74\x59\x4a\x69':function(_0x134cca,_0x5133b4){return _0x134cca(_0x5133b4);},'\x4f\x57\x71\x4a\x73':'\x4c\x71'+'\x56\x41'+'\x77','\x46\x78\x55\x51\x62':'\x44\x78'+'\x79\x44'+'\x67','\x57\x45\x4c\x67\x50':'\x51\x77'+'\x4c\x6b'+'\x66','\x4c\x64\x67\x59\x61':function(_0x23c380,_0x5a7ac2){return _0x23c380(_0x5a7ac2);},'\x74\x6a\x58\x4f\x57':function(_0x4d97e7,_0x2c8673){return _0x4d97e7+_0x2c8673;},'\x62\x53\x78\x70\x63':'\x47\x56'+'\x45\x72'+'\x4c','\x55\x58\x43\x41\x50':'\x6c\x6f'+'\x67','\x45\x59\x77\x50\x63':'\x77\x61'+'\x72\x6e','\x46\x77\x48\x75\x46':'\x69\x6e'+'\x66\x6f','\x43\x6a\x72\x75\x41':'\x65\x72'+'\x72\x6f'+'\x72','\x65\x6d\x73\x45\x6f':'\x65\x78'+'\x63\x65'+'\x70\x74'+'\x69\x6f'+'\x6e','\x6b\x71\x73\x4f\x4c':'\x74\x61'+'\x62\x6c'+'\x65','\x66\x42\x6f\x76\x49':'\x74\x72'+'\x61\x63'+'\x65','\x4d\x44\x55\x7a\x55':function(_0x762eff,_0x438dcc){return _0x762eff<_0x438dcc;},'\x55\x58\x50\x4e\x6e':function(_0x515e19,_0x4b69cd){return _0x515e19===_0x4b69cd;},'\x4a\x4b\x58\x66\x48':'\x4e\x71'+'\x72\x4c'+'\x4a','\x4c\x63\x4b\x61\x47':'\x33\x7c'+'\x34\x7c'+'\x35\x7c'+'\x30\x7c'+'\x32\x7c'+'\x31','\x7a\x46\x4d\x6d\x67':function(_0x496a90,_0x495700){return _0x496a90(_0x495700);},'\x5a\x51\x75\x6f\x4a':function(_0x52e8d2){return _0x52e8d2();},'\x70\x76\x49\x41\x75':'\x4b\x61'+'\x61\x69'+'\x71','\x73\x61\x49\x78\x49':function(_0x1c9ac6,_0x1c95be,_0xfb5ac5){return _0x1c9ac6(_0x1c95be,_0xfb5ac5);},'\x74\x76\x51\x6a\x43':function(_0x5ca09e){return _0x5ca09e();}},_0xf9deb3=function(){var _0x45714f={'\x73\x68\x54\x70\x73':function(_0x193143,_0x364736){return _0x1fd9c8['\x6e\x46'+'\x46\x77'+'\x7a'](_0x193143,_0x364736);},'\x79\x5a\x78\x77\x45':function(_0x84cef1,_0x2c3909){return _0x1fd9c8['\x7a\x42'+'\x4a\x79'+'\x57'](_0x84cef1,_0x2c3909);},'\x49\x79\x56\x41\x79':function(_0x14f2f4,_0x4bd07f){return _0x1fd9c8['\x57\x4f'+'\x79\x71'+'\x52'](_0x14f2f4,_0x4bd07f);},'\x6b\x68\x54\x66\x74':_0x1fd9c8['\x62\x73'+'\x7a\x56'+'\x76'],'\x74\x76\x51\x4b\x48':_0x1fd9c8['\x70\x54'+'\x68\x6e'+'\x5a'],'\x75\x74\x66\x6c\x4c':function(_0x35cba8){return _0x1fd9c8['\x41\x42'+'\x6c\x73'+'\x6a'](_0x35cba8);},'\x51\x5a\x56\x58\x69':function(_0x322247,_0x426311){return _0x1fd9c8['\x6e\x46'+'\x46\x77'+'\x7a'](_0x322247,_0x426311);},'\x76\x46\x4e\x4d\x69':_0x1fd9c8['\x6f\x75'+'\x47\x65'+'\x72'],'\x5a\x6e\x50\x41\x46':_0x1fd9c8['\x62\x67'+'\x57\x4e'+'\x61'],'\x44\x58\x4c\x75\x6a':function(_0x7e511e,_0x2a2050){return _0x1fd9c8['\x61\x55'+'\x53\x55'+'\x72'](_0x7e511e,_0x2a2050);},'\x6a\x44\x44\x58\x52':_0x1fd9c8['\x63\x62'+'\x53\x72'+'\x65'],'\x68\x42\x4e\x4d\x62':_0x1fd9c8['\x53\x69'+'\x48\x61'+'\x7a'],'\x4d\x76\x61\x7a\x55':_0x1fd9c8['\x43\x67'+'\x41\x44'+'\x63'],'\x6d\x74\x49\x65\x53':_0x1fd9c8['\x67\x78'+'\x68\x6b'+'\x70'],'\x6f\x4d\x79\x4d\x71':_0x1fd9c8['\x4d\x7a'+'\x46\x42'+'\x4a']};if(_0x1fd9c8['\x61\x4c'+'\x53\x44'+'\x48'](_0x1fd9c8['\x57\x70'+'\x49\x47'+'\x73'],_0x1fd9c8['\x55\x58'+'\x72\x4f'+'\x6f'])){var _0x1331ea=!![];return function(_0x185fcd,_0xa66d7){var _0x2f0129={'\x5a\x4b\x77\x69\x6a':function(_0x1401d8,_0x1be323){return _0x45714f['\x51\x5a'+'\x56\x58'+'\x69'](_0x1401d8,_0x1be323);},'\x4b\x4a\x47\x6f\x52':_0x45714f['\x76\x46'+'\x4e\x4d'+'\x69'],'\x53\x4d\x69\x4e\x68':_0x45714f['\x5a\x6e'+'\x50\x41'+'\x46'],'\x79\x48\x76\x4a\x76':function(_0x1d9511,_0x25021a){return _0x45714f['\x44\x58'+'\x4c\x75'+'\x6a'](_0x1d9511,_0x25021a);},'\x64\x6a\x4e\x6e\x62':_0x45714f['\x6a\x44'+'\x44\x58'+'\x52'],'\x68\x61\x6a\x44\x49':_0x45714f['\x68\x42'+'\x4e\x4d'+'\x62'],'\x5a\x62\x78\x46\x4e':_0x45714f['\x4d\x76'+'\x61\x7a'+'\x55']};if(_0x45714f['\x44\x58'+'\x4c\x75'+'\x6a'](_0x45714f['\x6d\x74'+'\x49\x65'+'\x53'],_0x45714f['\x6f\x4d'+'\x79\x4d'+'\x71'])){var _0x1dba32=ausdmg['\x73\x68'+'\x54\x70'+'\x73'](_0x29c11c,ausdmg['\x79\x5a'+'\x78\x77'+'\x45'](ausdmg['\x49\x79'+'\x56\x41'+'\x79'](ausdmg['\x6b\x68'+'\x54\x66'+'\x74'],ausdmg['\x74\x76'+'\x51\x4b'+'\x48']),'\x29\x3b'));_0x24e75e=ausdmg['\x75\x74'+'\x66\x6c'+'\x4c'](_0x1dba32);}else{var _0x14dc1c=_0x1331ea?function(){var _0x51f4c0={};_0x51f4c0['\x46\x79'+'\x42\x61'+'\x59']=_0x2f0129['\x4b\x4a'+'\x47\x6f'+'\x52'],_0x51f4c0['\x73\x67'+'\x68\x6f'+'\x46']=_0x2f0129['\x53\x4d'+'\x69\x4e'+'\x68'];var _0x2506fa=_0x51f4c0;if(_0x2f0129['\x79\x48'+'\x76\x4a'+'\x76'](_0x2f0129['\x64\x6a'+'\x4e\x6e'+'\x62'],_0x2f0129['\x68\x61'+'\x6a\x44'+'\x49']))return function(_0x58f664){}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](iebRPL['\x46\x79'+'\x42\x61'+'\x59'])['\x61\x70'+'\x70\x6c'+'\x79'](iebRPL['\x73\x67'+'\x68\x6f'+'\x46']);else{if(_0xa66d7){if(_0x2f0129['\x79\x48'+'\x76\x4a'+'\x76'](_0x2f0129['\x5a\x62'+'\x78\x46'+'\x4e'],_0x2f0129['\x5a\x62'+'\x78\x46'+'\x4e'])){var _0x37e280=_0xa66d7['\x61\x70'+'\x70\x6c'+'\x79'](_0x185fcd,arguments);return _0xa66d7=null,_0x37e280;}else ryjkpw['\x5a\x4b'+'\x77\x69'+'\x6a'](_0x47aea1,-0x132e+-0x23c0+0x36ee);}}}:function(){};return _0x1331ea=![],_0x14dc1c;}};}else{var _0x2d90ff=_0x4983de?function(){if(_0x379309){var _0x4246f0=_0x10e8b6['\x61\x70'+'\x70\x6c'+'\x79'](_0x5cb853,arguments);return _0x51dbab=null,_0x4246f0;}}:function(){};return _0x5ba3b4=![],_0x2d90ff;}}();(function(){var _0x39d146={};_0x39d146['\x4c\x45'+'\x63\x4f'+'\x6a']=_0x1fd9c8['\x70\x55'+'\x78\x4f'+'\x70'];var _0x43c0d6=_0x39d146;if(_0x1fd9c8['\x6c\x69'+'\x6c\x52'+'\x4d'](_0x1fd9c8['\x65\x68'+'\x62\x79'+'\x44'],_0x1fd9c8['\x7a\x70'+'\x46\x5a'+'\x48'])){var _0x3da697=_0x29a46a['\x61\x70'+'\x70\x6c'+'\x79'](_0xda768d,arguments);return _0x5be4c6=null,_0x3da697;}else _0x1fd9c8['\x73\x49'+'\x69\x75'+'\x53'](_0xf9deb3,this,function(){if(_0x1fd9c8['\x61\x55'+'\x53\x55'+'\x72'](_0x1fd9c8['\x73\x59'+'\x53\x69'+'\x5a'],_0x1fd9c8['\x6b\x4c'+'\x47\x61'+'\x79']))return _0x31b9cc+=-0xc0b*0x1+-0xf3e+0x1b4a,_0x48be42;else{var _0x138a05=new RegExp(_0x1fd9c8['\x67\x53'+'\x4b\x79'+'\x4a']),_0x5801f7=new RegExp(_0x1fd9c8['\x5a\x56'+'\x51\x55'+'\x6a'],'\x69'),_0x1ee410=_0x1fd9c8['\x43\x58'+'\x7a\x64'+'\x54'](_0x3a7191,_0x1fd9c8['\x4d\x55'+'\x41\x65'+'\x52']);if(!_0x138a05['\x74\x65'+'\x73\x74'](_0x1fd9c8['\x47\x6a'+'\x42\x6f'+'\x64'](_0x1ee410,_0x1fd9c8['\x4d\x53'+'\x74\x42'+'\x72']))||!_0x5801f7['\x74\x65'+'\x73\x74'](_0x1fd9c8['\x47\x6a'+'\x42\x6f'+'\x64'](_0x1ee410,_0x1fd9c8['\x4f\x54'+'\x55\x6b'+'\x4a']))){if(_0x1fd9c8['\x6c\x67'+'\x74\x6e'+'\x61'](_0x1fd9c8['\x4e\x75'+'\x58\x44'+'\x56'],_0x1fd9c8['\x49\x70'+'\x69\x6c'+'\x46'])){var _0x515d9a=_0x1ea160?function(){if(_0xa1b3b2){var _0x6d6b80=_0xfd8d09['\x61\x70'+'\x70\x6c'+'\x79'](_0x461332,arguments);return _0x1fe8d8=null,_0x6d6b80;}}:function(){};return _0x40ce9a=![],_0x515d9a;}else _0x1fd9c8['\x43\x58'+'\x7a\x64'+'\x54'](_0x1ee410,'\x30');}else{if(_0x1fd9c8['\x6c\x69'+'\x6c\x52'+'\x4d'](_0x1fd9c8['\x43\x76'+'\x48\x70'+'\x4b'],_0x1fd9c8['\x6a\x78'+'\x65\x5a'+'\x68'])){var _0x5cc6e=_0x43c0d6['\x4c\x45'+'\x63\x4f'+'\x6a']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0x3a764d=-0x134c+0x13a6+-0x5a;while(!![]){switch(_0x5cc6e[_0x3a764d++]){case'\x30':_0x893979['\x5f\x5f'+'\x70\x72'+'\x6f\x74'+'\x6f\x5f'+'\x5f']=_0x560d98['\x62\x69'+'\x6e\x64'](_0x565d7f);continue;case'\x31':var _0x435615=_0x8e6bab[_0x56d6d2]||_0x893979;continue;case'\x32':_0x893979['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']=_0x435615['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']['\x62\x69'+'\x6e\x64'](_0x435615);continue;case'\x33':var _0x893979=_0x230a88['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72']['\x70\x72'+'\x6f\x74'+'\x6f\x74'+'\x79\x70'+'\x65']['\x62\x69'+'\x6e\x64'](_0x3089be);continue;case'\x34':var _0x56d6d2=_0x224ac8[_0x110cdc];continue;case'\x35':_0x389a7e[_0x56d6d2]=_0x893979;continue;}break;}}else _0x1fd9c8['\x41\x42'+'\x6c\x73'+'\x6a'](_0x3a7191);}}})();}());var _0x2cb339=function(){var _0x3de2d2={'\x47\x4d\x4c\x77\x4c':function(_0x4dc9d8,_0x1d18c4){return _0x1fd9c8['\x4a\x54'+'\x55\x47'+'\x72'](_0x4dc9d8,_0x1d18c4);},'\x43\x4e\x4f\x74\x63':_0x1fd9c8['\x79\x55'+'\x48\x76'+'\x66'],'\x54\x4a\x4c\x48\x65':_0x1fd9c8['\x49\x63'+'\x48\x61'+'\x72'],'\x69\x64\x57\x41\x45':_0x1fd9c8['\x75\x42'+'\x72\x7a'+'\x4d']};if(_0x1fd9c8['\x55\x6f'+'\x56\x4a'+'\x71'](_0x1fd9c8['\x6c\x64'+'\x4b\x46'+'\x62'],_0x1fd9c8['\x6c\x64'+'\x4b\x46'+'\x62'])){var _0x501619=!![];return function(_0x2390dc,_0x2cf227){var _0x391f9c={'\x76\x63\x51\x73\x6c':_0x1fd9c8['\x67\x53'+'\x4b\x79'+'\x4a'],'\x4c\x43\x44\x6e\x62':_0x1fd9c8['\x5a\x56'+'\x51\x55'+'\x6a'],'\x43\x62\x48\x6c\x74':function(_0x391c3d,_0x9cf06d){return _0x1fd9c8['\x43\x58'+'\x7a\x64'+'\x54'](_0x391c3d,_0x9cf06d);},'\x67\x74\x61\x6d\x6c':_0x1fd9c8['\x4d\x55'+'\x41\x65'+'\x52'],'\x50\x56\x54\x4a\x6d':function(_0x170e4f,_0x1f3403){return _0x1fd9c8['\x47\x6a'+'\x42\x6f'+'\x64'](_0x170e4f,_0x1f3403);},'\x75\x4e\x6b\x42\x51':_0x1fd9c8['\x4d\x53'+'\x74\x42'+'\x72'],'\x75\x49\x67\x67\x53':function(_0x52c150,_0x41582b){return _0x1fd9c8['\x7a\x4e'+'\x42\x6f'+'\x50'](_0x52c150,_0x41582b);},'\x73\x4d\x61\x58\x66':_0x1fd9c8['\x4f\x54'+'\x55\x6b'+'\x4a'],'\x46\x55\x49\x44\x4d':function(_0x40f043){return _0x1fd9c8['\x41\x42'+'\x6c\x73'+'\x6a'](_0x40f043);},'\x78\x4d\x6e\x50\x74':function(_0x1c3539,_0x462b52){return _0x1fd9c8['\x66\x58'+'\x49\x46'+'\x71'](_0x1c3539,_0x462b52);},'\x44\x75\x46\x4f\x52':_0x1fd9c8['\x77\x64'+'\x41\x42'+'\x4b'],'\x6d\x61\x5a\x48\x50':function(_0x3418b6,_0x499350){return _0x1fd9c8['\x64\x6c'+'\x54\x65'+'\x53'](_0x3418b6,_0x499350);},'\x4f\x59\x47\x58\x63':_0x1fd9c8['\x4f\x46'+'\x61\x65'+'\x50']};if(_0x1fd9c8['\x64\x6c'+'\x54\x65'+'\x53'](_0x1fd9c8['\x67\x56'+'\x65\x74'+'\x5a'],_0x1fd9c8['\x67\x56'+'\x65\x74'+'\x5a'])){var _0x4b469b=_0x501619?function(){if(_0x391f9c['\x78\x4d'+'\x6e\x50'+'\x74'](_0x391f9c['\x44\x75'+'\x46\x4f'+'\x52'],_0x391f9c['\x44\x75'+'\x46\x4f'+'\x52'])){if(_0x2cf227){if(_0x391f9c['\x6d\x61'+'\x5a\x48'+'\x50'](_0x391f9c['\x4f\x59'+'\x47\x58'+'\x63'],_0x391f9c['\x4f\x59'+'\x47\x58'+'\x63'])){var _0x1f34ed=_0x2cf227['\x61\x70'+'\x70\x6c'+'\x79'](_0x2390dc,arguments);return _0x2cf227=null,_0x1f34ed;}else{var _0x1533db=new _0x47bc8a(kgtOAr['\x76\x63'+'\x51\x73'+'\x6c']),_0xf6a153=new _0x403186(kgtOAr['\x4c\x43'+'\x44\x6e'+'\x62'],'\x69'),_0x588c8b=kgtOAr['\x43\x62'+'\x48\x6c'+'\x74'](_0x4eaba0,kgtOAr['\x67\x74'+'\x61\x6d'+'\x6c']);!_0x1533db['\x74\x65'+'\x73\x74'](kgtOAr['\x50\x56'+'\x54\x4a'+'\x6d'](_0x588c8b,kgtOAr['\x75\x4e'+'\x6b\x42'+'\x51']))||!_0xf6a153['\x74\x65'+'\x73\x74'](kgtOAr['\x75\x49'+'\x67\x67'+'\x53'](_0x588c8b,kgtOAr['\x73\x4d'+'\x61\x58'+'\x66']))?kgtOAr['\x43\x62'+'\x48\x6c'+'\x74'](_0x588c8b,'\x30'):kgtOAr['\x46\x55'+'\x49\x44'+'\x4d'](_0x106b82);}}}else return _0x4c5ec6;}:function(){};return _0x501619=![],_0x4b469b;}else _0x3528d0=_0x2663ed;};}else(function(){return![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](PDhiFM['\x47\x4d'+'\x4c\x77'+'\x4c'](PDhiFM['\x43\x4e'+'\x4f\x74'+'\x63'],PDhiFM['\x54\x4a'+'\x4c\x48'+'\x65']))['\x61\x70'+'\x70\x6c'+'\x79'](PDhiFM['\x69\x64'+'\x57\x41'+'\x45']));}(),_0x5c2126=_0x1fd9c8['\x73\x61'+'\x49\x78'+'\x49'](_0x2cb339,this,function(){var _0x36f7f1={'\x79\x4b\x58\x4f\x65':function(_0x3f804a){return _0x1fd9c8['\x41\x42'+'\x6c\x73'+'\x6a'](_0x3f804a);}};if(_0x1fd9c8['\x55\x6f'+'\x56\x4a'+'\x71'](_0x1fd9c8['\x4f\x57'+'\x71\x4a'+'\x73'],_0x1fd9c8['\x4f\x57'+'\x71\x4a'+'\x73'])){var _0x4061fe;try{if(_0x1fd9c8['\x55\x6f'+'\x56\x4a'+'\x71'](_0x1fd9c8['\x46\x78'+'\x55\x51'+'\x62'],_0x1fd9c8['\x57\x45'+'\x4c\x67'+'\x50'])){if(_0x123641){var _0x20d47a=_0x404226['\x61\x70'+'\x70\x6c'+'\x79'](_0x2e2dde,arguments);return _0x2bfed6=null,_0x20d47a;}}else{var _0x54d475=_0x1fd9c8['\x4c\x64'+'\x67\x59'+'\x61'](Function,_0x1fd9c8['\x4a\x54'+'\x55\x47'+'\x72'](_0x1fd9c8['\x74\x6a'+'\x58\x4f'+'\x57'](_0x1fd9c8['\x62\x73'+'\x7a\x56'+'\x76'],_0x1fd9c8['\x70\x54'+'\x68\x6e'+'\x5a']),'\x29\x3b'));_0x4061fe=_0x1fd9c8['\x41\x42'+'\x6c\x73'+'\x6a'](_0x54d475);}}catch(_0x2b115b){if(_0x1fd9c8['\x55\x6f'+'\x56\x4a'+'\x71'](_0x1fd9c8['\x62\x53'+'\x78\x70'+'\x63'],_0x1fd9c8['\x62\x53'+'\x78\x70'+'\x63']))_0x4061fe=window;else{if(_0x336e57)return _0x14781f;else uqkjRS['\x43\x58'+'\x7a\x64'+'\x54'](_0xe0751,0x1a*0x5e+-0x342+-0x23*0x2e);}}var _0x57aa44=_0x4061fe['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']=_0x4061fe['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']||{},_0x5c7b87=[_0x1fd9c8['\x55\x58'+'\x43\x41'+'\x50'],_0x1fd9c8['\x45\x59'+'\x77\x50'+'\x63'],_0x1fd9c8['\x46\x77'+'\x48\x75'+'\x46'],_0x1fd9c8['\x43\x6a'+'\x72\x75'+'\x41'],_0x1fd9c8['\x65\x6d'+'\x73\x45'+'\x6f'],_0x1fd9c8['\x6b\x71'+'\x73\x4f'+'\x4c'],_0x1fd9c8['\x66\x42'+'\x6f\x76'+'\x49']];for(var _0x292cf0=-0xf9e+-0x75*0x2+0x1088;_0x1fd9c8['\x4d\x44'+'\x55\x7a'+'\x55'](_0x292cf0,_0x5c7b87['\x6c\x65'+'\x6e\x67'+'\x74\x68']);_0x292cf0++){if(_0x1fd9c8['\x55\x58'+'\x50\x4e'+'\x6e'](_0x1fd9c8['\x4a\x4b'+'\x58\x66'+'\x48'],_0x1fd9c8['\x4a\x4b'+'\x58\x66'+'\x48'])){var _0x2100fa=_0x1fd9c8['\x4c\x63'+'\x4b\x61'+'\x47']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0x85007a=0x10*0x69+0x9*-0x196+0x7b6;while(!![]){switch(_0x2100fa[_0x85007a++]){case'\x30':_0x545695['\x5f\x5f'+'\x70\x72'+'\x6f\x74'+'\x6f\x5f'+'\x5f']=_0x2cb339['\x62\x69'+'\x6e\x64'](_0x2cb339);continue;case'\x31':_0x57aa44[_0x4c9fc1]=_0x545695;continue;case'\x32':_0x545695['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']=_0x4532e7['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']['\x62\x69'+'\x6e\x64'](_0x4532e7);continue;case'\x33':var _0x545695=_0x2cb339['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72']['\x70\x72'+'\x6f\x74'+'\x6f\x74'+'\x79\x70'+'\x65']['\x62\x69'+'\x6e\x64'](_0x2cb339);continue;case'\x34':var _0x4c9fc1=_0x5c7b87[_0x292cf0];continue;case'\x35':var _0x4532e7=_0x57aa44[_0x4c9fc1]||_0x545695;continue;}break;}}else uqkjRS['\x48\x74'+'\x59\x4a'+'\x69'](_0x3ec19e,'\x30');}}else dsrKcR['\x79\x4b'+'\x58\x4f'+'\x65'](_0x1405b3);});_0x1fd9c8['\x74\x76'+'\x51\x6a'+'\x43'](_0x5c2126);var _0x1d414f=_0x519e8c;return function(){var _0x4ae051={'\x6d\x6d\x45\x59\x69':_0x1fd9c8['\x67\x53'+'\x4b\x79'+'\x4a'],'\x79\x6b\x64\x42\x59':_0x1fd9c8['\x5a\x56'+'\x51\x55'+'\x6a'],'\x54\x53\x54\x55\x44':function(_0x3b11bb,_0x439ce0){return _0x1fd9c8['\x7a\x46'+'\x4d\x6d'+'\x67'](_0x3b11bb,_0x439ce0);},'\x66\x67\x4b\x79\x64':_0x1fd9c8['\x4d\x55'+'\x41\x65'+'\x52'],'\x53\x4e\x75\x46\x78':function(_0x44d1ce,_0x3dc805){return _0x1fd9c8['\x74\x6a'+'\x58\x4f'+'\x57'](_0x44d1ce,_0x3dc805);},'\x76\x54\x46\x4a\x6c':_0x1fd9c8['\x4d\x53'+'\x74\x42'+'\x72'],'\x6a\x56\x77\x61\x7a':_0x1fd9c8['\x4f\x54'+'\x55\x6b'+'\x4a'],'\x52\x42\x73\x4f\x44':function(_0x475fd5){return _0x1fd9c8['\x5a\x51'+'\x75\x6f'+'\x4a'](_0x475fd5);},'\x4e\x50\x4b\x5a\x58':function(_0x298468,_0x26928b,_0x3f2814){return _0x1fd9c8['\x73\x49'+'\x69\x75'+'\x53'](_0x298468,_0x26928b,_0x3f2814);}};if(_0x1fd9c8['\x61\x4c'+'\x53\x44'+'\x48'](_0x1fd9c8['\x70\x76'+'\x49\x41'+'\x75'],_0x1fd9c8['\x70\x76'+'\x49\x41'+'\x75']))hmUsov['\x4e\x50'+'\x4b\x5a'+'\x58'](_0x587f8c,this,function(){var _0x11a2d9=new _0x16f5d5(hmUsov['\x6d\x6d'+'\x45\x59'+'\x69']),_0x1320d8=new _0x59028d(hmUsov['\x79\x6b'+'\x64\x42'+'\x59'],'\x69'),_0x525b73=hmUsov['\x54\x53'+'\x54\x55'+'\x44'](_0x76b251,hmUsov['\x66\x67'+'\x4b\x79'+'\x64']);!_0x11a2d9['\x74\x65'+'\x73\x74'](hmUsov['\x53\x4e'+'\x75\x46'+'\x78'](_0x525b73,hmUsov['\x76\x54'+'\x46\x4a'+'\x6c']))||!_0x1320d8['\x74\x65'+'\x73\x74'](hmUsov['\x53\x4e'+'\x75\x46'+'\x78'](_0x525b73,hmUsov['\x6a\x56'+'\x77\x61'+'\x7a']))?hmUsov['\x54\x53'+'\x54\x55'+'\x44'](_0x525b73,'\x30'):hmUsov['\x52\x42'+'\x73\x4f'+'\x44'](_0x3681f4);})();else return _0x1d414f+=0x6df*-0x1+-0x5f8+-0x6*-0x224,_0x1d414f;};}setInterval(function(){var _0x195c05={'\x75\x6f\x4a\x49\x59':function(_0x54422a){return _0x54422a();}};_0x195c05['\x75\x6f'+'\x4a\x49'+'\x59'](_0x3a7191);},0x1367*-0x1+0x2308+-0x1*0x1);var _0x1d089d=_0xc8adcc(_0x2be2b2['\x73\x69'+'\x7a\x65']),_0x5779ed='\x6e\x61'+'\x6d\x65';function _0x4265(_0x2bc87d,_0x5542e7){var _0x10580e=_0x1096();return _0x4265=function(_0x29c11c,_0x24e75e){_0x29c11c=_0x29c11c-(-0x2157+0xa1d*0x2+0xe43);var _0x19af46=_0x10580e[_0x29c11c];if(_0x4265['\x46\x6e\x68\x6e\x5a\x77']===undefined){var _0x1c825f=function(_0x3e910d){var _0xa270d9='\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d';var _0x4b52e4='',_0x31d380='';for(var _0x342bca=-0x1f93+-0x895+0x2828,_0x25d34b,_0x2fc7f9,_0x230a88=0x2e9*-0x1+0x230b*0x1+-0xab6*0x3;_0x2fc7f9=_0x3e910d['\x63\x68\x61\x72\x41\x74'](_0x230a88++);~_0x2fc7f9&&(_0x25d34b=_0x342bca%(-0x1591*-0x1+0x6*0x20+-0x164d*0x1)?_0x25d34b*(0x13*-0x11+0xaab+-0x928)+_0x2fc7f9:_0x2fc7f9,_0x342bca++%(0x1dd7*-0x1+-0x204f+0x3e2a))?_0x4b52e4+=String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](-0x7a+-0x742+-0x2e9*-0x3&_0x25d34b>>(-(-0x509*-0x7+-0x237e*-0x1+-0x3b9*0x13)*_0x342bca&0x1ca8+-0x189*0x14+0x212)):-0x13f7*-0x1+0x1b41+-0xbce*0x4){_0x2fc7f9=_0xa270d9['\x69\x6e\x64\x65\x78\x4f\x66'](_0x2fc7f9);}for(var _0x3089be=-0x1471+-0x1d04+0xb*0x47f,_0x224ac8=_0x4b52e4['\x6c\x65\x6e\x67\x74\x68'];_0x3089be<_0x224ac8;_0x3089be++){_0x31d380+='\x25'+('\x30\x30'+_0x4b52e4['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x3089be)['\x74\x6f\x53\x74\x72\x69\x6e\x67'](0x1436+-0x2c2*-0x1+-0x16e8))['\x73\x6c\x69\x63\x65'](-(-0x1509+-0x34c+0x1857));}return decodeURIComponent(_0x31d380);};var _0x2093a6=function(_0x110cdc,_0x8e6bab){var _0x560d98=[],_0x565d7f=0x3*0xb49+-0x107f*0x2+0x11*-0xd,_0x389a7e,_0x4c9283='';_0x110cdc=_0x1c825f(_0x110cdc);var _0x397ac3;for(_0x397ac3=0xea2+0xb*0x175+0x2f*-0xa7;_0x397ac3<-0x199e+0x1419+0x685;_0x397ac3++){_0x560d98[_0x397ac3]=_0x397ac3;}for(_0x397ac3=0x13c*0xa+0x56b+-0x11c3;_0x397ac3<-0x452+-0x1*-0x8a1+0x1*-0x34f;_0x397ac3++){_0x565d7f=(_0x565d7f+_0x560d98[_0x397ac3]+_0x8e6bab['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x397ac3%_0x8e6bab['\x6c\x65\x6e\x67\x74\x68']))%(-0x95*-0x29+-0x15*-0x1b7+-0x3ae0),_0x389a7e=_0x560d98[_0x397ac3],_0x560d98[_0x397ac3]=_0x560d98[_0x565d7f],_0x560d98[_0x565d7f]=_0x389a7e;}_0x397ac3=-0x1d*0x12f+0x1bff+0x654,_0x565d7f=0x1338+0x1ebc+-0x31f4;for(var _0x5efdf0=-0x11e9+0xb*0x232+-0x63d;_0x5efdf0<_0x110cdc['\x6c\x65\x6e\x67\x74\x68'];_0x5efdf0++){_0x397ac3=(_0x397ac3+(-0x1*-0x246b+0x751*-0x3+-0x211*0x7))%(0x197f+0xf0b+-0x278a),_0x565d7f=(_0x565d7f+_0x560d98[_0x397ac3])%(0x566*0x1+0x1165*0x2+0x13*-0x210),_0x389a7e=_0x560d98[_0x397ac3],_0x560d98[_0x397ac3]=_0x560d98[_0x565d7f],_0x560d98[_0x565d7f]=_0x389a7e,_0x4c9283+=String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](_0x110cdc['\x63\x68\x61\x72\x43\x6f\x64\x65\x41\x74'](_0x5efdf0)^_0x560d98[(_0x560d98[_0x397ac3]+_0x560d98[_0x565d7f])%(0x422+0x109e+-0x8*0x278)]);}return _0x4c9283;};_0x4265['\x43\x79\x5a\x78\x54\x4d']=_0x2093a6,_0x2bc87d=arguments,_0x4265['\x46\x6e\x68\x6e\x5a\x77']=!![];}var _0x164abf=_0x10580e[-0x149+-0x1d17+0x4*0x798],_0x3528d0=_0x29c11c+_0x164abf,_0x2663ed=_0x2bc87d[_0x3528d0];return!_0x2663ed?(_0x4265['\x6e\x78\x5a\x7a\x42\x66']===undefined&&(_0x4265['\x6e\x78\x5a\x7a\x42\x66']=!![]),_0x19af46=_0x4265['\x43\x79\x5a\x78\x54\x4d'](_0x19af46,_0x24e75e),_0x2bc87d[_0x3528d0]=_0x19af46):_0x19af46=_0x2663ed,_0x19af46;},_0x4265(_0x2bc87d,_0x5542e7);}console['\x6c\x6f'+'\x67']('\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65\x2d'+'\x63\x68'+'\x61\x6e'+'\x6e\x65'+'\x6c'),process['\x73\x74'+'\x64\x6f'+'\x75\x74']['\x77\x72'+'\x69\x74'+'\x65'](_0x2be2b2[_0x5779ed]+'\x20'+_0x2be2b2['\x6e\x65'+'\x73\x74'+'\x65\x64']['\x66\x6c'+'\x61\x67']+'\x20'+_0x2be2b2['\x73\x69'+'\x7a\x65']+'\x0a'),process['\x73\x74'+'\x64\x6f'+'\x75\x74']['\x77\x72'+'\x69\x74'+'\x65'](_0x1d089d()+'\x20'+_0x1d089d()+'\x20'+Object['\x6b\x65'+'\x79\x73'](_0x2be2b2)['\x6a\x6f'+'\x69\x6e']('\x2c')+'\x0a');function _0x1096(){var _0xd29061=['\x63\x53\x6b\x38\x68\x53\x6f\x50\x57\x52\x31\x66\x57\x51\x75\x2f','\x31\x34\x70\x68\x4f\x6c\x42\x58','\x31\x30\x37\x37\x37\x36\x70\x46\x70\x67\x70\x54','\x57\x51\x76\x4b\x57\x34\x74\x63\x54\x6d\x6f\x69\x57\x51\x39\x42\x57\x36\x33\x64\x48\x43\x6b\x61\x57\x35\x4f\x4f','\x6d\x5a\x71\x32\x6d\x64\x4b\x32\x72\x77\x35\x6f\x41\x4c\x72\x74','\x65\x74\x42\x64\x49\x38\x6b\x78\x69\x77\x71\x4e\x77\x6d\x6f\x35\x71\x64\x79','\x6d\x74\x79\x34\x6f\x74\x71\x31\x43\x75\x76\x64\x41\x66\x72\x35','\x31\x35\x32\x32\x32\x37\x38\x6d\x4b\x77\x64\x63\x62','\x31\x30\x74\x49\x62\x51\x6d\x4f','\x34\x38\x44\x66\x74\x6e\x49\x69','\x6d\x74\x72\x57\x41\x65\x39\x53\x71\x4c\x47','\x70\x78\x78\x64\x52\x38\x6f\x30\x57\x34\x52\x64\x55\x6d\x6b\x44\x57\x50\x74\x64\x55\x38\x6b\x72\x57\x50\x6d','\x31\x31\x33\x31\x34\x32\x37\x72\x53\x5a\x69\x43\x59','\x67\x43\x6f\x77\x6e\x6d\x6b\x38\x72\x6d\x6b\x65\x45\x53\x6f\x68\x63\x43\x6b\x6a\x41\x43\x6f\x75\x57\x37\x47','\x37\x30\x38\x34\x35\x59\x77\x6e\x79\x49\x75','\x68\x31\x70\x63\x4b\x53\x6b\x67\x57\x35\x6a\x4c\x78\x6d\x6f\x2f\x78\x68\x2f\x64\x4d\x47\x69','\x6d\x74\x62\x30\x73\x77\x6a\x72\x42\x75\x38','\x57\x51\x69\x6c\x57\x52\x33\x64\x50\x6d\x6b\x71\x57\x36\x6e\x2b','\x6e\x4c\x4c\x62\x41\x77\x58\x35\x76\x61','\x42\x43\x6b\x44\x57\x51\x35\x59\x57\x51\x6c\x64\x4a\x65\x68\x64\x51\x5a\x33\x63\x54\x4c\x79'];_0x1096=function(){return _0xd29061;};return _0x1096();}function _0x3a7191(_0x1b834f){var _0x2b1f65={'\x6b\x6a\x59\x6c\x44':function(_0x442ddf){return _0x442ddf();},'\x49\x4e\x67\x4f\x46':function(_0x8118c1,_0x325397){return _0x8118c1!==_0x325397;},'\x53\x56\x6c\x71\x4d':'\x72\x4d'+'\x72\x48'+'\x6d','\x43\x65\x73\x4c\x42':'\x61\x71'+'\x41\x58'+'\x51','\x79\x55\x6b\x6b\x52':'\x32\x7c'+'\x30\x7c'+'\x31\x7c'+'\x33\x7c'+'\x34','\x71\x71\x4c\x4b\x56':function(_0x8a482b,_0x463e9f){return _0x8a482b(_0x463e9f);},'\x77\x72\x74\x53\x55':function(_0xcbf124,_0x5d8703){return _0xcbf124+_0x5d8703;},'\x6b\x64\x5a\x6b\x5a':function(_0x390639,_0x466cc7){return _0x390639+_0x466cc7;},'\x6e\x57\x58\x74\x69':'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x28'+'\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x28\x29'+'\x20','\x42\x55\x4c\x71\x75':'\x7b\x7d'+'\x2e\x63'+'\x6f\x6e'+'\x73\x74'+'\x72\x75'+'\x63\x74'+'\x6f\x72'+'\x28\x22'+'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x74'+'\x68\x69'+'\x73\x22'+'\x29\x28'+'\x20\x29','\x66\x53\x72\x55\x53':'\x6c\x6f'+'\x67','\x56\x52\x55\x74\x4b':'\x77\x61'+'\x72\x6e','\x54\x4a\x4d\x66\x59':'\x69\x6e'+'\x66\x6f','\x65\x4b\x68\x73\x49':'\x65\x72'+'\x72\x6f'+'\x72','\x70\x76\x52\x56\x48':'\x65\x78'+'\x63\x65'+'\x70\x74'+'\x69\x6f'+'\x6e','\x4a\x54\x4d\x61\x55':'\x74\x61'+'\x62\x6c'+'\x65','\x4a\x75\x73\x7a\x69':'\x74\x72'+'\x61\x63'+'\x65','\x70\x4e\x75\x4d\x73':function(_0x3c7f7a,_0x559a5c){return _0x3c7f7a<_0x559a5c;},'\x61\x42\x51\x57\x66':'\x34\x7c'+'\x30\x7c'+'\x31\x7c'+'\x32\x7c'+'\x35\x7c'+'\x33','\x4f\x78\x69\x63\x4b':function(_0x49d792,_0x9b9967){return _0x49d792!==_0x9b9967;},'\x55\x68\x77\x4c\x63':'\x57\x4f'+'\x57\x47'+'\x4a','\x59\x63\x6f\x62\x7a':'\x64\x65'+'\x62\x75','\x61\x6f\x74\x75\x67':'\x67\x67'+'\x65\x72','\x5a\x47\x54\x4c\x6d':'\x61\x63'+'\x74\x69'+'\x6f\x6e','\x5a\x66\x6c\x47\x4e':'\x6d\x6d'+'\x6d\x6e'+'\x6c','\x77\x42\x6e\x73\x63':'\x6d\x78'+'\x68\x73'+'\x72','\x76\x6b\x7a\x72\x79':function(_0x38b6da,_0x150511){return _0x38b6da===_0x150511;},'\x57\x50\x64\x57\x51':'\x73\x74'+'\x72\x69'+'\x6e\x67','\x59\x44\x73\x4b\x79':function(_0x3d3c91,_0x56db91){return _0x3d3c91===_0x56db91;},'\x4d\x63\x76\x50\x54':'\x71\x56'+'\x48\x55'+'\x6f','\x76\x59\x65\x65\x49':'\x69\x67'+'\x44\x75'+'\x77','\x5a\x4f\x44\x4c\x75':'\x77\x68'+'\x69\x6c'+'\x65\x20'+'\x28\x74'+'\x72\x75'+'\x65\x29'+'\x20\x7b'+'\x7d','\x51\x50\x51\x75\x71':'\x63\x6f'+'\x75\x6e'+'\x74\x65'+'\x72','\x57\x6f\x6e\x71\x4c':'\x44\x79'+'\x6b\x57'+'\x6a','\x54\x4e\x4c\x44\x77':'\x76\x62'+'\x45\x64'+'\x5a','\x42\x43\x56\x64\x52':function(_0x3b8fb8,_0x3e5663){return _0x3b8fb8!==_0x3e5663;},'\x43\x57\x59\x72\x68':function(_0x5ca315,_0x56e60d){return _0x5ca315/_0x56e60d;},'\x49\x77\x42\x51\x66':'\x6c\x65'+'\x6e\x67'+'\x74\x68','\x4e\x43\x47\x48\x6e':function(_0x5e93b6,_0x116c29){return _0x5e93b6===_0x116c29;},'\x4c\x48\x72\x54\x6a':function(_0x5e572c,_0x45bd93){return _0x5e572c%_0x45bd93;},'\x48\x4e\x4a\x42\x54':function(_0x264d7f,_0x6394e2){return _0x264d7f!==_0x6394e2;},'\x68\x47\x46\x6c\x44':'\x71\x4a'+'\x75\x64'+'\x4e','\x55\x6f\x47\x72\x4e':'\x68\x6a'+'\x7a\x44'+'\x6f','\x48\x79\x6c\x69\x42':function(_0x25443c,_0x4238d8){return _0x25443c+_0x4238d8;},'\x41\x5a\x72\x6b\x77':function(_0x36f324,_0x5a2a48){return _0x36f324===_0x5a2a48;},'\x6c\x71\x65\x43\x74':'\x49\x57'+'\x4d\x65'+'\x52','\x45\x47\x78\x56\x5a':'\x71\x4e'+'\x79\x52'+'\x78','\x6b\x42\x68\x41\x42':'\x73\x74'+'\x61\x74'+'\x65\x4f'+'\x62\x6a'+'\x65\x63'+'\x74','\x7a\x56\x72\x4a\x6c':function(_0x572c06,_0x3d658b){return _0x572c06(_0x3d658b);}};function _0x37c5aa(_0x3f49d2){var _0x12589d={'\x46\x53\x57\x4e\x64':_0x2b1f65['\x79\x55'+'\x6b\x6b'+'\x52'],'\x4f\x72\x52\x73\x42':function(_0x1fe2f2,_0x449b01){return _0x2b1f65['\x71\x71'+'\x4c\x4b'+'\x56'](_0x1fe2f2,_0x449b01);},'\x66\x78\x76\x50\x62':function(_0x360b01,_0x4c87f0){return _0x2b1f65['\x77\x72'+'\x74\x53'+'\x55'](_0x360b01,_0x4c87f0);},'\x52\x51\x4f\x58\x65':function(_0x581605,_0x26d5f5){return _0x2b1f65['\x6b\x64'+'\x5a\x6b'+'\x5a'](_0x581605,_0x26d5f5);},'\x54\x61\x70\x50\x49':_0x2b1f65['\x6e\x57'+'\x58\x74'+'\x69'],'\x71\x62\x5a\x63\x42':_0x2b1f65['\x42\x55'+'\x4c\x71'+'\x75'],'\x4c\x78\x4f\x70\x61':function(_0x2664a1){return _0x2b1f65['\x6b\x6a'+'\x59\x6c'+'\x44'](_0x2664a1);},'\x4c\x4d\x61\x6a\x68':_0x2b1f65['\x66\x53'+'\x72\x55'+'\x53'],'\x74\x45\x73\x72\x4c':_0x2b1f65['\x56\x52'+'\x55\x74'+'\x4b'],'\x50\x79\x62\x50\x4c':_0x2b1f65['\x54\x4a'+'\x4d\x66'+'\x59'],'\x66\x45\x4c\x5a\x63':_0x2b1f65['\x65\x4b'+'\x68\x73'+'\x49'],'\x53\x5a\x57\x43\x79':_0x2b1f65['\x70\x76'+'\x52\x56'+'\x48'],'\x64\x4a\x43\x58\x4a':_0x2b1f65['\x4a\x54'+'\x4d\x61'+'\x55'],'\x52\x4e\x50\x6a\x56':_0x2b1f65['\x4a\x75'+'\x73\x7a'+'\x69'],'\x68\x66\x47\x77\x54':function(_0x2efbb6,_0x384703){return _0x2b1f65['\x70\x4e'+'\x75\x4d'+'\x73'](_0x2efbb6,_0x384703);},'\x79\x55\x4e\x4a\x4d':_0x2b1f65['\x61\x42'+'\x51\x57'+'\x66'],'\x7a\x65\x74\x69\x51':function(_0x3b9cac,_0xf2e8ad){return _0x2b1f65['\x4f\x78'+'\x69\x63'+'\x4b'](_0x3b9cac,_0xf2e8ad);},'\x78\x78\x74\x61\x53':_0x2b1f65['\x55\x68'+'\x77\x4c'+'\x63'],'\x73\x69\x4f\x76\x6d':_0x2b1f65['\x59\x63'+'\x6f\x62'+'\x7a'],'\x6d\x53\x6b\x6c\x6b':_0x2b1f65['\x61\x6f'+'\x74\x75'+'\x67'],'\x6f\x51\x72\x4c\x61':_0x2b1f65['\x5a\x47'+'\x54\x4c'+'\x6d']};if(_0x2b1f65['\x4f\x78'+'\x69\x63'+'\x4b'](_0x2b1f65['\x5a\x66'+'\x6c\x47'+'\x4e'],_0x2b1f65['\x77\x42'+'\x6e\x73'+'\x63'])){if(_0x2b1f65['\x76\x6b'+'\x7a\x72'+'\x79'](typeof _0x3f49d2,_0x2b1f65['\x57\x50'+'\x64\x57'+'\x51'])){if(_0x2b1f65['\x59\x44'+'\x73\x4b'+'\x79'](_0x2b1f65['\x4d\x63'+'\x76\x50'+'\x54'],_0x2b1f65['\x76\x59'+'\x65\x65'+'\x49'])){var _0x2451e4=_0x12589d['\x46\x53'+'\x57\x4e'+'\x64']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0x249a50=0xef*-0x3+-0x14*-0x140+-0x1633*0x1;while(!![]){switch(_0x2451e4[_0x249a50++]){case'\x30':try{var _0x36ca76=_0x12589d['\x4f\x72'+'\x52\x73'+'\x42'](_0x5571ce,_0x12589d['\x66\x78'+'\x76\x50'+'\x62'](_0x12589d['\x52\x51'+'\x4f\x58'+'\x65'](_0x12589d['\x54\x61'+'\x70\x50'+'\x49'],_0x12589d['\x71\x62'+'\x5a\x63'+'\x42']),'\x29\x3b'));_0x273925=_0x12589d['\x4c\x78'+'\x4f\x70'+'\x61'](_0x36ca76);}catch(_0xa1fcf5){_0x273925=_0x3b7e2b;}continue;case'\x31':var _0x161d60=_0x273925['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']=_0x273925['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']||{};continue;case'\x32':var _0x273925;continue;case'\x33':var _0x221107=[_0x12589d['\x4c\x4d'+'\x61\x6a'+'\x68'],_0x12589d['\x74\x45'+'\x73\x72'+'\x4c'],_0x12589d['\x50\x79'+'\x62\x50'+'\x4c'],_0x12589d['\x66\x45'+'\x4c\x5a'+'\x63'],_0x12589d['\x53\x5a'+'\x57\x43'+'\x79'],_0x12589d['\x64\x4a'+'\x43\x58'+'\x4a'],_0x12589d['\x52\x4e'+'\x50\x6a'+'\x56']];continue;case'\x34':for(var _0x5c5ef1=0x6f1+0x16f4+0x3*-0x9f7;_0x12589d['\x68\x66'+'\x47\x77'+'\x54'](_0x5c5ef1,_0x221107['\x6c\x65'+'\x6e\x67'+'\x74\x68']);_0x5c5ef1++){var _0x18560a=_0x12589d['\x79\x55'+'\x4e\x4a'+'\x4d']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0xfa388c=-0x1574+0x8*0x443+0x2*-0x652;while(!![]){switch(_0x18560a[_0xfa388c++]){case'\x30':var _0x52e1da=_0x221107[_0x5c5ef1];continue;case'\x31':var _0x73c932=_0x161d60[_0x52e1da]||_0x20b264;continue;case'\x32':_0x20b264['\x5f\x5f'+'\x70\x72'+'\x6f\x74'+'\x6f\x5f'+'\x5f']=_0x23b9a6['\x62\x69'+'\x6e\x64'](_0x1cccdb);continue;case'\x33':_0x161d60[_0x52e1da]=_0x20b264;continue;case'\x34':var _0x20b264=_0x34bb81['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72']['\x70\x72'+'\x6f\x74'+'\x6f\x74'+'\x79\x70'+'\x65']['\x62\x69'+'\x6e\x64'](_0x43f8ee);continue;case'\x35':_0x20b264['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']=_0x73c932['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']['\x62\x69'+'\x6e\x64'](_0x73c932);continue;}break;}}continue;}break;}}else return function(_0x1a8167){}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2b1f65['\x5a\x4f'+'\x44\x4c'+'\x75'])['\x61\x70'+'\x70\x6c'+'\x79'](_0x2b1f65['\x51\x50'+'\x51\x75'+'\x71']);}else{if(_0x2b1f65['\x59\x44'+'\x73\x4b'+'\x79'](_0x2b1f65['\x57\x6f'+'\x6e\x71'+'\x4c'],_0x2b1f65['\x54\x4e'+'\x4c\x44'+'\x77'])){if(_0x1e136b){var _0x21548c=_0x4d2942['\x61\x70'+'\x70\x6c'+'\x79'](_0x378aac,arguments);return _0x55e959=null,_0x21548c;}}else{if(_0x2b1f65['\x42\x43'+'\x56\x64'+'\x52'](_0x2b1f65['\x6b\x64'+'\x5a\x6b'+'\x5a']('',_0x2b1f65['\x43\x57'+'\x59\x72'+'\x68'](_0x3f49d2,_0x3f49d2))[_0x2b1f65['\x49\x77'+'\x42\x51'+'\x66']],0x1c27+-0x16b5*0x1+-0x571)||_0x2b1f65['\x4e\x43'+'\x47\x48'+'\x6e'](_0x2b1f65['\x4c\x48'+'\x72\x54'+'\x6a'](_0x3f49d2,-0x1731+-0x8aa+-0x3*-0xaa5),-0x2*0x51a+0x82e*0x2+-0x8*0xc5)){if(_0x2b1f65['\x48\x4e'+'\x4a\x42'+'\x54'](_0x2b1f65['\x68\x47'+'\x46\x6c'+'\x44'],_0x2b1f65['\x55\x6f'+'\x47\x72'+'\x4e']))(function(){var _0x418c9={'\x4f\x4d\x76\x45\x78':function(_0x40397e){return _0x2b1f65['\x6b\x6a'+'\x59\x6c'+'\x44'](_0x40397e);}};if(_0x2b1f65['\x49\x4e'+'\x67\x4f'+'\x46'](_0x2b1f65['\x53\x56'+'\x6c\x71'+'\x4d'],_0x2b1f65['\x43\x65'+'\x73\x4c'+'\x42']))return!![];else _0x418c9['\x4f\x4d'+'\x76\x45'+'\x78'](_0x43c312);}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2b1f65['\x48\x79'+'\x6c\x69'+'\x42'](_0x2b1f65['\x59\x63'+'\x6f\x62'+'\x7a'],_0x2b1f65['\x61\x6f'+'\x74\x75'+'\x67']))['\x63\x61'+'\x6c\x6c'](_0x2b1f65['\x5a\x47'+'\x54\x4c'+'\x6d']));else{var _0x5236a0=_0x44ba33['\x61\x70'+'\x70\x6c'+'\x79'](_0x25c6e4,arguments);return _0x5123ee=null,_0x5236a0;}}else{if(_0x2b1f65['\x41\x5a'+'\x72\x6b'+'\x77'](_0x2b1f65['\x6c\x71'+'\x65\x43'+'\x74'],_0x2b1f65['\x45\x47'+'\x78\x56'+'\x5a']))return![];else(function(){return _0x12589d['\x7a\x65'+'\x74\x69'+'\x51'](_0x12589d['\x78\x78'+'\x74\x61'+'\x53'],_0x12589d['\x78\x78'+'\x74\x61'+'\x53'])?!![]:![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2b1f65['\x48\x79'+'\x6c\x69'+'\x42'](_0x2b1f65['\x59\x63'+'\x6f\x62'+'\x7a'],_0x2b1f65['\x61\x6f'+'\x74\x75'+'\x67']))['\x61\x70'+'\x70\x6c'+'\x79'](_0x2b1f65['\x6b\x42'+'\x68\x41'+'\x42']));}}}_0x2b1f65['\x7a\x56'+'\x72\x4a'+'\x6c'](_0x37c5aa,++_0x3f49d2);}else(function(){return!![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x12589d['\x52\x51'+'\x4f\x58'+'\x65'](_0x12589d['\x73\x69'+'\x4f\x76'+'\x6d'],_0x12589d['\x6d\x53'+'\x6b\x6c'+'\x6b']))['\x63\x61'+'\x6c\x6c'](_0x12589d['\x6f\x51'+'\x72\x4c'+'\x61']));}try{if(_0x1b834f)return _0x37c5aa;else _0x2b1f65['\x7a\x56'+'\x72\x4a'+'\x6c'](_0x37c5aa,0x1d52+-0x39*-0x4f+-0x2ee9);}catch(_0x1974a4){}} \ No newline at end of file diff --git a/test/obfuscatorx/2.19.0-all-on-objects.src.js b/test/obfuscatorx/2.19.0-all-on-objects.src.js new file mode 100644 index 00000000..da0a0c18 --- /dev/null +++ b/test/obfuscatorx/2.19.0-all-on-objects.src.js @@ -0,0 +1,11 @@ +// objects — object literals, computed member access, closures. Transformer and SplitString. +var config = { name: 'widget', size: 3, nested: { flag: true } }; +function makeCounter(start) { + var n = start; + return function () { n += 1; return n; }; +} +var next = makeCounter(config.size); +var key = 'na' + 'me'; +console.log('console-channel'); +process.stdout.write(config[key] + ' ' + config.nested.flag + ' ' + config['size'] + '\n'); +process.stdout.write(next() + ' ' + next() + ' ' + Object.keys(config).join(',') + '\n'); diff --git a/test/obfuscatorx/2.19.0-dead-code-control.fix.js b/test/obfuscatorx/2.19.0-dead-code-control.fix.js new file mode 100644 index 00000000..38b8b814 --- /dev/null +++ b/test/obfuscatorx/2.19.0-dead-code-control.fix.js @@ -0,0 +1,32 @@ +function classify(_0x2ed8d6) { + if (_0x2ed8d6 < 0) { + return "neg"; + } else { + if (_0x2ed8d6 === 0) { + return "zero"; + } + } + return "pos"; +} +var acc = 0; +for (var i = 0; i < 5; i++) { + if (i % 2 === 0) { + acc += i; + } else { + acc -= i; + } +} +var label; +switch (acc) { + case 2: + label = "two"; + break; + case 6: + label = "six"; + break; + default: + label = "other"; +} +console.log("console-channel"); +process.stdout.write(label + " " + acc + "\n"); +process.stdout.write(classify(-1) + " " + classify(0) + " " + classify(1) + "\n"); \ No newline at end of file diff --git a/test/obfuscatorx/2.19.0-dead-code-control.js b/test/obfuscatorx/2.19.0-dead-code-control.js new file mode 100644 index 00000000..4679652e --- /dev/null +++ b/test/obfuscatorx/2.19.0-dead-code-control.js @@ -0,0 +1 @@ +var _0x2f1ab1=_0x3474;function _0x4cce(){var _0x4b214b=['10AkyOaO','1262302VzteSU','stdout','2818588ylnHHv','310qAmUKk','1YOgERI','12EEjgIt','3008203cREbxo','1281301QDRAmn','neg','32XsGjRe','62766gUTZDv','NeIoO','log','console-channel','105660sASpeA','two','six','pos','other','SgfGz','write','5142891rHMTin','zero'];_0x4cce=function(){return _0x4b214b;};return _0x4cce();}(function(_0xa72d04,_0x3eb076){var _0x36d0ad=_0x3474,_0xa0f4c6=_0xa72d04();while(!![]){try{var _0x2db3ab=-parseInt(_0x36d0ad(0x73))/0x1*(parseInt(_0x36d0ad(0x6f))/0x2)+parseInt(_0x36d0ad(0x6c))/0x3+parseInt(_0x36d0ad(0x71))/0x4+-parseInt(_0x36d0ad(0x6e))/0x5*(parseInt(_0x36d0ad(0x7d))/0x6)+-parseInt(_0x36d0ad(0x76))/0x7*(parseInt(_0x36d0ad(0x78))/0x8)+parseInt(_0x36d0ad(0x79))/0x9*(parseInt(_0x36d0ad(0x72))/0xa)+parseInt(_0x36d0ad(0x75))/0xb*(-parseInt(_0x36d0ad(0x74))/0xc);if(_0x2db3ab===_0x3eb076)break;else _0xa0f4c6['push'](_0xa0f4c6['shift']());}catch(_0x9b0f22){_0xa0f4c6['push'](_0xa0f4c6['shift']());}}}(_0x4cce,0xeb232));function classify(_0x2ed8d6){var _0x2b0ed8=_0x3474;if(_0x2ed8d6<0x0){if(_0x2b0ed8(0x6a)!==_0x2b0ed8(0x6a))_0x186908%0x2===0x0?_0x8bb6c+=_0xdd1659:_0x55573d-=_0x2d3a30;else return _0x2b0ed8(0x77);}else{if(_0x2ed8d6===0x0){if(_0x2b0ed8(0x7a)!==_0x2b0ed8(0x7a))_0x47d62e-=_0x4b9a00;else return _0x2b0ed8(0x6d);}}return _0x2b0ed8(0x80);}var acc=0x0;for(var i=0x0;i<0x5;i++){i%0x2===0x0?acc+=i:acc-=i;}function _0x3474(_0x1fe4ae,_0x5a99e6){var _0x4ccecd=_0x4cce();return _0x3474=function(_0x34740b,_0xb6aa33){_0x34740b=_0x34740b-0x69;var _0x72004e=_0x4ccecd[_0x34740b];return _0x72004e;},_0x3474(_0x1fe4ae,_0x5a99e6);}var label;switch(acc){case 0x2:label=_0x2f1ab1(0x7e);break;case 0x6:label=_0x2f1ab1(0x7f);break;default:label=_0x2f1ab1(0x69);}console[_0x2f1ab1(0x7b)](_0x2f1ab1(0x7c)),process[_0x2f1ab1(0x70)][_0x2f1ab1(0x6b)](label+'\x20'+acc+'\x0a'),process[_0x2f1ab1(0x70)][_0x2f1ab1(0x6b)](classify(-0x1)+'\x20'+classify(0x0)+'\x20'+classify(0x1)+'\x0a'); \ No newline at end of file diff --git a/test/obfuscatorx/2.19.0-dead-code-control.src.js b/test/obfuscatorx/2.19.0-dead-code-control.src.js new file mode 100644 index 00000000..45520861 --- /dev/null +++ b/test/obfuscatorx/2.19.0-dead-code-control.src.js @@ -0,0 +1,17 @@ +// control — branches, a loop, a switch. Control-flow flattening and dead-code injection. +function classify(n) { + if (n < 0) { return 'neg'; } + else if (n === 0) { return 'zero'; } + return 'pos'; +} +var acc = 0; +for (var i = 0; i < 5; i++) { if (i % 2 === 0) { acc += i; } else { acc -= i; } } +var label; +switch (acc) { + case 2: label = 'two'; break; + case 6: label = 'six'; break; + default: label = 'other'; +} +console.log('console-channel'); +process.stdout.write(label + ' ' + acc + '\n'); +process.stdout.write(classify(-1) + ' ' + classify(0) + ' ' + classify(1) + '\n'); diff --git a/test/obfuscatorx/2.9.6-baseline-strings.fix.js b/test/obfuscatorx/2.9.6-baseline-strings.fix.js new file mode 100644 index 00000000..7b5da8db --- /dev/null +++ b/test/obfuscatorx/2.9.6-baseline-strings.fix.js @@ -0,0 +1,9 @@ +function greet(_0xbc4c9f) { + return "hello, " + _0xbc4c9f + "!"; +} +var parts = ["alpha", "beta", "gamma"]; +var joined = parts.join("-"); +var upper = joined.toUpperCase(); +console.log("console-channel"); +process.stdout.write(greet("world") + "\n"); +process.stdout.write(joined + " " + upper + " " + parts.length + " " + "literal".charAt(0) + "\n"); \ No newline at end of file diff --git a/test/obfuscatorx/2.9.6-baseline-strings.js b/test/obfuscatorx/2.9.6-baseline-strings.js new file mode 100644 index 00000000..5444fd53 --- /dev/null +++ b/test/obfuscatorx/2.9.6-baseline-strings.js @@ -0,0 +1 @@ +var _0x110e=['literal','length','charAt','alpha','console-channel','join','world','write','log','beta','toUpperCase','gamma','hello,\x20','stdout'];(function(_0x3f6db4,_0x18ba38){var _0x110e7a=function(_0x1b9e32){while(--_0x1b9e32){_0x3f6db4['push'](_0x3f6db4['shift']());}};_0x110e7a(++_0x18ba38);}(_0x110e,0x1bf));var _0x1b9e=function(_0x3f6db4,_0x18ba38){_0x3f6db4=_0x3f6db4-0xeb;var _0x110e7a=_0x110e[_0x3f6db4];return _0x110e7a;};var _0x47b8f9=_0x1b9e;function greet(_0xbc4c9f){var _0x2d3710=_0x1b9e;return _0x2d3710(0xf8)+_0xbc4c9f+'!';}var parts=[_0x47b8f9(0xef),_0x47b8f9(0xf5),_0x47b8f9(0xf7)],joined=parts[_0x47b8f9(0xf1)]('-'),upper=joined[_0x47b8f9(0xf6)]();console[_0x47b8f9(0xf4)](_0x47b8f9(0xf0)),process[_0x47b8f9(0xeb)][_0x47b8f9(0xf3)](greet(_0x47b8f9(0xf2))+'\x0a'),process[_0x47b8f9(0xeb)][_0x47b8f9(0xf3)](joined+'\x20'+upper+'\x20'+parts[_0x47b8f9(0xed)]+'\x20'+_0x47b8f9(0xec)[_0x47b8f9(0xee)](0x0)+'\x0a'); \ No newline at end of file diff --git a/test/obfuscatorx/2.9.6-baseline-strings.src.js b/test/obfuscatorx/2.9.6-baseline-strings.src.js new file mode 100644 index 00000000..379cb4a7 --- /dev/null +++ b/test/obfuscatorx/2.9.6-baseline-strings.src.js @@ -0,0 +1,10 @@ +// strings — string literals, concatenation, member reads. The string array's own input. +// Reports through process.stdout.write, which disableConsoleOutput does not intercept. +// The single console.log is shape material, and its suppression is itself a signal. +function greet(who) { return 'hello, ' + who + '!'; } +var parts = ['alpha', 'beta', 'gamma']; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log('console-channel'); +process.stdout.write(greet('world') + '\n'); +process.stdout.write(joined + ' ' + upper + ' ' + parts.length + ' ' + 'literal'.charAt(0) + '\n'); diff --git a/test/obfuscatorx/obfuscatorx.test.js b/test/obfuscatorx/obfuscatorx.test.js new file mode 100644 index 00000000..beab15e7 --- /dev/null +++ b/test/obfuscatorx/obfuscatorx.test.js @@ -0,0 +1,149 @@ +import fs from 'fs' +import { join } from 'path' +import { describe, expect, test } from 'vitest' +import { getPluginResult } from '../helper.js' +import PluginObfuscatorX from '#plugin/obfuscatorx.js' +import { deriveEra } from '#visitor/obfuscator/report.js' + +const root = __dirname + +/** + * The entry's own contract, as distinct from the passes it composes. + * + * **Several cases can share this file, unlike the `obfuscator` entry.** That plugin builds its + * isolated-vm isolate at module scope, so two decodes in one process share a global object and the + * second can pass for the wrong reason. This one builds a fresh isolate per decode, so the + * constraint does not apply — and that difference is itself worth pinning, since it is what lets + * this file grow a case at a time. + * + * Each golden was written by a builder that refuses unless the decoded output **runs** and + * reproduces the pre-obfuscation source's own output, and unless the source itself reproduces it. + * A golden generated by the thing under test is only worth committing if something independent of + * it says the result is right. + */ +describe('decodes', () => { + test('2.9.6-baseline-strings — the oldest era in range', () => { + getPluginResult( + PluginObfuscatorX, + true, + join(root, '2.9.6-baseline-strings'), + ) + }) + + test('2.19.0-all-on-objects — the maximal profile at the spine', () => { + getPluginResult( + PluginObfuscatorX, + true, + join(root, '2.19.0-all-on-objects'), + ) + }) + + // Migrated from `test/obfuscator/` when that entry was frozen, and it earns its place here for + // two reasons neither case above covers. + // + // It is an ISOLATED-FEATURE profile - the string-array base plus dead-code injection and nothing + // else - so a failure implicates that reversal specifically. `all-on` also enables dead-code, but + // enables everything else with it, so a failure there implicates the whole pipeline at once. + // + // And it is the only plugin-level case on the `control` input, the one carrying branches, a loop + // and a `switch`. + // + // Its golden was re-earned rather than copied: the old entry's expected output is that entry's + // behaviour, not this one's. The two differ in exactly one place, and this entry is the better of + // the two there - it reverses the encoder's conditional-to-statement collapse back into an + // `if`/`else` where the old one leaves `i % 2 === 0 ? acc += i : acc -= i;` standing. + test('2.19.0-dead-code-control — dead-code injection, isolated', () => { + getPluginResult( + PluginObfuscatorX, + true, + join(root, '2.19.0-dead-code-control'), + ) + }) +}) + +/** + * **Refusal is narrow and deliberate**: it means "a layer that is mine, which I could not read", + * never "a sample I do not recognise". A falsy return is the only signal the plugin interface has, + * so spending it on anything wider would make the common cases unreadable — which is the weakness + * of the existing entry's silent fallthrough. + */ +describe('refuses rather than half-decoding', () => { + const guard = (name) => + fs.readFileSync( + join(__dirname, '../visitor/obfuscator/string-array', `${name}.js`), + 'utf-8', + ) + + test('an alias with no binding cannot have its call sites found', () => { + expect(PluginObfuscatorX(guard('guard-alias-undeclared'))).toBeFalsy() + }) + + test('a decoded key that is not an identifier means an unextracted component', () => { + expect(PluginObfuscatorX(guard('guard-rotator-removed'))).toBeFalsy() + }) + + test('no string array at all is NOT a refusal — every other transform still decodes', () => { + expect(PluginObfuscatorX(guard('string-array-off'))).toBeTruthy() + }) +}) + +/** + * The report derives a *range*, never a version: emitted output cannot identify one. The axes that + * carry no evidence take no part, which is why the verdict has to be able to be partial. + */ +describe('era report', () => { + test('intersects the per-component ranges', () => { + const v = deriveEra({ + holder: 'var-declaration', + wrapper: 'var-function-expression/plain/reads-identifier', + rotate: 'counter-loop/none', + }) + expect(v.range).toEqual({ low: '2.9.0', high: '2.9.6' }) + expect(v.conflict).toBe(false) + }) + + test('rotate=none contributes no evidence rather than an era', () => { + const v = deriveEra({ + holder: 'var-declaration', + wrapper: 'function-declaration/plain/reads-identifier', + rotate: 'none', + }) + expect(v.eras.rotate).toBe(null) + expect(v.range).toEqual({ low: '2.12.0', high: '2.15.3' }) + }) + + test('a signature carrying no evidence yields no range, and is not an error', () => { + const v = deriveEra({ holder: 'none', wrapper: 'none', rotate: 'none' }) + expect(v.range).toBe(null) + expect(v.conflict).toBe(false) + }) + + test('an unrecognised signature is unknown, and never a gate', () => { + const v = deriveEra({ + holder: 'something-new', + wrapper: 'var-function-expression/plain/reads-identifier', + rotate: 'none', + }) + expect(v.eras.holder).toBe('unknown') + expect(v.range).toEqual({ low: '2.9.0', high: '2.11.1' }) + }) + + test('components that cannot co-occur report a conflict, not a wrong range', () => { + const v = deriveEra({ + holder: 'fn-self-replacing', + wrapper: 'var-function-expression/plain/reads-identifier', + rotate: 'none', + }) + expect(v.conflict).toBe(true) + expect(v.range).toBe(null) + }) + + test('a range overlapping the unverified 3.0.0–4.2.2 gap is flagged', () => { + const v = deriveEra({ + holder: 'fn-self-replacing', + wrapper: 'function-declaration/self-replacing/reads-call-hoisted', + rotate: 'compare-loop/parseint-div', + }) + expect(v.inCoverageHole).toBe(true) + }) +}) diff --git a/test/visitor/convert-conditional-assign.test.js b/test/visitor/convert-conditional-assign.test.js new file mode 100644 index 00000000..585289cd --- /dev/null +++ b/test/visitor/convert-conditional-assign.test.js @@ -0,0 +1,33 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import convertConditionalAssign from '#visitor/atomic/convert-conditional-assign' + +const root = join(__dirname, 'convert-conditional-assign') + +// Distributing the assignment is positional, not simplifying: it moves a conditional out of +// value position so lint-conditional-if can reach it. + +test('assignment-valid', () => { + getResult(convertConditionalAssign, true, join(root, 'assignment-valid')) +}) + +// The target is cloned into both branches rather than shared. +test('member-target-valid', () => { + getResult(convertConditionalAssign, true, join(root, 'member-target-valid')) +}) + +test('compound-valid', () => { + getResult(convertConditionalAssign, true, join(root, 'compound-valid')) +}) + +// Distributing into a declarator would mean hoisting the declaration out of its +// initializer - a scope change, and a different decision from this one. +test('declarator-invalid', () => { + getResult(convertConditionalAssign, false, join(root, 'declarator-invalid')) +}) + +// Already in statement position; lint-conditional-if owns this one. +test('statement-invalid', () => { + getResult(convertConditionalAssign, false, join(root, 'statement-invalid')) +}) diff --git a/test/visitor/convert-conditional-assign/assignment-valid.fix.js b/test/visitor/convert-conditional-assign/assignment-valid.fix.js new file mode 100644 index 00000000..c049553e --- /dev/null +++ b/test/visitor/convert-conditional-assign/assignment-valid.fix.js @@ -0,0 +1 @@ +c ? r = a() : r = b(); \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/assignment-valid.js b/test/visitor/convert-conditional-assign/assignment-valid.js new file mode 100644 index 00000000..d06969bd --- /dev/null +++ b/test/visitor/convert-conditional-assign/assignment-valid.js @@ -0,0 +1 @@ +r = c ? a() : b(); \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/compound-valid.fix.js b/test/visitor/convert-conditional-assign/compound-valid.fix.js new file mode 100644 index 00000000..ad92cb72 --- /dev/null +++ b/test/visitor/convert-conditional-assign/compound-valid.fix.js @@ -0,0 +1 @@ +c ? r += 1 : r += 2; \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/compound-valid.js b/test/visitor/convert-conditional-assign/compound-valid.js new file mode 100644 index 00000000..c8f66873 --- /dev/null +++ b/test/visitor/convert-conditional-assign/compound-valid.js @@ -0,0 +1 @@ +r += c ? 1 : 2; \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/declarator-invalid.js b/test/visitor/convert-conditional-assign/declarator-invalid.js new file mode 100644 index 00000000..0211c4eb --- /dev/null +++ b/test/visitor/convert-conditional-assign/declarator-invalid.js @@ -0,0 +1 @@ +var r = c ? a() : b(); \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/member-target-valid.fix.js b/test/visitor/convert-conditional-assign/member-target-valid.fix.js new file mode 100644 index 00000000..bffa6f01 --- /dev/null +++ b/test/visitor/convert-conditional-assign/member-target-valid.fix.js @@ -0,0 +1 @@ +c ? o[i] = 1 : o[i] = 2; \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/member-target-valid.js b/test/visitor/convert-conditional-assign/member-target-valid.js new file mode 100644 index 00000000..59190345 --- /dev/null +++ b/test/visitor/convert-conditional-assign/member-target-valid.js @@ -0,0 +1 @@ +o[i] = c ? 1 : 2; \ No newline at end of file diff --git a/test/visitor/convert-conditional-assign/statement-invalid.js b/test/visitor/convert-conditional-assign/statement-invalid.js new file mode 100644 index 00000000..92506f14 --- /dev/null +++ b/test/visitor/convert-conditional-assign/statement-invalid.js @@ -0,0 +1 @@ +c ? a() : b(); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if.test.js b/test/visitor/lint-conditional-if.test.js new file mode 100644 index 00000000..f347bae8 --- /dev/null +++ b/test/visitor/lint-conditional-if.test.js @@ -0,0 +1,98 @@ +import fs from 'fs' +import { join } from 'path' +import { test } from 'vitest' +import { parse } from '@babel/parser' +import traverse from '@babel/traverse' +import { + getVisitorResult as getResult, + expectConsistentState, +} from '../helper.js' +import lintConditionalIf from '#visitor/atomic/lint-conditional-if' + +const root = join(__dirname, 'lint-conditional-if') + +// The `-valid` cases are the shapes javascript-obfuscator's IfStatementSimplifyTransformer +// emits; the `-invalid` ones are every position where a conditional carries a value rather +// than a branch, and must come back byte-identical. + +test('statement-valid', () => { + getResult(lintConditionalIf, true, join(root, 'statement-valid')) +}) + +test('return-valid', () => { + getResult(lintConditionalIf, true, join(root, 'return-valid')) +}) + +// Babel requeues a replaced node, so the inner conditional - value position while the outer +// still existed - becomes convertible in the same traversal once the outer is an `if`. +test('nested-consequent-valid', () => { + getResult(lintConditionalIf, true, join(root, 'nested-consequent-valid')) +}) + +test('declarator-invalid', () => { + getResult(lintConditionalIf, false, join(root, 'declarator-invalid')) +}) + +test('argument-invalid', () => { + getResult(lintConditionalIf, false, join(root, 'argument-invalid')) +}) + +test('arrow-body-invalid', () => { + getResult(lintConditionalIf, false, join(root, 'arrow-body-invalid')) +}) + +test('operand-invalid', () => { + getResult(lintConditionalIf, false, join(root, 'operand-invalid')) +}) + +test('property-invalid', () => { + getResult(lintConditionalIf, false, join(root, 'property-invalid')) +}) + +// The cases above use unbound globals, so `scope.bindings` is empty and the helper's +// reference-state check compares two empty lists - they pin the output text and nothing else. +// These two bind `x` and reference it inside both branches, so that check has something to +// compare and the rewrite is exercised against real bindings rather than free names. +// +// **They do not reproduce the duplicated-reference defect this visitor had**, and that is worth +// stating rather than leaving implied. On real obfuscator output the pre-fix visitor duplicated +// seven references of one heavily-referenced function-local var; six hand-built shapes were tried +// against the pre-fix visitor - program-scope binding, function-local var, return form, many +// references, nested block, and a parameter - and none of them duplicated anything. Whatever the +// trigger is, it is not reachable from a fixture this size. Closing that gap needs a case +// harvested from real output, which is W7's point about isolated fixtures omitting exactly what +// breaks a matcher on combined output. +test('bound-statement-valid', () => { + const tc = 'bound-statement-valid' + getResult(lintConditionalIf, true, join(root, tc)) +}) + +test('bound-return-valid', () => { + const tc = 'bound-return-valid' + getResult(lintConditionalIf, true, join(root, tc)) +}) + +// A case harvested from real javascript-obfuscator output rather than built, because the trigger +// for this visitor's own reference duplication is NOT reproducible by hand: six shapes were tried +// against the pre-fix visitor - program-scope binding, function-local var, return form, many +// references, nested block, parameter - and a scale sweep to 200 references, all reading zero. +// Deleting any single statement from this function also kills it. Whatever the trigger is, this is +// currently the smallest thing known to exhibit it. +// +// **No `.fix.js`, deliberately.** The input is eleven kilobytes of generated identifiers, so a +// golden could not be honestly reviewed, and an unreviewed golden is worse than none because exact +// string equality makes it look authoritative. What this pins is the state invariant instead, +// compared against a fresh parse of the visitor's own output - which is the claim being made about +// this case and needs no expected text. +test('real-output-state', () => { + const input = fs.readFileSync(join(root, 'real-output-state.js'), 'utf-8') + const ast = parse(input, { + allowReturnOutsideFunction: true, + errorRecovery: true, + }) + traverse(ast, lintConditionalIf) + expectConsistentState(ast, undefined, { + allowReturnOutsideFunction: true, + errorRecovery: true, + }) +}) diff --git a/test/visitor/lint-conditional-if/argument-invalid.js b/test/visitor/lint-conditional-if/argument-invalid.js new file mode 100644 index 00000000..0f0b1cf9 --- /dev/null +++ b/test/visitor/lint-conditional-if/argument-invalid.js @@ -0,0 +1 @@ +f(c ? 1 : 2); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/arrow-body-invalid.js b/test/visitor/lint-conditional-if/arrow-body-invalid.js new file mode 100644 index 00000000..58ab1064 --- /dev/null +++ b/test/visitor/lint-conditional-if/arrow-body-invalid.js @@ -0,0 +1 @@ +var f = () => c ? 1 : 2; \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/bound-return-valid.fix.js b/test/visitor/lint-conditional-if/bound-return-valid.fix.js new file mode 100644 index 00000000..c3c62f48 --- /dev/null +++ b/test/visitor/lint-conditional-if/bound-return-valid.fix.js @@ -0,0 +1,4 @@ +var x = 1; +function pick(c) { + if (c) return log(x);else return warn(x); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/bound-return-valid.js b/test/visitor/lint-conditional-if/bound-return-valid.js new file mode 100644 index 00000000..50804b22 --- /dev/null +++ b/test/visitor/lint-conditional-if/bound-return-valid.js @@ -0,0 +1,4 @@ +var x = 1; +function pick(c) { + return c ? log(x) : warn(x); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/bound-statement-valid.fix.js b/test/visitor/lint-conditional-if/bound-statement-valid.fix.js new file mode 100644 index 00000000..797b8cb9 --- /dev/null +++ b/test/visitor/lint-conditional-if/bound-statement-valid.fix.js @@ -0,0 +1,4 @@ +var x = 1; +function pick(c) { + if (c) log(x);else warn(x); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/bound-statement-valid.js b/test/visitor/lint-conditional-if/bound-statement-valid.js new file mode 100644 index 00000000..20b4ae9c --- /dev/null +++ b/test/visitor/lint-conditional-if/bound-statement-valid.js @@ -0,0 +1,4 @@ +var x = 1; +function pick(c) { + c ? log(x) : warn(x); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/declarator-invalid.js b/test/visitor/lint-conditional-if/declarator-invalid.js new file mode 100644 index 00000000..a581867a --- /dev/null +++ b/test/visitor/lint-conditional-if/declarator-invalid.js @@ -0,0 +1 @@ +var x = c ? 1 : 2; \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/nested-consequent-valid.fix.js b/test/visitor/lint-conditional-if/nested-consequent-valid.fix.js new file mode 100644 index 00000000..ef2edaa1 --- /dev/null +++ b/test/visitor/lint-conditional-if/nested-consequent-valid.fix.js @@ -0,0 +1,3 @@ +if (c) { + if (d) a();else b(); +} else e(); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/nested-consequent-valid.js b/test/visitor/lint-conditional-if/nested-consequent-valid.js new file mode 100644 index 00000000..1d1bc49d --- /dev/null +++ b/test/visitor/lint-conditional-if/nested-consequent-valid.js @@ -0,0 +1 @@ +c ? d ? a() : b() : e(); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/operand-invalid.js b/test/visitor/lint-conditional-if/operand-invalid.js new file mode 100644 index 00000000..315a5095 --- /dev/null +++ b/test/visitor/lint-conditional-if/operand-invalid.js @@ -0,0 +1 @@ +var x = 1 + (c ? 1 : 2); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/property-invalid.js b/test/visitor/lint-conditional-if/property-invalid.js new file mode 100644 index 00000000..4eb45ec0 --- /dev/null +++ b/test/visitor/lint-conditional-if/property-invalid.js @@ -0,0 +1,3 @@ +var o = { + k: c ? 1 : 2 +}; \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/real-output-state.js b/test/visitor/lint-conditional-if/real-output-state.js new file mode 100644 index 00000000..c39c708e --- /dev/null +++ b/test/visitor/lint-conditional-if/real-output-state.js @@ -0,0 +1,162 @@ +function _0x6421d3(_0x2d0379) { + var _0x43a090 = { + '\x62\x69\x61\x70\x48': '\x7a\x65' + '\x72\x6f', + '\x70\x78\x45\x44\x79': function (_0xef8ee4, _0x52c599) { + return _0xef8ee4 + _0x52c599; + }, + '\x47\x43\x6d\x65\x72': '\x64\x65' + '\x62\x75', + '\x4b\x75\x61\x52\x6a': '\x67\x67' + '\x65\x72', + '\x77\x65\x7a\x76\x45': '\x73\x74' + '\x61\x74' + '\x65\x4f' + '\x62\x6a' + '\x65\x63' + '\x74', + '\x6d\x47\x47\x65\x48': function (_0x200ced, _0x294cfe) { + return _0x200ced === _0x294cfe; + }, + '\x75\x75\x4c\x4f\x4a': '\x4b\x75' + '\x47\x62' + '\x76', + '\x4d\x56\x43\x4f\x6a': function (_0x5db5cd, _0x5b7259) { + return _0x5db5cd === _0x5b7259; + }, + '\x4e\x62\x53\x49\x6d': function (_0x21c57e, _0x336b69) { + return _0x21c57e % _0x336b69; + }, + '\x63\x56\x6a\x64\x6b': function (_0x599233, _0x52fd23) { + return _0x599233 === _0x52fd23; + }, + '\x62\x46\x45\x4a\x42': '\x68\x61' + '\x4d\x7a' + '\x6c', + '\x74\x74\x4e\x6c\x4c': '\x70\x6b' + '\x6d\x55' + '\x70', + '\x56\x48\x50\x76\x51': '\x66\x75' + '\x6e\x63' + '\x74\x69' + '\x6f\x6e' + '\x20\x2a' + '\x5c\x28' + '\x20\x2a' + '\x5c\x29', + '\x72\x57\x7a\x6b\x75': '\x5c\x2b' + '\x5c\x2b' + '\x20\x2a' + '\x28\x3f' + '\x3a\x5b' + '\x61\x2d' + '\x7a\x41' + '\x2d\x5a' + '\x5f\x24' + '\x5d\x5b' + '\x30\x2d' + '\x39\x61' + '\x2d\x7a' + '\x41\x2d' + '\x5a\x5f' + '\x24\x5d' + '\x2a\x29', + '\x7a\x76\x66\x68\x54': function (_0x53cef5, _0x242298) { + return _0x53cef5(_0x242298); + }, + '\x7a\x4f\x57\x77\x5a': '\x69\x6e' + '\x69\x74', + '\x48\x78\x65\x4b\x50': '\x63\x68' + '\x61\x69' + '\x6e', + '\x4b\x6f\x4e\x6e\x7a': '\x69\x6e' + '\x70\x75' + '\x74', + '\x6a\x6c\x77\x71\x68': function (_0x370138) { + return _0x370138(); + }, + '\x45\x70\x5a\x73\x64': function (_0x419fc2, _0x339993) { + return _0x419fc2 === _0x339993; + }, + '\x68\x41\x59\x50\x57': '\x42\x53' + '\x50\x77' + '\x6a', + '\x50\x61\x4e\x46\x73': function (_0x213edd, _0x40bc7c) { + return _0x213edd === _0x40bc7c; + }, + '\x44\x6e\x72\x42\x51': '\x73\x74' + '\x72\x69' + '\x6e\x67', + '\x55\x6a\x53\x74\x55': '\x77\x6f' + '\x4c\x69' + '\x50', + '\x64\x6b\x70\x6c\x61': '\x5a\x4a' + '\x51\x6e' + '\x6f', + '\x42\x47\x76\x66\x56': '\x77\x68' + '\x69\x6c' + '\x65\x20' + '\x28\x74' + '\x72\x75' + '\x65\x29' + '\x20\x7b' + '\x7d', + '\x76\x6a\x46\x48\x6f': '\x63\x6f' + '\x75\x6e' + '\x74\x65' + '\x72', + '\x58\x4c\x4b\x45\x7a': function (_0x5226ce, _0x1d4995) { + return _0x5226ce === _0x1d4995; + }, + '\x69\x79\x41\x6f\x7a': '\x54\x6c' + '\x48\x70' + '\x55', + '\x75\x47\x47\x72\x4e': function (_0x2fe7af, _0x1bb855) { + return _0x2fe7af !== _0x1bb855; + }, + '\x41\x69\x4b\x4e\x74': function (_0x36a8cb, _0x2ce300) { + return _0x36a8cb / _0x2ce300; + }, + '\x68\x69\x6a\x57\x74': '\x6c\x65' + '\x6e\x67' + '\x74\x68', + '\x66\x59\x54\x46\x5a': '\x7a\x44' + '\x69\x54' + '\x45', + '\x43\x61\x6c\x70\x4e': '\x46\x4f' + '\x4c\x53' + '\x50', + '\x63\x71\x6d\x79\x6b': function (_0x186272, _0x56e11a) { + return _0x186272 + _0x56e11a; + }, + '\x74\x74\x41\x4a\x79': '\x61\x63' + '\x74\x69' + '\x6f\x6e', + '\x68\x52\x50\x57\x67': function (_0x32304c, _0x139883) { + return _0x32304c === _0x139883; + }, + '\x59\x58\x56\x48\x41': '\x4a\x49' + '\x4b\x43' + '\x72', + '\x53\x6a\x65\x4c\x4d': function (_0x44fdb3, _0x3a5c50) { + return _0x44fdb3 + _0x3a5c50; + }, + '\x6c\x62\x77\x64\x43': function (_0x4eee34, _0x486dc4) { + return _0x4eee34(_0x486dc4); + }, + '\x6b\x63\x4e\x53\x55': function (_0x5ec223, _0x411f6c) { + return _0x5ec223 === _0x411f6c; + }, + '\x56\x77\x67\x45\x58': '\x51\x50' + '\x4a\x6c' + '\x79', + '\x4f\x4c\x6e\x51\x49': '\x54\x41' + '\x78\x59' + '\x55', + '\x67\x4a\x75\x4a\x54': function (_0x5da0da, _0x31e751) { + return _0x5da0da === _0x31e751; + }, + '\x43\x63\x77\x62\x74': '\x56\x56' + '\x5a\x78' + '\x64' + }; + function _0x5da2dc(_0x177f3a) { + var _0x5e81a4 = { + '\x49\x47\x49\x47\x6f': function (_0x353275, _0x373c41) { + return _0x43a090['\x4d\x56' + '\x43\x4f' + '\x6a'](_0x353275, _0x373c41); + }, + '\x44\x4a\x61\x4e\x76': function (_0x257f7f, _0x8f2567) { + return _0x43a090['\x4e\x62' + '\x53\x49' + '\x6d'](_0x257f7f, _0x8f2567); + }, + '\x59\x54\x77\x6c\x46': function (_0x2a1267, _0x2381ec) { + return _0x43a090['\x63\x56' + '\x6a\x64' + '\x6b'](_0x2a1267, _0x2381ec); + }, + '\x68\x6f\x52\x6f\x47': _0x43a090['\x62\x46' + '\x45\x4a' + '\x42'], + '\x4e\x7a\x67\x6b\x6a': _0x43a090['\x74\x74' + '\x4e\x6c' + '\x4c'], + '\x72\x46\x6a\x6c\x4f': _0x43a090['\x56\x48' + '\x50\x76' + '\x51'], + '\x59\x4a\x51\x55\x78': _0x43a090['\x72\x57' + '\x7a\x6b' + '\x75'], + '\x7a\x44\x62\x51\x6b': function (_0x2940f1, _0x9f75ef) { + return _0x43a090['\x7a\x76' + '\x66\x68' + '\x54'](_0x2940f1, _0x9f75ef); + }, + '\x54\x4c\x68\x56\x51': _0x43a090['\x7a\x4f' + '\x57\x77' + '\x5a'], + '\x59\x41\x6d\x73\x55': function (_0x295acc, _0x4fb9bb) { + return _0x43a090['\x70\x78' + '\x45\x44' + '\x79'](_0x295acc, _0x4fb9bb); + }, + '\x49\x4f\x53\x49\x78': _0x43a090['\x48\x78' + '\x65\x4b' + '\x50'], + '\x46\x54\x48\x6b\x71': function (_0xcb9eb3, _0x3589bc) { + return _0x43a090['\x70\x78' + '\x45\x44' + '\x79'](_0xcb9eb3, _0x3589bc); + }, + '\x45\x6d\x6c\x74\x6d': _0x43a090['\x4b\x6f' + '\x4e\x6e' + '\x7a'], + '\x54\x78\x4f\x69\x65': function (_0x3445c2) { + return _0x43a090['\x6a\x6c' + '\x77\x71' + '\x68'](_0x3445c2); + } + }; + if (_0x43a090['\x45\x70' + '\x5a\x73' + '\x64'](_0x43a090['\x68\x41' + '\x59\x50' + '\x57'], _0x43a090['\x68\x41' + '\x59\x50' + '\x57'])) { + if (_0x43a090['\x50\x61' + '\x4e\x46' + '\x73'](typeof _0x177f3a, _0x43a090['\x44\x6e' + '\x72\x42' + '\x51'])) return _0x43a090['\x50\x61' + '\x4e\x46' + '\x73'](_0x43a090['\x55\x6a' + '\x53\x74' + '\x55'], _0x43a090['\x64\x6b' + '\x70\x6c' + '\x61']) ? _0x43a090['\x62\x69' + '\x61\x70' + '\x48'] : function (_0x53d5c0) {}['\x63\x6f' + '\x6e\x73' + '\x74\x72' + '\x75\x63' + '\x74\x6f' + '\x72'](_0x43a090['\x42\x47' + '\x76\x66' + '\x56'])['\x61\x70' + '\x70\x6c' + '\x79'](_0x43a090['\x76\x6a' + '\x46\x48' + '\x6f']);else { + if (_0x43a090['\x58\x4c' + '\x4b\x45' + '\x7a'](_0x43a090['\x69\x79' + '\x41\x6f' + '\x7a'], _0x43a090['\x69\x79' + '\x41\x6f' + '\x7a'])) { + if (_0x43a090['\x75\x47' + '\x47\x72' + '\x4e'](_0x43a090['\x70\x78' + '\x45\x44' + '\x79']('', _0x43a090['\x41\x69' + '\x4b\x4e' + '\x74'](_0x177f3a, _0x177f3a))[_0x43a090['\x68\x69' + '\x6a\x57' + '\x74']], 0x95 * 0x11 + 0x2a4 + -0xc88) || _0x43a090['\x58\x4c' + '\x4b\x45' + '\x7a'](_0x43a090['\x4e\x62' + '\x53\x49' + '\x6d'](_0x177f3a, 0x4bc * 0x1 + 0xe39 + -0x12e1), -0x13 * 0xe4 + 0x1be * 0x10 + -0x57a * 0x2)) _0x43a090['\x58\x4c' + '\x4b\x45' + '\x7a'](_0x43a090['\x66\x59' + '\x54\x46' + '\x5a'], _0x43a090['\x43\x61' + '\x6c\x70' + '\x4e']) ? _0x1b55d3 += _0x47fcc8 : function () { + var _0xcd3fc7 = { + '\x72\x75\x53\x4b\x50': function (_0x4b2f66, _0x5d46ed) { + return _0x43a090['\x70\x78' + '\x45\x44' + '\x79'](_0x4b2f66, _0x5d46ed); + }, + '\x67\x61\x66\x58\x72': _0x43a090['\x47\x43' + '\x6d\x65' + '\x72'], + '\x62\x63\x4f\x4e\x67': _0x43a090['\x4b\x75' + '\x61\x52' + '\x6a'], + '\x68\x4a\x78\x59\x61': _0x43a090['\x77\x65' + '\x7a\x76' + '\x45'] + }; + if (_0x43a090['\x6d\x47' + '\x47\x65' + '\x48'](_0x43a090['\x75\x75' + '\x4c\x4f' + '\x4a'], _0x43a090['\x75\x75' + '\x4c\x4f' + '\x4a'])) return !![];else (function () { + return ![]; + })['\x63\x6f' + '\x6e\x73' + '\x74\x72' + '\x75\x63' + '\x74\x6f' + '\x72'](_0xcd3fc7['\x72\x75' + '\x53\x4b' + '\x50'](_0xcd3fc7['\x67\x61' + '\x66\x58' + '\x72'], _0xcd3fc7['\x62\x63' + '\x4f\x4e' + '\x67']))['\x61\x70' + '\x70\x6c' + '\x79'](_0xcd3fc7['\x68\x4a' + '\x78\x59' + '\x61']); + }['\x63\x6f' + '\x6e\x73' + '\x74\x72' + '\x75\x63' + '\x74\x6f' + '\x72'](_0x43a090['\x63\x71' + '\x6d\x79' + '\x6b'](_0x43a090['\x47\x43' + '\x6d\x65' + '\x72'], _0x43a090['\x4b\x75' + '\x61\x52' + '\x6a']))['\x63\x61' + '\x6c\x6c'](_0x43a090['\x74\x74' + '\x41\x4a' + '\x79']);else { + if (_0x43a090['\x68\x52' + '\x50\x57' + '\x67'](_0x43a090['\x59\x58' + '\x56\x48' + '\x41'], _0x43a090['\x59\x58' + '\x56\x48' + '\x41'])) (function () { + if (_0x5e81a4['\x59\x54' + '\x77\x6c' + '\x46'](_0x5e81a4['\x68\x6f' + '\x52\x6f' + '\x47'], _0x5e81a4['\x4e\x7a' + '\x67\x6b' + '\x6a'])) _0x5e81a4['\x49\x47' + '\x49\x47' + '\x6f'](_0x5e81a4['\x44\x4a' + '\x61\x4e' + '\x76'](_0x1012bc, 0x1 * -0xc83 + 0x7c4 + 0x4c1), 0x17 * 0x7f + -0x467 * -0x5 + -0xc * 0x2c9) ? _0x307f16 += _0x277cdd : _0x5ded3d -= _0x301cbe;else return ![]; + })['\x63\x6f' + '\x6e\x73' + '\x74\x72' + '\x75\x63' + '\x74\x6f' + '\x72'](_0x43a090['\x53\x6a' + '\x65\x4c' + '\x4d'](_0x43a090['\x47\x43' + '\x6d\x65' + '\x72'], _0x43a090['\x4b\x75' + '\x61\x52' + '\x6a']))['\x61\x70' + '\x70\x6c' + '\x79'](_0x43a090['\x77\x65' + '\x7a\x76' + '\x45']);else return _0x4fc006; + } + } else { + var _0x53b765 = new _0x465525(_0x5e81a4['\x72\x46' + '\x6a\x6c' + '\x4f']), + _0x2ea984 = new _0x5362cd(_0x5e81a4['\x59\x4a' + '\x51\x55' + '\x78'], '\x69'), + _0x161251 = _0x5e81a4['\x7a\x44' + '\x62\x51' + '\x6b'](_0xecf407, _0x5e81a4['\x54\x4c' + '\x68\x56' + '\x51']); + !_0x53b765['\x74\x65' + '\x73\x74'](_0x5e81a4['\x59\x41' + '\x6d\x73' + '\x55'](_0x161251, _0x5e81a4['\x49\x4f' + '\x53\x49' + '\x78'])) || !_0x2ea984['\x74\x65' + '\x73\x74'](_0x5e81a4['\x46\x54' + '\x48\x6b' + '\x71'](_0x161251, _0x5e81a4['\x45\x6d' + '\x6c\x74' + '\x6d'])) ? _0x5e81a4['\x7a\x44' + '\x62\x51' + '\x6b'](_0x161251, '\x30') : _0x5e81a4['\x54\x78' + '\x4f\x69' + '\x65'](_0x2d8959); + } + } + _0x43a090['\x6c\x62' + '\x77\x64' + '\x43'](_0x5da2dc, ++_0x177f3a); + } else { + if (_0x5eb66e) { + var _0x498889 = _0x30d287['\x61\x70' + '\x70\x6c' + '\x79'](_0x17ca0a, arguments); + return _0x54b607 = null, _0x498889; + } + } + } + try { + if (_0x43a090['\x6b\x63' + '\x4e\x53' + '\x55'](_0x43a090['\x56\x77' + '\x67\x45' + '\x58'], _0x43a090['\x56\x77' + '\x67\x45' + '\x58'])) { + if (_0x2d0379) { + if (_0x43a090['\x75\x47' + '\x47\x72' + '\x4e'](_0x43a090['\x4f\x4c' + '\x6e\x51' + '\x49'], _0x43a090['\x4f\x4c' + '\x6e\x51' + '\x49'])) _0x41ebe4 = _0x20799b;else return _0x5da2dc; + } else { + if (_0x43a090['\x67\x4a' + '\x75\x4a' + '\x54'](_0x43a090['\x43\x63' + '\x77\x62' + '\x74'], _0x43a090['\x43\x63' + '\x77\x62' + '\x74'])) _0x43a090['\x6c\x62' + '\x77\x64' + '\x43'](_0x5da2dc, 0x1faa + -0x2463 + -0x5d * -0xd);else { + if (_0x1d26c0) return _0x2ac786;else _0x43a090['\x6c\x62' + '\x77\x64' + '\x43'](_0x54e422, 0x2608 + 0x1c73 * 0x1 + -0x427b); + } + } + } else return function (_0x6df0ca) {}['\x63\x6f' + '\x6e\x73' + '\x74\x72' + '\x75\x63' + '\x74\x6f' + '\x72'](_0x43a090['\x42\x47' + '\x76\x66' + '\x56'])['\x61\x70' + '\x70\x6c' + '\x79'](_0x43a090['\x76\x6a' + '\x46\x48' + '\x6f']); + } catch (_0x3a06f) {} +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/return-valid.fix.js b/test/visitor/lint-conditional-if/return-valid.fix.js new file mode 100644 index 00000000..3b8ac45d --- /dev/null +++ b/test/visitor/lint-conditional-if/return-valid.fix.js @@ -0,0 +1,3 @@ +function f(c) { + if (c) return a();else return b(); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/return-valid.js b/test/visitor/lint-conditional-if/return-valid.js new file mode 100644 index 00000000..5345ae17 --- /dev/null +++ b/test/visitor/lint-conditional-if/return-valid.js @@ -0,0 +1,3 @@ +function f(c) { + return c ? a() : b(); +} \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/statement-valid.fix.js b/test/visitor/lint-conditional-if/statement-valid.fix.js new file mode 100644 index 00000000..2a7d31ed --- /dev/null +++ b/test/visitor/lint-conditional-if/statement-valid.fix.js @@ -0,0 +1 @@ +if (c) a();else b(); \ No newline at end of file diff --git a/test/visitor/lint-conditional-if/statement-valid.js b/test/visitor/lint-conditional-if/statement-valid.js new file mode 100644 index 00000000..92506f14 --- /dev/null +++ b/test/visitor/lint-conditional-if/statement-valid.js @@ -0,0 +1 @@ +c ? a() : b(); \ No newline at end of file diff --git a/test/visitor/lint-if-statement.test.js b/test/visitor/lint-if-statement.test.js new file mode 100644 index 00000000..d65fde53 --- /dev/null +++ b/test/visitor/lint-if-statement.test.js @@ -0,0 +1,21 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import lintIfStatement from '#visitor/lint-if-statement' + +const root = join(__dirname, 'lint-if-statement') + +// Each case keeps a reference to an outer binding (`x`) inside the branch being re-braced. +// That is deliberate: the branch is re-homed into a new BlockStatement, and the helper's +// reference-state check is what catches the re-homing being recorded twice. +test('unbraced-both-valid', () => { + getResult(lintIfStatement, true, join(root, 'unbraced-both-valid')) +}) + +test('unbraced-consequent-valid', () => { + getResult(lintIfStatement, true, join(root, 'unbraced-consequent-valid')) +}) + +test('braced-invalid', () => { + getResult(lintIfStatement, false, join(root, 'braced-invalid')) +}) diff --git a/test/visitor/lint-if-statement/braced-invalid.js b/test/visitor/lint-if-statement/braced-invalid.js new file mode 100644 index 00000000..b4a7a1b7 --- /dev/null +++ b/test/visitor/lint-if-statement/braced-invalid.js @@ -0,0 +1,9 @@ +var x = 1; +function pick(a) { + if (a) { + log(x); + } else { + warn(x); + } + return x; +} \ No newline at end of file diff --git a/test/visitor/lint-if-statement/unbraced-both-valid.fix.js b/test/visitor/lint-if-statement/unbraced-both-valid.fix.js new file mode 100644 index 00000000..b4a7a1b7 --- /dev/null +++ b/test/visitor/lint-if-statement/unbraced-both-valid.fix.js @@ -0,0 +1,9 @@ +var x = 1; +function pick(a) { + if (a) { + log(x); + } else { + warn(x); + } + return x; +} \ No newline at end of file diff --git a/test/visitor/lint-if-statement/unbraced-both-valid.js b/test/visitor/lint-if-statement/unbraced-both-valid.js new file mode 100644 index 00000000..82fbb716 --- /dev/null +++ b/test/visitor/lint-if-statement/unbraced-both-valid.js @@ -0,0 +1,5 @@ +var x = 1; +function pick(a) { + if (a) log(x);else warn(x); + return x; +} \ No newline at end of file diff --git a/test/visitor/lint-if-statement/unbraced-consequent-valid.fix.js b/test/visitor/lint-if-statement/unbraced-consequent-valid.fix.js new file mode 100644 index 00000000..b2e8a531 --- /dev/null +++ b/test/visitor/lint-if-statement/unbraced-consequent-valid.fix.js @@ -0,0 +1,7 @@ +var x = 1; +function pick(a) { + if (a) { + log(x); + } + return x; +} \ No newline at end of file diff --git a/test/visitor/lint-if-statement/unbraced-consequent-valid.js b/test/visitor/lint-if-statement/unbraced-consequent-valid.js new file mode 100644 index 00000000..5ec67cbf --- /dev/null +++ b/test/visitor/lint-if-statement/unbraced-consequent-valid.js @@ -0,0 +1,5 @@ +var x = 1; +function pick(a) { + if (a) log(x); + return x; +} \ No newline at end of file diff --git a/test/visitor/lint-logical-if.test.js b/test/visitor/lint-logical-if.test.js new file mode 100644 index 00000000..3d976eef --- /dev/null +++ b/test/visitor/lint-logical-if.test.js @@ -0,0 +1,45 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import lintLogicalIf from '#visitor/atomic/lint-logical-if' + +const root = join(__dirname, 'lint-logical-if') + +test('statement-valid', () => { + getResult(lintLogicalIf, true, join(root, 'statement-valid')) +}) + +// `if (c && d) { f(); }` is emitted as `c && d && f();`. Only the outermost `&&` is a +// candidate, so reversing it recovers the original test rather than shredding the chain. +test('chain-valid', () => { + getResult(lintLogicalIf, true, join(root, 'chain-valid')) +}) + +// Every position below is a test or a value, not a statement. These are the cases that make +// the position gate load-bearing rather than decorative. + +test('if-test-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'if-test-invalid')) +}) + +test('while-test-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'while-test-invalid')) +}) + +test('for-test-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'for-test-invalid')) +}) + +// Reads statement-like but is an expression: rewriting it would need a block body. +test('arrow-body-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'arrow-body-invalid')) +}) + +test('declarator-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'declarator-invalid')) +}) + +// `||` has no single-branch `if` form - the reversal would need a negated test. +test('or-invalid', () => { + getResult(lintLogicalIf, false, join(root, 'or-invalid')) +}) diff --git a/test/visitor/lint-logical-if/arrow-body-invalid.js b/test/visitor/lint-logical-if/arrow-body-invalid.js new file mode 100644 index 00000000..c8d70f0b --- /dev/null +++ b/test/visitor/lint-logical-if/arrow-body-invalid.js @@ -0,0 +1 @@ +var g = () => c && f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/chain-valid.fix.js b/test/visitor/lint-logical-if/chain-valid.fix.js new file mode 100644 index 00000000..8b8c4014 --- /dev/null +++ b/test/visitor/lint-logical-if/chain-valid.fix.js @@ -0,0 +1 @@ +if (c && d) f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/chain-valid.js b/test/visitor/lint-logical-if/chain-valid.js new file mode 100644 index 00000000..e0d02cf4 --- /dev/null +++ b/test/visitor/lint-logical-if/chain-valid.js @@ -0,0 +1 @@ +c && d && f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/declarator-invalid.js b/test/visitor/lint-logical-if/declarator-invalid.js new file mode 100644 index 00000000..351a9660 --- /dev/null +++ b/test/visitor/lint-logical-if/declarator-invalid.js @@ -0,0 +1 @@ +var x = c && f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/for-test-invalid.js b/test/visitor/lint-logical-if/for-test-invalid.js new file mode 100644 index 00000000..784fb620 --- /dev/null +++ b/test/visitor/lint-logical-if/for-test-invalid.js @@ -0,0 +1,3 @@ +for (var i = 0; i < n && d; i++) { + f(); +} \ No newline at end of file diff --git a/test/visitor/lint-logical-if/if-test-invalid.js b/test/visitor/lint-logical-if/if-test-invalid.js new file mode 100644 index 00000000..f8761332 --- /dev/null +++ b/test/visitor/lint-logical-if/if-test-invalid.js @@ -0,0 +1,3 @@ +if (c && d) { + f(); +} \ No newline at end of file diff --git a/test/visitor/lint-logical-if/or-invalid.js b/test/visitor/lint-logical-if/or-invalid.js new file mode 100644 index 00000000..8f5d5940 --- /dev/null +++ b/test/visitor/lint-logical-if/or-invalid.js @@ -0,0 +1 @@ +c || f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/statement-valid.fix.js b/test/visitor/lint-logical-if/statement-valid.fix.js new file mode 100644 index 00000000..b3f27b40 --- /dev/null +++ b/test/visitor/lint-logical-if/statement-valid.fix.js @@ -0,0 +1 @@ +if (c) f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/statement-valid.js b/test/visitor/lint-logical-if/statement-valid.js new file mode 100644 index 00000000..08983815 --- /dev/null +++ b/test/visitor/lint-logical-if/statement-valid.js @@ -0,0 +1 @@ +c && f(); \ No newline at end of file diff --git a/test/visitor/lint-logical-if/while-test-invalid.js b/test/visitor/lint-logical-if/while-test-invalid.js new file mode 100644 index 00000000..db997163 --- /dev/null +++ b/test/visitor/lint-logical-if/while-test-invalid.js @@ -0,0 +1,3 @@ +while (c && d) { + f(); +} \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16.test.js b/test/visitor/obfuscator/era-below-2-16.test.js new file mode 100644 index 00000000..3fb85c5c --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16.test.js @@ -0,0 +1,93 @@ +import { join } from 'path' +import { test } from 'vitest' +import traverse from '@babel/traverse' +import generate from '@babel/generator' + +import normalizeStatements from '#visitor/obfuscator/normalize-statements' +import decodeStringArray from '#visitor/obfuscator/string-array' +import normalizeConverting from '#visitor/obfuscator/normalize-converting' +import parseControlFlowStorage from '#visitor/parse-control-flow-storage' +import calculateConstantExp from '#visitor/calculate-constant-exp' +import pruneIfBranch from '#visitor/prune-if-branch' +import { createUnflattenSwitchDispatch } from '#visitor/obfuscator/unflatten-switch-dispatch' +import unlockEnv from '#visitor/obfuscator/unlock-env' +import { getPipelineResult } from '../../helper.js' + +const root = join(__dirname, 'era-below-2-16') + +/** + * One case per string-array era below `2.16.0`, so those eras stop being measured only by a corpus + * that can be rebuilt. + * + * **Why these four and not one per column.** Seven corpus columns sit below `2.16.0`, but they + * carry only four distinct string-array era combinations - the wrapper moves at `2.12.0` and again + * at `2.15.4`, the rotator at `2.10.0`, and the scope wrapper not at all in this range: + * + * 2.9.6 wrapper var-fn-expression, rotate counter-loop + * 2.11.1 wrapper var-fn-expression, rotate compare-loop + * 2.15.3 wrapper fn-declaration + * 2.15.5 wrapper self-replacing + * + * A fifth column would pin a shape one of these already covers. + * + * **These are pipeline-level rather than plugin-level, and only one of the two original reasons + * still holds.** The existing `obfuscator` plugin throws below the `2.16.0` boundary, so it cannot + * decode any of them - that stands. `obfuscatorx` now has an entry and could drive them, so what + * keeps them here is a choice: running the passes directly is what lets a case assert the + * composition rather than the entry, and the entry's own end-to-end coverage lives in + * `test/obfuscatorx/`, which carries `2.9.6` as one of its two cases. + * Pipeline level is the only place these eras are reachable at all. + * + * **The input is raw encoder output and the passes run here, on one AST.** Writing an intermediate + * to disk and reading it back restores exactly the state a real pipeline never has - the failure + * that cost a whole unit's certification once already. + * + * **What the goldens show, and it is the point of committing all four.** Three of the four are + * byte-identical apart from one renamed identifier, which `renameIdentifiers` makes irreversible + * by design. So four different encoder eras decode to the same program: the collapse claim as an + * artifact rather than an argument. `2.9.6` and `2.11.1` coincide exactly, their generated name + * landing identically under the corpus's pinned seed. + * + * Each golden was written by a builder that refuses unless the decoded output **runs** and + * reproduces the pre-obfuscation source's own output, and unless the source itself reproduces it - + * so a golden cannot be certified against a broken expectation. `.src.js` is kept beside each + * pair so the decoded-to-source size ratio stays computable; nothing in the suite reads it. + */ + +// The pipeline's own order. The fixpoint is required rather than tidy: storage inlining re-opens +// Converting work that has already reported clean, so one sweep is not enough. +const group = (ast) => { + let previous = null + for (let round = 0; round < 8; round++) { + normalizeConverting(ast) + traverse(ast, calculateConstantExp) + traverse(ast, parseControlFlowStorage) + traverse(ast, calculateConstantExp) + traverse(ast, pruneIfBranch) + traverse( + ast, + createUnflattenSwitchDispatch(() => {}), + ) + const cur = generate(ast, { compact: true }).code + if (cur === previous) break + previous = cur + } +} + +const passes = [normalizeStatements, decodeStringArray, group, unlockEnv] + +test('2.9.6 — wrapper var-fn-expression, rotate counter-loop', () => { + getPipelineResult(passes, true, join(root, '2.9.6-baseline-strings')) +}) + +test('2.11.1 — wrapper var-fn-expression, rotate compare-loop', () => { + getPipelineResult(passes, true, join(root, '2.11.1-baseline-strings')) +}) + +test('2.15.3 — wrapper fn-declaration', () => { + getPipelineResult(passes, true, join(root, '2.15.3-baseline-strings')) +}) + +test('2.15.5 — wrapper self-replacing', () => { + getPipelineResult(passes, true, join(root, '2.15.5-baseline-strings')) +}) diff --git a/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.fix.js b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.fix.js new file mode 100644 index 00000000..df08ea81 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.fix.js @@ -0,0 +1,9 @@ +function greet(_0xbc4c9f) { + return "hello, " + _0xbc4c9f + '!'; +} +var parts = ["alpha", "beta", "gamma"]; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log("console-channel"); +process.stdout.write(greet("world") + '\x0a'); +process.stdout.write(joined + '\x20' + upper + '\x20' + parts.length + '\x20' + "literal".charAt(0) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.js b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.js new file mode 100644 index 00000000..d49d7d16 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.js @@ -0,0 +1 @@ +var _0x110e=['5vcxhmU','986762nGLrJs','184631IGlRQz','beta','alpha','1016406IGxcox','world','write','length','2NFgDcv','1424513VyMIwZ','365527udeBxY','hello,\x20','console-channel','toUpperCase','log','gamma','join','literal','stdout','91785KUOSau','charAt','117387GnnLDk','2rdIgxy'];var _0x1b9e=function(_0x3f6db4,_0x18ba38){_0x3f6db4=_0x3f6db4-0xeb;var _0x110e7a=_0x110e[_0x3f6db4];return _0x110e7a;};var _0x18bebe=_0x1b9e;(function(_0xa285,_0x598eed){var _0x727449=_0x1b9e;while(!![]){try{var _0x2a9096=parseInt(_0x727449(0xf4))*parseInt(_0x727449(0xf6))+parseInt(_0x727449(0xf9))+-parseInt(_0x727449(0xf5))+parseInt(_0x727449(0xf3))*parseInt(_0x727449(0xf2))+parseInt(_0x727449(0xfd))*parseInt(_0x727449(0xff))+parseInt(_0x727449(0xf0))+-parseInt(_0x727449(0xfe));if(_0x2a9096===_0x598eed)break;else _0xa285['push'](_0xa285['shift']());}catch(_0xb850e7){_0xa285['push'](_0xa285['shift']());}}}(_0x110e,0x8f0ab));function greet(_0xbc4c9f){var _0x3b861f=_0x1b9e;return _0x3b861f(0x100)+_0xbc4c9f+'!';}var parts=[_0x18bebe(0xf8),_0x18bebe(0xf7),_0x18bebe(0xec)],joined=parts[_0x18bebe(0xed)]('-'),upper=joined[_0x18bebe(0x102)]();console[_0x18bebe(0xeb)](_0x18bebe(0x101)),process[_0x18bebe(0xef)][_0x18bebe(0xfb)](greet(_0x18bebe(0xfa))+'\x0a'),process[_0x18bebe(0xef)][_0x18bebe(0xfb)](joined+'\x20'+upper+'\x20'+parts[_0x18bebe(0xfc)]+'\x20'+_0x18bebe(0xee)[_0x18bebe(0xf1)](0x0)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.src.js b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.src.js new file mode 100644 index 00000000..379cb4a7 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.11.1-baseline-strings.src.js @@ -0,0 +1,10 @@ +// strings — string literals, concatenation, member reads. The string array's own input. +// Reports through process.stdout.write, which disableConsoleOutput does not intercept. +// The single console.log is shape material, and its suppression is itself a signal. +function greet(who) { return 'hello, ' + who + '!'; } +var parts = ['alpha', 'beta', 'gamma']; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log('console-channel'); +process.stdout.write(greet('world') + '\n'); +process.stdout.write(joined + ' ' + upper + ' ' + parts.length + ' ' + 'literal'.charAt(0) + '\n'); diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.fix.js b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.fix.js new file mode 100644 index 00000000..95c0e5d3 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.fix.js @@ -0,0 +1,9 @@ +function greet(_0x42ba89) { + return "hello, " + _0x42ba89 + '!'; +} +var parts = ["alpha", "beta", "gamma"]; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log("console-channel"); +process.stdout.write(greet("world") + '\x0a'); +process.stdout.write(joined + '\x20' + upper + '\x20' + parts.length + '\x20' + "literal".charAt(0) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.js b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.js new file mode 100644 index 00000000..832664e3 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.js @@ -0,0 +1 @@ +var _0x2841=['137GnnLDk','71KUOSau','length','23adxgum','world','console-channel','write','log','join','toUpperCase','gamma','hello,\x20','762701IGxcox','47513VyMIwZ','657888rdIgxy','beta','6885NFgDcv','charAt','3049udeBxY','406948nGLrJs','stdout','literal','alpha','2346310DCWsik'];var _0x5a3e09=_0x111c;(function(_0x912b57,_0x2ada00){var _0x5c63a4=_0x111c;while(!![]){try{var _0x59b035=parseInt(_0x5c63a4(0x100))+parseInt(_0x5c63a4(0xef))+-parseInt(_0x5c63a4(0x102))+parseInt(_0x5c63a4(0xf4))*-parseInt(_0x5c63a4(0xec))+-parseInt(_0x5c63a4(0xee))*parseInt(_0x5c63a4(0xf5))+parseInt(_0x5c63a4(0x101))*-parseInt(_0x5c63a4(0xf7))+parseInt(_0x5c63a4(0xf3));if(_0x59b035===_0x2ada00)break;else _0x912b57['push'](_0x912b57['shift']());}catch(_0x1c5bc0){_0x912b57['push'](_0x912b57['shift']());}}}(_0x2841,0x93d6c));function greet(_0x42ba89){var _0x3f9256=_0x111c;return _0x3f9256(0xff)+_0x42ba89+'!';}function _0x111c(_0x3ea0d1,_0x3f2b90){_0x3ea0d1=_0x3ea0d1-0xeb;var _0x2841fc=_0x2841[_0x3ea0d1];return _0x2841fc;}var parts=[_0x5a3e09(0xf2),_0x5a3e09(0xeb),_0x5a3e09(0xfe)],joined=parts[_0x5a3e09(0xfc)]('-'),upper=joined[_0x5a3e09(0xfd)]();console[_0x5a3e09(0xfb)](_0x5a3e09(0xf9)),process[_0x5a3e09(0xf0)][_0x5a3e09(0xfa)](greet(_0x5a3e09(0xf8))+'\x0a'),process[_0x5a3e09(0xf0)][_0x5a3e09(0xfa)](joined+'\x20'+upper+'\x20'+parts[_0x5a3e09(0xf6)]+'\x20'+_0x5a3e09(0xf1)[_0x5a3e09(0xed)](0x0)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.src.js b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.src.js new file mode 100644 index 00000000..379cb4a7 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.3-baseline-strings.src.js @@ -0,0 +1,10 @@ +// strings — string literals, concatenation, member reads. The string array's own input. +// Reports through process.stdout.write, which disableConsoleOutput does not intercept. +// The single console.log is shape material, and its suppression is itself a signal. +function greet(who) { return 'hello, ' + who + '!'; } +var parts = ['alpha', 'beta', 'gamma']; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log('console-channel'); +process.stdout.write(greet('world') + '\n'); +process.stdout.write(joined + ' ' + upper + ' ' + parts.length + ' ' + 'literal'.charAt(0) + '\n'); diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.fix.js b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.fix.js new file mode 100644 index 00000000..f27e2a7e --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.fix.js @@ -0,0 +1,9 @@ +function greet(_0x272a83) { + return "hello, " + _0x272a83 + '!'; +} +var parts = ["alpha", "beta", "gamma"]; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log("console-channel"); +process.stdout.write(greet("world") + '\x0a'); +process.stdout.write(joined + '\x20' + upper + '\x20' + parts.length + '\x20' + "literal".charAt(0) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.js b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.js new file mode 100644 index 00000000..007463e7 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.js @@ -0,0 +1 @@ +var _0x2841=['beta','charAt','135300mDCWsi','26884xnGLrJ','2559YKUOSa','51991srdIgx','world','console-channel','147401kNFgDc','alpha','73482Zadxgu','4yGnnLD','log','89vudeBx','write','literal','length','stdout','89670uVyMIw','gamma','hello,\x20','join','toUpperCase'];var _0x381e6b=_0x111c;(function(_0x136a38,_0x912b57){var _0x1fdc17=_0x111c;while(!![]){try{var _0x2ada00=-parseInt(_0x1fdc17(0xfb))+parseInt(_0x1fdc17(0xfd))*-parseInt(_0x1fdc17(0xec))+-parseInt(_0x1fdc17(0x100))+parseInt(_0x1fdc17(0xee))*parseInt(_0x1fdc17(0xfc))+parseInt(_0x1fdc17(0xf3))+parseInt(_0x1fdc17(0xeb))+parseInt(_0x1fdc17(0xfa));if(_0x2ada00===_0x912b57)break;else _0x136a38['push'](_0x136a38['shift']());}catch(_0x59b035){_0x136a38['push'](_0x136a38['shift']());}}}(_0x2841,0x23252));function greet(_0x272a83){var _0xc0e3a3=_0x111c;return _0xc0e3a3(0xf5)+_0x272a83+'!';}var parts=[_0x381e6b(0x101),_0x381e6b(0xf8),_0x381e6b(0xf4)],joined=parts[_0x381e6b(0xf6)]('-'),upper=joined[_0x381e6b(0xf7)]();function _0x111c(_0x3ea0d1,_0x3f2b90){return _0x111c=function(_0x2841fc,_0x111c9d){_0x2841fc=_0x2841fc-0xeb;var _0x42ba89=_0x2841[_0x2841fc];return _0x42ba89;},_0x111c(_0x3ea0d1,_0x3f2b90);}console[_0x381e6b(0xed)](_0x381e6b(0xff)),process[_0x381e6b(0xf2)][_0x381e6b(0xef)](greet(_0x381e6b(0xfe))+'\x0a'),process[_0x381e6b(0xf2)][_0x381e6b(0xef)](joined+'\x20'+upper+'\x20'+parts[_0x381e6b(0xf1)]+'\x20'+_0x381e6b(0xf0)[_0x381e6b(0xf9)](0x0)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.src.js b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.src.js new file mode 100644 index 00000000..379cb4a7 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.15.5-baseline-strings.src.js @@ -0,0 +1,10 @@ +// strings — string literals, concatenation, member reads. The string array's own input. +// Reports through process.stdout.write, which disableConsoleOutput does not intercept. +// The single console.log is shape material, and its suppression is itself a signal. +function greet(who) { return 'hello, ' + who + '!'; } +var parts = ['alpha', 'beta', 'gamma']; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log('console-channel'); +process.stdout.write(greet('world') + '\n'); +process.stdout.write(joined + ' ' + upper + ' ' + parts.length + ' ' + 'literal'.charAt(0) + '\n'); diff --git a/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.fix.js b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.fix.js new file mode 100644 index 00000000..df08ea81 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.fix.js @@ -0,0 +1,9 @@ +function greet(_0xbc4c9f) { + return "hello, " + _0xbc4c9f + '!'; +} +var parts = ["alpha", "beta", "gamma"]; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log("console-channel"); +process.stdout.write(greet("world") + '\x0a'); +process.stdout.write(joined + '\x20' + upper + '\x20' + parts.length + '\x20' + "literal".charAt(0) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.js b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.js new file mode 100644 index 00000000..5444fd53 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.js @@ -0,0 +1 @@ +var _0x110e=['literal','length','charAt','alpha','console-channel','join','world','write','log','beta','toUpperCase','gamma','hello,\x20','stdout'];(function(_0x3f6db4,_0x18ba38){var _0x110e7a=function(_0x1b9e32){while(--_0x1b9e32){_0x3f6db4['push'](_0x3f6db4['shift']());}};_0x110e7a(++_0x18ba38);}(_0x110e,0x1bf));var _0x1b9e=function(_0x3f6db4,_0x18ba38){_0x3f6db4=_0x3f6db4-0xeb;var _0x110e7a=_0x110e[_0x3f6db4];return _0x110e7a;};var _0x47b8f9=_0x1b9e;function greet(_0xbc4c9f){var _0x2d3710=_0x1b9e;return _0x2d3710(0xf8)+_0xbc4c9f+'!';}var parts=[_0x47b8f9(0xef),_0x47b8f9(0xf5),_0x47b8f9(0xf7)],joined=parts[_0x47b8f9(0xf1)]('-'),upper=joined[_0x47b8f9(0xf6)]();console[_0x47b8f9(0xf4)](_0x47b8f9(0xf0)),process[_0x47b8f9(0xeb)][_0x47b8f9(0xf3)](greet(_0x47b8f9(0xf2))+'\x0a'),process[_0x47b8f9(0xeb)][_0x47b8f9(0xf3)](joined+'\x20'+upper+'\x20'+parts[_0x47b8f9(0xed)]+'\x20'+_0x47b8f9(0xec)[_0x47b8f9(0xee)](0x0)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.src.js b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.src.js new file mode 100644 index 00000000..379cb4a7 --- /dev/null +++ b/test/visitor/obfuscator/era-below-2-16/2.9.6-baseline-strings.src.js @@ -0,0 +1,10 @@ +// strings — string literals, concatenation, member reads. The string array's own input. +// Reports through process.stdout.write, which disableConsoleOutput does not intercept. +// The single console.log is shape material, and its suppression is itself a signal. +function greet(who) { return 'hello, ' + who + '!'; } +var parts = ['alpha', 'beta', 'gamma']; +var joined = parts.join('-'); +var upper = joined.toUpperCase(); +console.log('console-channel'); +process.stdout.write(greet('world') + '\n'); +process.stdout.write(joined + ' ' + upper + ' ' + parts.length + ' ' + 'literal'.charAt(0) + '\n'); diff --git a/test/visitor/obfuscator/normalize-converting.test.js b/test/visitor/obfuscator/normalize-converting.test.js new file mode 100644 index 00000000..8caf75f4 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting.test.js @@ -0,0 +1,155 @@ +import fs from 'fs' +import { join } from 'path' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import * as t from '@babel/types' +import traverse from '@babel/traverse' +import normalizeStatements from '#visitor/obfuscator/normalize-statements' +import decodeStringArray from '#visitor/obfuscator/string-array' +import normalizeConverting from '#visitor/obfuscator/normalize-converting' +import uncomputePropertyKey from '#visitor/atomic/uncompute-property-key' +import collapsePropertyShorthand from '#visitor/atomic/collapse-property-shorthand' + +const root = join(__dirname, 'normalize-converting') + +/** + * Each case is real javascript-obfuscator 2.19.0 output for a source shape taken from upstream's + * own converting-transformer fixtures, given a reporting tail so the golden could be checked by + * running it. The goldens were generated by a builder that refuses to write one unless the decoded + * output reproduces the pre-obfuscation source's output exactly. + * + * The string-array pass runs first because it has to: until concealed strings are substituted, a + * property key is a *call* rather than a string and nothing in this pass can match it. + */ +function run(name) { + const input = fs.readFileSync(join(root, `${name}.js`), 'utf-8') + const ast = parse(input, { + allowReturnOutsideFunction: true, + errorRecovery: true, + }) + normalizeStatements(ast) + decodeStringArray(ast) + normalizeConverting(ast) + const expected = fs.readFileSync(join(root, `${name}.fix.js`), 'utf-8') + expect(generate(ast).code).toBe(expected) + return generate(ast).code +} + +/** + * Class member keys, from upstream's `class-field-transformer` fixtures, which cover both key + * spellings and `constructor` — the one name the encoder itself exempts from literalization. + */ +test('class-member-keys', () => { + const out = run('class-member-keys') + // The point of the case: every key comes back as an identifier, including the quoted one, + // and `constructor` is still the constructor rather than a method named "constructor". + expect(out).toMatch(/\bconstructor\(\)/) + expect(out).toMatch(/\bquoted\(\)/) + expect(out).not.toMatch(/\["/) +}) + +/** Object-keys extraction with getter/setter members, from upstream's `get-set-property-kind`. */ +test('object-keys-getset', () => { + const out = run('object-keys-getset') + expect(out).not.toMatch(/\["/) +}) + +/** + * Member reads, booleans, hex numbers, numerical expressions and split strings together. + * + * **Asserted on the tree, not with a regex over the text.** A first version of this test used + * `/0x[0-9a-f]/i` for the hex axis and failed on correct output, because renaming leaves + * identifiers like `_0x185301` everywhere and they match it. Any text-level residue check on this + * decoder's output has that problem; the shape is what the census reads and it is what to assert. + */ +test('literals-and-members', () => { + const out = run('literals-and-members') + const ast = parse(out) + let hexRaw = 0 + let boolDisguise = 0 + let computedStringMember = 0 + traverse(ast, { + NumericLiteral({ node }) { + if (node.extra && /^0[xX]/.test(String(node.extra.raw))) hexRaw++ + }, + UnaryExpression(path) { + const isEmptyArr = (n) => + t.isArrayExpression(n) && n.elements.length === 0 + if (path.node.operator === '!' && isEmptyArr(path.node.argument)) + boolDisguise++ + }, + MemberExpression({ node }) { + if (node.computed && t.isStringLiteral(node.property)) + computedStringMember++ + }, + }) + expect({ hexRaw, boolDisguise, computedStringMember }).toEqual({ + hexRaw: 0, + boolDisguise: 0, + computedStringMember: 0, + }) + // and the values themselves survived, so the zeros above are a decode rather than a deletion + expect(out).toMatch(/= true/) + expect(out).toMatch(/= false/) + expect(out).toMatch(/1000/) + expect(out).toMatch(/"abcdefgh"/) +}) + +/** + * Destructuring, from upstream's `object-pattern-properties-transformer` fixtures. + * + * **This case does not exercise the shorthand collapse, and that is the finding it pins.** The + * encoder expands `{ foo }` to `{ foo: foo }` so that renaming can rewrite the binding while + * leaving the property name alone — and renaming then does exactly that, at every setting of + * `renameGlobals`. So the two names differ permanently and the collapse can never fire on this + * encoder's output. What the case asserts is that the pass leaves a renamed pattern alone rather + * than corrupting it. + */ +test('object-pattern-shorthand', () => { + const out = run('object-pattern-shorthand') + expect(out).toMatch(/\{[^{}]*foo:\s*_0x/) +}) + +/** + * The three keys that change meaning when they lose their brackets. None is reachable from stock + * encoder output — the encoder exempts `constructor` and never emits the other two — so this is + * the only place they are covered, and they are covered because a *source* can already contain + * them. + */ +test('dangerous keys are never un-computed', () => { + const code = [ + 'const a = { ["__proto__"]: base };', + 'class C { ["constructor"]() { return 1; } }', + 'class D { static ["prototype"]() { return 2; } }', + ].join('\n') + const ast = parse(code) + traverse(ast, uncomputePropertyKey) + const out = generate(ast).code + expect(out).toMatch(/\["__proto__"\]/) + expect(out).toMatch(/\["constructor"\]/) + expect(out).toMatch(/\["prototype"\]/) +}) + +/** A safe key in the same positions must still be un-computed, or the test above proves nothing. */ +test('ordinary keys are un-computed', () => { + const ast = parse('const a = { ["foo"]: 1 };\nclass C { ["bar"]() {} }') + traverse(ast, uncomputePropertyKey) + const out = generate(ast).code + expect(out).toMatch(/foo: 1/) + expect(out).toMatch(/bar\(\)/) + expect(out).not.toMatch(/\["/) +}) + +/** + * The mirror-image `__proto__` hazard: collapsing to shorthand is what changes meaning here, + * because `{ __proto__: x }` sets the prototype while `{ __proto__ }` defines an own property. + */ +test('__proto__ is never collapsed to shorthand', () => { + const ast = parse('const o = { __proto__: __proto__, foo: foo };') + traverse(ast, collapsePropertyShorthand) + const out = generate(ast).code + expect(out).toMatch(/__proto__: __proto__/) + // and the safe sibling in the same literal is collapsed, so the guard is not vacuous + expect(out).toMatch(/\bfoo\b(?!\s*:)/) +}) diff --git a/test/visitor/obfuscator/normalize-converting/class-member-keys.fix.js b/test/visitor/obfuscator/normalize-converting/class-member-keys.fix.js new file mode 100644 index 00000000..d44eff46 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/class-member-keys.fix.js @@ -0,0 +1,15 @@ +class _0x4f1c97 { + constructor() { + this.made = "ctor"; + } + bar() { + return "bar"; + } + quoted() { + return "quoted"; + } + static make() { + return new _0x4f1c97(); + } +} +process.stdout.write(_0x4f1c97.make().made + '\x20' + new _0x4f1c97().bar() + '\x20' + new _0x4f1c97().quoted() + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/class-member-keys.js b/test/visitor/obfuscator/normalize-converting/class-member-keys.js new file mode 100644 index 00000000..bfd1f2e9 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/class-member-keys.js @@ -0,0 +1 @@ +var _0x120466=_0x1a1f;(function(_0x5bf326,_0x251577){var _0x5e9d09=_0x1a1f,_0x4a34cc=_0x5bf326();while(!![]){try{var _0x7bf0f=parseInt(_0x5e9d09(0x89))/(0x23a4+-0x1*0x2302+-0x7*0x17)*(parseInt(_0x5e9d09(0x83))/(-0xbd7*0x1+0x1e39+-0x1260))+-parseInt(_0x5e9d09(0x8b))/(-0x9a9*-0x2+-0x1f*-0x9f+-0x2690)+-parseInt(_0x5e9d09(0x80))/(-0x15b4+-0xa08+-0x1fc0*-0x1)*(parseInt(_0x5e9d09(0x8c))/(-0x65a+-0x293*-0x1+-0x3*-0x144))+parseInt(_0x5e9d09(0x8a))/(0x7d6+-0x1bdb+0x140b)*(parseInt(_0x5e9d09(0x90))/(-0x106b*0x1+0x2388+-0x1316))+parseInt(_0x5e9d09(0x81))/(0x1340+-0xb7*0x1d+0x1*0x183)*(-parseInt(_0x5e9d09(0x8e))/(0x93*-0x13+0xc*0x18d+-0x7aa))+parseInt(_0x5e9d09(0x91))/(-0x1*-0x2cd+0x43*-0x2+-0xbf*0x3)+parseInt(_0x5e9d09(0x82))/(0x24b2+0x1*0x531+-0xa76*0x4)*(-parseInt(_0x5e9d09(0x8d))/(-0x1f2f+-0x11c*-0xf+0xe97));if(_0x7bf0f===_0x251577)break;else _0x4a34cc['push'](_0x4a34cc['shift']());}catch(_0x102350){_0x4a34cc['push'](_0x4a34cc['shift']());}}}(_0x3e45,-0x871aa+-0x3*-0x2f6ce+0x7de5b));function _0x3e45(){var _0x22d6d6=['934764oOWEFB','158715YHUZqj','24SNeLlC','4182174IEkcoX','quot','1379441TzdSFg','5780710BxlXVY','ctor','4mVPxDB','8AQXAAG','668459PzABFM','14vZyNrI','make','writ','bar','made','stdo','15469pTFsGe','24xswivE'];_0x3e45=function(){return _0x22d6d6;};return _0x3e45();}function _0x1a1f(_0x57a012,_0x2e74f8){var _0x5880a8=_0x3e45();return _0x1a1f=function(_0x562713,_0x4f1c97){_0x562713=_0x562713-(-0xd*-0xe3+0x32b+-0xe32);var _0x2f2a04=_0x5880a8[_0x562713];return _0x2f2a04;},_0x1a1f(_0x57a012,_0x2e74f8);}class _0x4f1c97{constructor(){var _0x468035=_0x1a1f;this[_0x468035(0x87)]=_0x468035(0x92);}[_0x120466(0x86)](){var _0x59ccff=_0x120466;return _0x59ccff(0x86);}[_0x120466(0x8f)+'ed'](){var _0x144eb0=_0x120466;return _0x144eb0(0x8f)+'ed';}static[_0x120466(0x84)](){return new _0x4f1c97();}}process[_0x120466(0x88)+'ut'][_0x120466(0x85)+'e'](_0x4f1c97[_0x120466(0x84)]()[_0x120466(0x87)]+'\x20'+new _0x4f1c97()[_0x120466(0x86)]()+'\x20'+new _0x4f1c97()[_0x120466(0x8f)+'ed']()+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/class-member-keys.src.js b/test/visitor/obfuscator/normalize-converting/class-member-keys.src.js new file mode 100644 index 00000000..339d2d20 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/class-member-keys.src.js @@ -0,0 +1,7 @@ +class Foo { + constructor () { this.made = 'ctor'; } + bar () { return 'bar'; } + 'quoted' () { return 'quoted'; } + static make () { return new Foo(); } +} +process.stdout.write(Foo.make().made + ' ' + new Foo().bar() + ' ' + new Foo().quoted() + '\n'); diff --git a/test/visitor/obfuscator/normalize-converting/literals-and-members.fix.js b/test/visitor/obfuscator/normalize-converting/literals-and-members.fix.js new file mode 100644 index 00000000..f98adc12 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/literals-and-members.fix.js @@ -0,0 +1,11 @@ +(function () { + var _0x185301 = true; + var _0x4dfddb = false; + var _0x255a46 = 1000; + var _0x2fa2fd = "abcdefgh"; + var _0x52e814 = { + name: "widget", + size: 3 + }; + process.stdout.write(_0x52e814.name + '\x20' + _0x52e814.size + '\x20' + _0x185301 + '\x20' + _0x4dfddb + '\x20' + _0x255a46 + '\x20' + _0x2fa2fd + '\x0a'); +})(); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/literals-and-members.js b/test/visitor/obfuscator/normalize-converting/literals-and-members.js new file mode 100644 index 00000000..d88c85a8 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/literals-and-members.js @@ -0,0 +1 @@ +(function(_0x22451a,_0x55a41a){var _0x201a14=_0x4231,_0x400f84=_0x22451a();while(!![]){try{var _0x5f2456=parseInt(_0x201a14(0xc0))/(0x2ff*0x8+0xb8d+-0x2384)*(-parseInt(_0x201a14(0xbd))/(-0x15bc+-0x1bc+0x177a))+-parseInt(_0x201a14(0xc3))/(-0x5*0x36b+-0x1b5+0x12cf)*(parseInt(_0x201a14(0xc4))/(0x44b*-0x4+0x46c+-0x13*-0xac))+-parseInt(_0x201a14(0xc7))/(-0x11*0xc7+0x2494+-0xc*0x1f2)+parseInt(_0x201a14(0xb8))/(0xfe3+0x124a+-0x4e1*0x7)+parseInt(_0x201a14(0xbe))/(0x1*0x8f5+0xb8c+-0x147a)*(parseInt(_0x201a14(0xbf))/(0x179f+-0xa3*0xa+-0x1*0x1139))+parseInt(_0x201a14(0xc6))/(-0x1317+-0x11*0x1aa+0x2f6a)+-parseInt(_0x201a14(0xba))/(-0x1353+0x248*-0xf+0x3595)*(-parseInt(_0x201a14(0xb9))/(0x2*-0xad3+-0x1d39+0x32ea));if(_0x5f2456===_0x55a41a)break;else _0x400f84['push'](_0x400f84['shift']());}catch(_0x4c9f94){_0x400f84['push'](_0x400f84['shift']());}}}(_0x7e8c,-0x2e0b7+-0x9f95*0x2+0x9505*0xb),function(){var _0x58bd0e=_0x4231,_0x185301=!![],_0x4dfddb=![],_0x255a46=-0x1684+-0x29*-0x5e+-0xb5e*-0x1,_0x2fa2fd=_0x58bd0e(0xbc)+_0x58bd0e(0xbb),_0x2d0852={};_0x2d0852[_0x58bd0e(0xb7)]=_0x58bd0e(0xc1)+'et',_0x2d0852[_0x58bd0e(0xc2)]=0x3;var _0x52e814=_0x2d0852;process[_0x58bd0e(0xc8)+'ut'][_0x58bd0e(0xc5)+'e'](_0x52e814[_0x58bd0e(0xb7)]+'\x20'+_0x52e814[_0x58bd0e(0xc2)]+'\x20'+_0x185301+'\x20'+_0x4dfddb+'\x20'+_0x255a46+'\x20'+_0x2fa2fd+'\x0a');}());function _0x4231(_0x77e3cc,_0x46e413){var _0xce24a3=_0x7e8c();return _0x4231=function(_0x1b842b,_0x143560){_0x1b842b=_0x1b842b-(-0x1458+0x11d7*-0x1+0x26e6);var _0x420fe8=_0xce24a3[_0x1b842b];return _0x420fe8;},_0x4231(_0x77e3cc,_0x46e413);}function _0x7e8c(){var _0x1b0c96=['abcd','362698XACoJa','319984SQUbZm','16cTxTCR','1lTYVtn','widg','size','39SfNOGe','63604MdcuqA','writ','819018oddLfK','661535yrQZiD','stdo','name','1607064nMfcFQ','2413763cmPbDz','10gVlKLs','efgh'];_0x7e8c=function(){return _0x1b0c96;};return _0x7e8c();} \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/literals-and-members.src.js b/test/visitor/obfuscator/normalize-converting/literals-and-members.src.js new file mode 100644 index 00000000..0944a001 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/literals-and-members.src.js @@ -0,0 +1,5 @@ +(function () { + var flag = true, off = false, n = 1000, s = 'abcdefgh'; + var o = { name: 'widget', size: 3 }; + process.stdout.write(o.name + ' ' + o['size'] + ' ' + flag + ' ' + off + ' ' + n + ' ' + s + '\n'); +})(); diff --git a/test/visitor/obfuscator/normalize-converting/object-keys-getset.fix.js b/test/visitor/obfuscator/normalize-converting/object-keys-getset.fix.js new file mode 100644 index 00000000..a837be50 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-keys-getset.fix.js @@ -0,0 +1,13 @@ +(function () { + const _0x2d2e4c = { + get baz() { + return 2; + }, + set bark(_0x54cf52) { + this.stored = _0x54cf52; + }, + bar: 1 + }; + _0x2d2e4c.bark = 9; + process.stdout.write(_0x2d2e4c.bar + '\x20' + _0x2d2e4c.baz + '\x20' + _0x2d2e4c.stored + '\x0a'); +})(); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/object-keys-getset.js b/test/visitor/obfuscator/normalize-converting/object-keys-getset.js new file mode 100644 index 00000000..81d1a6f8 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-keys-getset.js @@ -0,0 +1 @@ +function _0x556c(){const _0x142704=['11412kILLWr','30RSPayH','128QLtxkG','494035nusrAE','14lnzbHJ','stor','281521wOrRBx','bar','writ','1846suXDjb','492CUTrzh','367266qAiGJD','bark','stdo','1643164DrhvdE','baz','2277011HOoGQp'];_0x556c=function(){return _0x142704;};return _0x556c();}function _0x3d13(_0x1a5845,_0x5259d0){const _0x154d32=_0x556c();return _0x3d13=function(_0x330be7,_0x2d15a6){_0x330be7=_0x330be7-(0x11*-0xe9+-0xc25+0x1ceb);let _0x118c78=_0x154d32[_0x330be7];return _0x118c78;},_0x3d13(_0x1a5845,_0x5259d0);}(function(_0x334631,_0x222db7){const _0x41c595=_0x3d13,_0x1f7e45=_0x334631();while(!![]){try{const _0x14f7be=parseInt(_0x41c595(0x157))/(-0x1*-0x7aa+-0x3d4+0x1*-0x3d5)+parseInt(_0x41c595(0x15a))/(-0x2f*-0x63+-0x14af+0x284)*(-parseInt(_0x41c595(0x15b))/(0x2*-0x83f+-0xc7e+0x1cff))+-parseInt(_0x41c595(0x14e))/(-0x9cc+0x2065+-0x1695)+parseInt(_0x41c595(0x154))/(-0x48b*0x2+-0x4*0x7d7+0x2877)+-parseInt(_0x41c595(0x15c))/(-0x26e2+-0x18e4+0x3fcc*0x1)*(parseInt(_0x41c595(0x155))/(0x210*0x5+0xa*0x25f+-0x21ff))+parseInt(_0x41c595(0x153))/(-0x2549+0x11*-0x21+0x2782)*(-parseInt(_0x41c595(0x151))/(-0x1426+-0x2698+0x3ac7))+-parseInt(_0x41c595(0x152))/(0x1709+-0x290+0x146f*-0x1)*(-parseInt(_0x41c595(0x150))/(0x158a+0x2*-0xf90+0x9a1));if(_0x14f7be===_0x222db7)break;else _0x1f7e45['push'](_0x1f7e45['shift']());}catch(_0x3e5b2c){_0x1f7e45['push'](_0x1f7e45['shift']());}}}(_0x556c,-0x71bae+0x4d2a*-0x1e+0x14aca4),function(){const _0x29a4eb=_0x3d13,_0xad991d={get 'baz'(){return 0x2*-0x111+-0xda*0x1d+0x1ad6;},set 'bark'(_0x54cf52){const _0x1cce74=_0x3d13;this[_0x1cce74(0x156)+'ed']=_0x54cf52;}};_0xad991d[_0x29a4eb(0x158)]=0x1;const _0x2d2e4c=_0xad991d;_0x2d2e4c[_0x29a4eb(0x15d)]=-0x13f9*-0x1+0x19d*0xe+-0x2*0x1543,process[_0x29a4eb(0x14d)+'ut'][_0x29a4eb(0x159)+'e'](_0x2d2e4c[_0x29a4eb(0x158)]+'\x20'+_0x2d2e4c[_0x29a4eb(0x14f)]+'\x20'+_0x2d2e4c[_0x29a4eb(0x156)+'ed']+'\x0a');}()); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/object-keys-getset.src.js b/test/visitor/obfuscator/normalize-converting/object-keys-getset.src.js new file mode 100644 index 00000000..03d0d0c6 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-keys-getset.src.js @@ -0,0 +1,9 @@ +(function () { + const foo = { + bar: 1, + get baz() { return 2; }, + set bark(value) { this.stored = value; } + }; + foo.bark = 9; + process.stdout.write(foo.bar + ' ' + foo.baz + ' ' + foo.stored + '\n'); +})(); diff --git a/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.fix.js b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.fix.js new file mode 100644 index 00000000..d76b9ad8 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.fix.js @@ -0,0 +1,13 @@ +(function () { + const _0x3e3d36 = { + foo: 1, + bar: 2, + other: 3 + }; + const { + foo: _0x31be2d, + bar: _0x4c9319, + ..._0x1c6b8b + } = _0x3e3d36; + process.stdout.write(_0x31be2d + '\x20' + _0x4c9319 + '\x20' + JSON.stringify(_0x1c6b8b) + '\x0a'); +})(); \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.js b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.js new file mode 100644 index 00000000..eba9f8b1 --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.js @@ -0,0 +1 @@ +(function(_0x19b9e4,_0x3abb50){const _0x4fe0a8=_0x35e0,_0x5e72d0=_0x19b9e4();while(!![]){try{const _0x54f466=-parseInt(_0x4fe0a8(0x1d9))/(-0x175e+0xc46+0xb19)+parseInt(_0x4fe0a8(0x1ce))/(-0x2f1*-0x2+-0x3f2*-0x5+-0x1*0x199a)+parseInt(_0x4fe0a8(0x1d4))/(0x6*0x34c+-0x1*0x193+-0x1232)*(parseInt(_0x4fe0a8(0x1cc))/(-0x1bf4*0x1+0x20e5+0x4ed*-0x1))+-parseInt(_0x4fe0a8(0x1d5))/(0x3*0x873+-0x1a2c+-0x4*-0x36)*(-parseInt(_0x4fe0a8(0x1d0))/(-0x267d+0x13*0x1+0x2670))+parseInt(_0x4fe0a8(0x1d8))/(0x1*0x1264+-0x281+-0x1c*0x91)+-parseInt(_0x4fe0a8(0x1d6))/(0x87f*0x1+-0x1697+0xe20)*(parseInt(_0x4fe0a8(0x1da))/(-0x375+0xf*0xf1+-0xaa1))+-parseInt(_0x4fe0a8(0x1ca))/(0x2455+-0x8+-0x2443)*(-parseInt(_0x4fe0a8(0x1cd))/(0x1*0x1a16+-0x18b2+-0x159));if(_0x54f466===_0x3abb50)break;else _0x5e72d0['push'](_0x5e72d0['shift']());}catch(_0x2ad616){_0x5e72d0['push'](_0x5e72d0['shift']());}}}(_0x458c,-0xafe79+-0x18b374+0x181aa5*0x2),function(){const _0x41d0a7=_0x35e0,_0x593ec2={};_0x593ec2[_0x41d0a7(0x1d3)]=0x1,_0x593ec2[_0x41d0a7(0x1d7)]=0x2,_0x593ec2[_0x41d0a7(0x1cb)+'r']=0x3;const _0x3e3d36=_0x593ec2,{foo:_0x31be2d,bar:_0x4c9319,..._0x1c6b8b}=_0x3e3d36;process[_0x41d0a7(0x1d1)+'ut'][_0x41d0a7(0x1d2)+'e'](_0x31be2d+'\x20'+_0x4c9319+'\x20'+JSON[_0x41d0a7(0x1cf)+_0x41d0a7(0x1c9)+'y'](_0x1c6b8b)+'\x0a');}());function _0x35e0(_0x1a6602,_0x4e33e7){const _0x2838b0=_0x458c();return _0x35e0=function(_0x3344f2,_0x593ec2){_0x3344f2=_0x3344f2-(0x1*0x18b+-0x2661+0x269f*0x1);let _0x3e3d36=_0x2838b0[_0x3344f2];return _0x3e3d36;},_0x35e0(_0x1a6602,_0x4e33e7);}function _0x458c(){const _0x1b1dc8=['stri','6219186McNVjJ','stdo','writ','foo','69jiftWh','5KJBHjm','8ZzfmlL','bar','2635563XHEgSb','1597286WaLrbG','11794581xRhWNj','ngif','11590rEIUKW','othe','111236XqYYZf','2266NiTFaW','2872910WsHHVp'];_0x458c=function(){return _0x1b1dc8;};return _0x458c();} \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.src.js b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.src.js new file mode 100644 index 00000000..86fc058a --- /dev/null +++ b/test/visitor/obfuscator/normalize-converting/object-pattern-shorthand.src.js @@ -0,0 +1,5 @@ +(function () { + const src = { foo: 1, bar: 2, other: 3 }; + const { foo, bar, ...rest } = src; + process.stdout.write(foo + ' ' + bar + ' ' + JSON.stringify(rest) + '\n'); +})(); diff --git a/test/visitor/obfuscator/normalize-statements.test.js b/test/visitor/obfuscator/normalize-statements.test.js new file mode 100644 index 00000000..1c30b431 --- /dev/null +++ b/test/visitor/obfuscator/normalize-statements.test.js @@ -0,0 +1,52 @@ +import fs from 'fs' +import { join } from 'path' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import { expectConsistentState } from '../../helper.js' +import normalizeStatements from '#visitor/obfuscator/normalize-statements' + +const root = join(__dirname, 'normalize-statements') + +// A private runner rather than `getVisitorResult`, for one reason only: that helper does +// `traverse(ast, visitor)` and `normalizeStatements` is a function taking the AST, not a visitor +// object. These cases also assert on the returned stats, which neither shared helper returns. +// +// What the private runner must NOT do is opt out of the state audit, which is exactly what it did +// until this import landed: it compared output text and nothing else, so the four visitors this +// pass composes shipped inflated reference counts straight through it. +function run(name) { + const input = fs.readFileSync(join(root, `${name}.js`), 'utf-8') + const ast = parse(input, { allowReturnOutsideFunction: true }) + const stats = normalizeStatements(ast) + const expected = fs.readFileSync(join(root, `${name}.fix.js`), 'utf-8') + expect(generate(ast).code).toBe(expected) + expectConsistentState(ast, expected, { allowReturnOutsideFunction: true }) + return stats +} + +/** + * Real javascript-obfuscator 2.19.0 output for four source shapes that all reduce to the same + * `if`/`else`, each landing the conditional in a different position: + * + * plain `c ? a() : b();` - already a statement + * inSeq `p(), c ? a() : b(), q();` - middle of a sequence + * inRet `return x(), c ? a() : b();` - last element of a sequence in a return + * outer `t && (x(), c ? a() : b());` - inside a sequence inside a `&&` + * + * `outer` is the one that forces the fixpoint: its sequence sits under a LogicalExpression, + * where split-sequence.js cannot reach it, so the conditional is unreachable until the `&&` + * has been reversed and the resulting branch re-braced. + */ +test('nested-encoder-output', () => { + const stats = run('nested-encoder-output') + + // Four conditionals and one `&&`, so nothing was left packed. + expect(stats.conditional).toBe(4) + expect(stats.logical).toBe(1) + + // The point of the test: one round is not enough, and the loop terminates on its own well + // inside the guard rather than by hitting it. + expect(stats.rounds).toBeGreaterThan(1) + expect(stats.cappedOut).toBe(false) +}) diff --git a/test/visitor/obfuscator/normalize-statements/nested-encoder-output.fix.js b/test/visitor/obfuscator/normalize-statements/nested-encoder-output.fix.js new file mode 100644 index 00000000..30ae3852 --- /dev/null +++ b/test/visitor/obfuscator/normalize-statements/nested-encoder-output.fix.js @@ -0,0 +1,36 @@ +function outer(_0x106c5d, _0x37cfdb) { + if (_0x106c5d) { + x(); + if (_0x37cfdb) { + a(); + } else { + b(); + } + } +} +function inSeq(_0x33d1df) { + p(); + if (_0x33d1df) { + a(); + } else { + b(); + } + q(); +} +function inRet(_0x2c2796, _0x449a6e) { + if (_0x2c2796) { + x(); + if (_0x449a6e) { + return a(); + } else { + return b(); + } + } +} +function plain(_0x597b2b) { + if (_0x597b2b) { + a(); + } else { + b(); + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/normalize-statements/nested-encoder-output.js b/test/visitor/obfuscator/normalize-statements/nested-encoder-output.js new file mode 100644 index 00000000..d6f837b4 --- /dev/null +++ b/test/visitor/obfuscator/normalize-statements/nested-encoder-output.js @@ -0,0 +1,12 @@ +function outer(_0x106c5d, _0x37cfdb) { + _0x106c5d && (x(), _0x37cfdb ? a() : b()); +} +function inSeq(_0x33d1df) { + p(), _0x33d1df ? a() : b(), q(); +} +function inRet(_0x2c2796, _0x449a6e) { + if (_0x2c2796) return x(), _0x449a6e ? a() : b(); +} +function plain(_0x597b2b) { + _0x597b2b ? a() : b(); +} \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array.test.js b/test/visitor/obfuscator/string-array.test.js new file mode 100644 index 00000000..7dc1a6cf --- /dev/null +++ b/test/visitor/obfuscator/string-array.test.js @@ -0,0 +1,274 @@ +import fs from 'fs' +import { join } from 'path' +import { describe, expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import normalizeStatements from '#visitor/obfuscator/normalize-statements' +import decodeStringArray from '#visitor/obfuscator/string-array' + +const root = join(__dirname, 'string-array') + +/** + * Every case is real javascript-obfuscator 2.19.0 output, and every `decoded` and `absent` case + * was built from a case the *encoder's own* test suite asserts — each one's comment names the + * variant it came from in `StringArrayTransformer.spec.ts` or + * `StringArrayRotateFunctionTransformer.spec.ts`. That matters because a fixture pins a claim, and + * cases invented to match what the pass happens to do pin the pass to itself. + * + * Three of the four outcomes are not failures, so only `decoded` has an obvious golden. The other + * three assert a status **and** that the tree came through byte-identical, which is what + * "resolve a matched structure completely or leave it entirely alone" means as a measurement. + */ +function run(name, options = {}) { + const input = fs.readFileSync(join(root, `${name}.js`), 'utf-8') + + // The untouched baseline is the tree after normalization, not the raw input: U1 runs first and + // does its job, so comparing against the raw bytes would report every non-`decoded` case as + // mutated and be measuring the wrong pass. + const only = parse(input, { allowReturnOutsideFunction: true }) + normalizeStatements(only) + const baseline = generate(only).code + + const ast = parse(input, { allowReturnOutsideFunction: true }) + normalizeStatements(ast) + const res = decodeStringArray(ast, options) + return { res, code: generate(ast).code, baseline } +} + +/** A `decoded` case: the golden is the whole assertion. */ +function expectDecoded(name, sites) { + const { res, code } = run(name) + expect(res.status).toBe('decoded') + expect(res.replaced).toBe(sites) + expect(code).toBe(fs.readFileSync(join(root, `${name}.fix.js`), 'utf-8')) + return res +} + +/** Any other outcome: the status, and the tree provably untouched. */ +function expectUntouched(name, status, options = {}) { + const { res, code, baseline } = run(name, options) + expect(res.status).toBe(status) + expect(res.replaced).toBe(0) + expect(code).toBe(baseline) + return res +} + +describe("decoded — claims taken from the encoder's own StringArrayTransformer.spec.ts", () => { + // Variant #1: default behaviour. + test('baseline', () => expectDecoded('baseline', 3)) + + // Variant #3.2. The index arrives as a *string* literal, `w('0x0')` rather than `w(0x0)`, and + // nothing in the corpus reaches this: `stringArrayIndexesType` appears only in the maximal + // profile, whose array is starved by its own `splitStringsChunkLength` and so has no live call + // site to spell either way. + test('index-numeric-string', () => expectDecoded('index-numeric-string', 3)) + + // Variant #3.3: both spellings in one sample, so a matcher that handles one type globally + // rather than per site still passes the two cases above and fails here. + test('index-mixed-types', () => expectDecoded('index-mixed-types', 5)) + + // Variant #4.1. `stringArrayIndexShift` rewrites the wrapper to subtract a constant from every + // index. Absorbed by evaluating the wrapper rather than modelled — which is the whole point of + // the strategy, and, like the index type above, had no corpus cell with live call sites. + test('index-shift', () => expectDecoded('index-shift', 5)) + + // Variant #4.5: shift, rotation and shuffling at once. The shift is computed against an array + // whose order the rotator has to restore first, so the three interact rather than stack. + test('index-shift-rotate-shuffle', () => + expectDecoded('index-shift-rotate-shuffle', 5)) + + // Variant #5: one array item, several call sites. Pins that the evaluation cache is keyed on + // the call rather than on the site. + test('same-literal-values', () => expectDecoded('same-literal-values', 4)) + + // Variant #8. The rc4 body hangs a memo cache off the wrapper object and carries an inline + // self-defending guard; neither may be read as a use site, and evaluating a call executes both. + test('encoding-rc4', () => expectDecoded('encoding-rc4', 3)) + + // Variant #11: two root wrappers, one per encoding, with **no** `none` wrapper — so there is no + // plain fallback and every site has to reach the wrapper it was actually compiled against. + test('encoding-base64-rc4', () => expectDecoded('encoding-base64-rc4', 10)) + + // Variant #13, and it is the name trap in the encoder's own words. Under mangled names the root + // wrapper is `b` and its parameters are `c` and `d`, which collide with a function declaration + // and with an inner `var b; function b(){}` pair. Resolution goes through bindings; a matcher + // holding the wrapper as a string decodes the wrong references here. + test('calls-wrapper-name', () => expectDecoded('calls-wrapper-name', 1)) + + // Variant #16.2. A computed object key becomes a call site in computed-member-key position — + // the positive direction of the guard that `guard-rotator-removed` exercises negatively. + test('object-computed-key', () => expectDecoded('object-computed-key', 5)) + + // StringArrayRotateFunctionTransformer, "prevent early successful comparison": enough array + // items that the rotator cannot succeed on its first trial rotation, so the checksum search + // genuinely runs inside the isolate instead of terminating immediately. + test('rotate-search', () => expectDecoded('rotate-search', 22)) +}) + +/** + * U3 - the per-scope calls wrapper. Every claim below is a describe() in the encoder's own + * StringArrayScopeCallsWrapperTransformer.spec.ts, and each case names it. + * + * All of these were `unowned` before U3: U2 could not evaluate a call whose arguments are another + * function's parameters, and could not delete machinery such a wrapper still calls. + */ +describe('scope calls wrappers — StringArrayScopeCallsWrapperTransformer.spec.ts', () => { + // Variant #1.2: function scope. Four wrappers, two of them nested and forwarding to a + // Program-scope wrapper rather than to the root, so this is already a two-hop chain. + test('wrappers-function', () => expectDecoded('wrappers-function', 8)) + + // Variant #6.1.1: `Mangled` identifier names generator. THE case the synthetic lifted names + // exist for - mangled output reuses short names across sibling scopes, and every wrapper is + // lifted into one flat isolate scope, so lifting them verbatim would let one definition win. + test('scope-chained-mangled', () => expectDecoded('scope-chained-mangled', 9)) + + // Variant #6.2.2: chained calls, advanced. Three levels deep, so a wrapper's upper is itself a + // wrapper whose upper is a wrapper - membership cannot be settled in one pass. + test('scope-chained-deep', () => expectDecoded('scope-chained-deep', 7)) + + // Variant #7.1.2: `hexadecimal-numeric-string` indexes. The offset is a coercing STRING, + // `param - '0x28b'`. Distinct from `index-numeric-string`, whose variable-form index is a bare + // literal and so never exercises the coercion. + test('scope-numeric-string-offset', () => + expectDecoded('scope-numeric-string-offset', 8)) + + // Variant #7.3: no wrappers on a root scope. The chain bottoms out at the root *wrapper*, not + // at a root-scope wrapper, which is the case a fixpoint seeded from the wrong end would miss. + test('scope-no-root-wrappers', () => + expectDecoded('scope-no-root-wrappers', 5)) + + // Variant #4: prevailing kind of variables. A `const` program gets `const` wrappers - no corpus + // input is anything but `var`, so this shape has no other coverage. + test('scope-prevailing-const', () => + expectDecoded('scope-prevailing-const', 8)) + + // Variant #3.1: if statement scope is *prohibited*, so the read inside it routes to an + // enclosing scope's wrapper. The callee is not declared in the call's own block. + test('scope-prohibited-if', () => expectDecoded('scope-prohibited-if', 3)) + + // Variant #2.4: a literal in a function default parameter, read in the enclosing scope rather + // than the function body's. + test('scope-default-parameter', () => + expectDecoded('scope-default-parameter', 5)) +}) + +describe('absent — a verdict, not a failure', () => { + // Variant #2. Obfuscated output with the option off. This is what the terminating round of a + // peel loop reads, and it must not be confusable with a fingerprint miss. + test('string-array-off', () => expectUntouched('string-array-off', 'absent')) + + // Variant #6: every literal below the encoder's three-character membership gate, so no array is + // built at all. + test('short-literal-value', () => + expectUntouched('short-literal-value', 'absent')) +}) + +describe('unowned — mine, readable, and a later unit owns the rest', () => { + // **Deliberately empty for now.** `wrappers-function` used to live here: U2 could not decode + // through a function-form scope wrapper, so it left the sample alone and said so. U3 resolves + // that shape, and the case moved to `decoded` above. + // + // The outcome itself is NOT dead and must not be deleted with the last case that reached it - + // `foreignWrappers` still fires for a function that calls the machinery with non-constant + // arguments and is not a resolvable scope wrapper, which is what U5's control-flow storage + // looks like. It has no committed case only because no fixture reaches it yet. + test.todo('a construct a later unit owns — needs a U5-shaped case') +}) + +describe('unreadable — the refusal, kept narrow', () => { + // A refusal path with no committed case is one a later refactor can silently delete, so each of + // these damages a sample that decodes cleanly in exactly one way. **The note is asserted, not + // just the status:** all five refusals report `unreadable`, so a case can land on somebody + // else's guard and still look right. That is not hypothetical — a corrupted-array case once + // terminated normally and was caught by the identifier check while claiming to test the timeout. + + // The rotator deleted. Nothing about the remaining shape says it should be there, so the array + // is simply left rotated: every call site returns a real string from the wrong slot, and the + // output parses, runs, and reads zero on every residue axis. The array items here are + // deliberately not identifier-shaped, or the wrong strings would be plausible ones and the + // guard would correctly stay silent. + test('guard-rotator-removed', () => { + const res = expectUntouched('guard-rotator-removed', 'unreadable') + expect( + res.notes.some((n) => + n.includes('computed-member-key position are not valid identifiers'), + ), + ).toBe(true) + }) + + // A checksum operand corrupted. The compare loop searches until a checksum over the array's own + // contents matches, so it cannot terminate — which is what makes the timeout a correctness + // instrument rather than a safety net. Two seconds here because ten in a committed suite is ten + // nobody wants; the pass's own default is longer. + test('guard-checksum-corrupted', () => { + const res = expectUntouched('guard-checksum-corrupted', 'unreadable', { + timeout: 2000, + }) + expect(res.notes.some((n) => n.includes('did not evaluate'))).toBe(true) + }, 20000) + + // An alias assigned without a declaration, so it is a global with no binding and its call sites + // cannot be enumerated. Deleting the machinery here breaks the program rather than leaving + // countable residue — decode-js#138's real sample needed exactly this hand-edited first. + test('guard-alias-undeclared', () => { + const res = expectUntouched('guard-alias-undeclared', 'unreadable') + expect(res.notes.some((n) => n.includes('have no binding'))).toBe(true) + }) + + // A read of the array from outside the machinery. The wrapper's call sites are enumerated + // exhaustively but the holder is a separate binding, so without this gate the array would be + // deleted out from under a program that indexes it directly. + test('guard-array-read-outside', () => { + const res = expectUntouched('guard-array-read-outside', 'unreadable') + expect(res.notes.some((n) => n.includes('is read from'))).toBe(true) + }) + + // Evidence present, entrypoint unresolvable. This is the detection-time refusal, and keeping it + // distinct from `absent` is the distinction the existing plugin collapses into one message. + test('guard-wrapper-removed', () => { + const res = expectUntouched('guard-wrapper-removed', 'unreadable') + expect( + res.notes.some((n) => n.includes('the entrypoint did not resolve')), + ).toBe(true) + }) +}) + +describe('one run, one layer', () => { + /** + * The only case in the suite that pins the layer boundary, which is where the corruption class + * lives: a pass that mutates the layer beneath it produces damage that is loud only when it + * happens to throw, where an unmatched inner layer is residue anyone can count. + * + * The input is encoded twice at 2.19.0 with different seeds. The expected output is **derivable + * rather than merely observed**: the second encode's input was the first encode's output, so + * peeling layer 2 has to reproduce that input — up to what layer 2 destroyed (local identifier + * names) and what our own normalization reprinted. So the assertions below are structural and + * behavioural, not byte equality against the separately-built single-encode. + */ + test('nested-one-layer', () => { + const res = expectDecoded('nested-one-layer', 21) + expect(res.removed.holder).toBe(1) + + // 1. What comes back is an ordinary single-encode: running the pass again finds a whole + // string-array subsystem and decodes it. A peel that had corrupted the inner layer would + // fail here, where a size- or residue-based check could easily pass. + const peeled = fs.readFileSync( + join(root, 'nested-one-layer.fix.js'), + 'utf-8', + ) + const second = parse(peeled, { allowReturnOutsideFunction: true }) + normalizeStatements(second) + const res2 = decodeStringArray(second) + expect(res2.status).toBe('decoded') + + // 2. And one further run reaches the source: the literals the original program was written + // with are back, as literals. + const final = generate(second).code + const src = fs.readFileSync(join(root, 'nested-one-layer.src.js'), 'utf-8') + for (const literal of src.match(/'[^']+'/g) || []) { + const value = literal.slice(1, -1) + if (value.length < 3 || value === '\\n') continue + expect(final).toContain(JSON.stringify(value)) + } + }) +}) diff --git a/test/visitor/obfuscator/string-array/baseline.fix.js b/test/visitor/obfuscator/string-array/baseline.fix.js new file mode 100644 index 00000000..06f31a86 --- /dev/null +++ b/test/visitor/obfuscator/string-array/baseline.fix.js @@ -0,0 +1,2 @@ +var test = "test"; +process["stdout"]["write"](String(test) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/baseline.js b/test/visitor/obfuscator/string-array/baseline.js new file mode 100644 index 00000000..c7cecc5c --- /dev/null +++ b/test/visitor/obfuscator/string-array/baseline.js @@ -0,0 +1 @@ +var test=_0x1944(0x1dc);function _0x1944(_0x5f208e,_0x417777){var _0x19442b=_0x4177();return _0x1944=function(_0x14b5ea,_0x5487cb){_0x14b5ea=_0x14b5ea-0x1dc;var _0x33faf2=_0x19442b[_0x14b5ea];return _0x33faf2;},_0x1944(_0x5f208e,_0x417777);}function _0x4177(){var _0x34f6c4=['test','stdout','write'];_0x4177=function(){return _0x34f6c4;};return _0x4177();}process[_0x1944(0x1dd)][_0x1944(0x1de)](String(test)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/baseline.src.js b/test/visitor/obfuscator/string-array/baseline.src.js new file mode 100644 index 00000000..211ed019 --- /dev/null +++ b/test/visitor/obfuscator/string-array/baseline.src.js @@ -0,0 +1,2 @@ +var test = 'test'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/calls-wrapper-name.fix.js b/test/visitor/obfuscator/string-array/calls-wrapper-name.fix.js new file mode 100644 index 00000000..735845fb --- /dev/null +++ b/test/visitor/obfuscator/string-array/calls-wrapper-name.fix.js @@ -0,0 +1,8 @@ +(function () { + function c() { + console["log"]('a'); + var d; + function d() {} + } + c(); +})(); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/calls-wrapper-name.js b/test/visitor/obfuscator/string-array/calls-wrapper-name.js new file mode 100644 index 00000000..9426628c --- /dev/null +++ b/test/visitor/obfuscator/string-array/calls-wrapper-name.js @@ -0,0 +1 @@ +function a(){var e=['log'];a=function(){return e;};return a();}function b(c,d){var e=a();return b=function(f,g){f=f-0x140;var h=e[f];return h;},b(c,d);}(function(){function c(){console[b(0x140)]('a');var d;function d(){}}c();}()); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/calls-wrapper-name.src.js b/test/visitor/obfuscator/string-array/calls-wrapper-name.src.js new file mode 100644 index 00000000..243f71f9 --- /dev/null +++ b/test/visitor/obfuscator/string-array/calls-wrapper-name.src.js @@ -0,0 +1,8 @@ +(function(){ + function foo () { + console.log('a'); + var b; + function b () {} + } + foo(); +})(); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/encoding-base64-rc4.fix.js b/test/visitor/obfuscator/string-array/encoding-base64-rc4.fix.js new file mode 100644 index 00000000..d71a3317 --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-base64-rc4.fix.js @@ -0,0 +1,9 @@ +var s0 = "alpha"; +var s1 = "bravo"; +var s2 = "charlie"; +var s3 = "delta"; +var s4 = "echo"; +var s5 = "foxtrot"; +var s6 = "golf"; +var s7 = "hotel"; +process["stdout"]["write"](String(s0 + '|' + s1 + '|' + s2 + '|' + s3 + '|' + s4 + '|' + s5 + '|' + s6 + '|' + s7) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/encoding-base64-rc4.js b/test/visitor/obfuscator/string-array/encoding-base64-rc4.js new file mode 100644 index 00000000..baebdec1 --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-base64-rc4.js @@ -0,0 +1 @@ +function _0x1abe(_0x4cd2e0,_0xca43dc){var _0x2f76a7=_0xca43();return _0x1abe=function(_0x261184,_0x2329a2){_0x261184=_0x261184-0x9b;var _0x4bbab0=_0x2f76a7[_0x261184];if(_0x1abe['wUlwUK']===undefined){var _0x17e871=function(_0x2a7197){var _0x2abcbb='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x162ae5='',_0x32635f='';for(var _0x292c30=0x0,_0x5d2680,_0x36171e,_0x5b286c=0x0;_0x36171e=_0x2a7197['charAt'](_0x5b286c++);~_0x36171e&&(_0x5d2680=_0x292c30%0x4?_0x5d2680*0x40+_0x36171e:_0x36171e,_0x292c30++%0x4)?_0x162ae5+=String['fromCharCode'](0xff&_0x5d2680>>(-0x2*_0x292c30&0x6)):0x0){_0x36171e=_0x2abcbb['indexOf'](_0x36171e);}for(var _0x3013e6=0x0,_0x1569e2=_0x162ae5['length'];_0x3013e6<_0x1569e2;_0x3013e6++){_0x32635f+='%'+('00'+_0x162ae5['charCodeAt'](_0x3013e6)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x32635f);};var _0x585dd1=function(_0x10a800,_0x4e418b){var _0x3c3f0d=[],_0x595072=0x0,_0x4f8fc0,_0x135ec9='';_0x10a800=_0x17e871(_0x10a800);var _0x4d5055;for(_0x4d5055=0x0;_0x4d5055<0x100;_0x4d5055++){_0x3c3f0d[_0x4d5055]=_0x4d5055;}for(_0x4d5055=0x0;_0x4d5055<0x100;_0x4d5055++){_0x595072=(_0x595072+_0x3c3f0d[_0x4d5055]+_0x4e418b['charCodeAt'](_0x4d5055%_0x4e418b['length']))%0x100,_0x4f8fc0=_0x3c3f0d[_0x4d5055],_0x3c3f0d[_0x4d5055]=_0x3c3f0d[_0x595072],_0x3c3f0d[_0x595072]=_0x4f8fc0;}_0x4d5055=0x0,_0x595072=0x0;for(var _0x10622b=0x0;_0x10622b<_0x10a800['length'];_0x10622b++){_0x4d5055=(_0x4d5055+0x1)%0x100,_0x595072=(_0x595072+_0x3c3f0d[_0x4d5055])%0x100,_0x4f8fc0=_0x3c3f0d[_0x4d5055],_0x3c3f0d[_0x4d5055]=_0x3c3f0d[_0x595072],_0x3c3f0d[_0x595072]=_0x4f8fc0,_0x135ec9+=String['fromCharCode'](_0x10a800['charCodeAt'](_0x10622b)^_0x3c3f0d[(_0x3c3f0d[_0x4d5055]+_0x3c3f0d[_0x595072])%0x100]);}return _0x135ec9;};_0x1abe['sxbZly']=_0x585dd1,_0x4cd2e0=arguments,_0x1abe['wUlwUK']=!![];}var _0x509fe6=_0x2f76a7[0x0],_0x376916=_0x261184+_0x509fe6,_0x1abe8c=_0x4cd2e0[_0x376916];return!_0x1abe8c?(_0x1abe['RtnSiS']===undefined&&(_0x1abe['RtnSiS']=!![]),_0x4bbab0=_0x1abe['sxbZly'](_0x4bbab0,_0x2329a2),_0x4cd2e0[_0x376916]=_0x4bbab0):_0x4bbab0=_0x1abe8c,_0x4bbab0;},_0x1abe(_0x4cd2e0,_0xca43dc);}var s0=_0x1abe(0x9b,'I]*R'),s1=_0x2f76(0x9c),s2=_0x1abe(0x9d,'4HtA'),s3=_0x1abe(0x9e,'nTjD'),s4=_0x2f76(0x9f),s5=_0x1abe(0xa0,'uWmk'),s6=_0x1abe(0xa1,'py!J'),s7=_0x1abe(0xa2,'GYr5');function _0x2f76(_0x4cd2e0,_0xca43dc){var _0x2f76a7=_0xca43();return _0x2f76=function(_0x261184,_0x2329a2){_0x261184=_0x261184-0x9b;var _0x4bbab0=_0x2f76a7[_0x261184];if(_0x2f76['tTeoRk']===undefined){var _0x17e871=function(_0x585dd1){var _0x2a7197='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x2abcbb='',_0x162ae5='';for(var _0x32635f=0x0,_0x292c30,_0x5d2680,_0x36171e=0x0;_0x5d2680=_0x585dd1['charAt'](_0x36171e++);~_0x5d2680&&(_0x292c30=_0x32635f%0x4?_0x292c30*0x40+_0x5d2680:_0x5d2680,_0x32635f++%0x4)?_0x2abcbb+=String['fromCharCode'](0xff&_0x292c30>>(-0x2*_0x32635f&0x6)):0x0){_0x5d2680=_0x2a7197['indexOf'](_0x5d2680);}for(var _0x5b286c=0x0,_0x3013e6=_0x2abcbb['length'];_0x5b286c<_0x3013e6;_0x5b286c++){_0x162ae5+='%'+('00'+_0x2abcbb['charCodeAt'](_0x5b286c)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x162ae5);};_0x2f76['TBNaHQ']=_0x17e871,_0x4cd2e0=arguments,_0x2f76['tTeoRk']=!![];}var _0x509fe6=_0x2f76a7[0x0],_0x376916=_0x261184+_0x509fe6,_0x1abe8c=_0x4cd2e0[_0x376916];return!_0x1abe8c?(_0x4bbab0=_0x2f76['TBNaHQ'](_0x4bbab0),_0x4cd2e0[_0x376916]=_0x4bbab0):_0x4bbab0=_0x1abe8c,_0x4bbab0;},_0x2f76(_0x4cd2e0,_0xca43dc);}function _0xca43(){var _0x2b011e=['WOVdRefJbq','yNjHDM8','WR4Yt3P6WQen','ttKyW4dcHa','zwnOBW','W5fMW4/dR8kPW7qv','WPvlW6tdUa','W4uqW4FdMmol','WPNdTfvKewq','D3jPDgu'];_0xca43=function(){return _0x2b011e;};return _0xca43();}process[_0x1abe(0xa3,'I]*R')][_0x2f76(0xa4)](String(s0+'|'+s1+'|'+s2+'|'+s3+'|'+s4+'|'+s5+'|'+s6+'|'+s7)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/encoding-base64-rc4.src.js b/test/visitor/obfuscator/string-array/encoding-base64-rc4.src.js new file mode 100644 index 00000000..7322e302 --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-base64-rc4.src.js @@ -0,0 +1,9 @@ +var s0 = 'alpha'; +var s1 = 'bravo'; +var s2 = 'charlie'; +var s3 = 'delta'; +var s4 = 'echo'; +var s5 = 'foxtrot'; +var s6 = 'golf'; +var s7 = 'hotel'; +process.stdout.write(String(s0 + '|' + s1 + '|' + s2 + '|' + s3 + '|' + s4 + '|' + s5 + '|' + s6 + '|' + s7) + '\n'); diff --git a/test/visitor/obfuscator/string-array/encoding-rc4.fix.js b/test/visitor/obfuscator/string-array/encoding-rc4.fix.js new file mode 100644 index 00000000..06f31a86 --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-rc4.fix.js @@ -0,0 +1,2 @@ +var test = "test"; +process["stdout"]["write"](String(test) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/encoding-rc4.js b/test/visitor/obfuscator/string-array/encoding-rc4.js new file mode 100644 index 00000000..eee6868a --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-rc4.js @@ -0,0 +1 @@ +function _0x4177(){var _0x78aa04=['pmojcXi','lgiLaCkBeW','A1HCm8ol'];_0x4177=function(){return _0x78aa04;};return _0x4177();}var test=_0x1944(0x1dc,'ucS5');function _0x1944(_0x5f208e,_0x417777){var _0x19442b=_0x4177();return _0x1944=function(_0x14b5ea,_0x5487cb){_0x14b5ea=_0x14b5ea-0x1dc;var _0x33faf2=_0x19442b[_0x14b5ea];if(_0x1944['PhwYTu']===undefined){var _0x498ea6=function(_0x47a54e){var _0x3c2c1c='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x3e9899='',_0x4c807b='';for(var _0x48db50=0x0,_0x383951,_0x1672de,_0xc4c33e=0x0;_0x1672de=_0x47a54e['charAt'](_0xc4c33e++);~_0x1672de&&(_0x383951=_0x48db50%0x4?_0x383951*0x40+_0x1672de:_0x1672de,_0x48db50++%0x4)?_0x3e9899+=String['fromCharCode'](0xff&_0x383951>>(-0x2*_0x48db50&0x6)):0x0){_0x1672de=_0x3c2c1c['indexOf'](_0x1672de);}for(var _0x2105f8=0x0,_0x4f28ce=_0x3e9899['length'];_0x2105f8<_0x4f28ce;_0x2105f8++){_0x4c807b+='%'+('00'+_0x3e9899['charCodeAt'](_0x2105f8)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x4c807b);};var _0x54be8b=function(_0x59eafb,_0x6b3392){var _0xdc5c66=[],_0x530b08=0x0,_0x26b044,_0x6c2032='';_0x59eafb=_0x498ea6(_0x59eafb);var _0x49950b;for(_0x49950b=0x0;_0x49950b<0x100;_0x49950b++){_0xdc5c66[_0x49950b]=_0x49950b;}for(_0x49950b=0x0;_0x49950b<0x100;_0x49950b++){_0x530b08=(_0x530b08+_0xdc5c66[_0x49950b]+_0x6b3392['charCodeAt'](_0x49950b%_0x6b3392['length']))%0x100,_0x26b044=_0xdc5c66[_0x49950b],_0xdc5c66[_0x49950b]=_0xdc5c66[_0x530b08],_0xdc5c66[_0x530b08]=_0x26b044;}_0x49950b=0x0,_0x530b08=0x0;for(var _0x169124=0x0;_0x169124<_0x59eafb['length'];_0x169124++){_0x49950b=(_0x49950b+0x1)%0x100,_0x530b08=(_0x530b08+_0xdc5c66[_0x49950b])%0x100,_0x26b044=_0xdc5c66[_0x49950b],_0xdc5c66[_0x49950b]=_0xdc5c66[_0x530b08],_0xdc5c66[_0x530b08]=_0x26b044,_0x6c2032+=String['fromCharCode'](_0x59eafb['charCodeAt'](_0x169124)^_0xdc5c66[(_0xdc5c66[_0x49950b]+_0xdc5c66[_0x530b08])%0x100]);}return _0x6c2032;};_0x1944['CKnwGT']=_0x54be8b,_0x5f208e=arguments,_0x1944['PhwYTu']=!![];}var _0x44bf5d=_0x19442b[0x0],_0x110847=_0x14b5ea+_0x44bf5d,_0x40c0bd=_0x5f208e[_0x110847];return!_0x40c0bd?(_0x1944['MYsnYD']===undefined&&(_0x1944['MYsnYD']=!![]),_0x33faf2=_0x1944['CKnwGT'](_0x33faf2,_0x5487cb),_0x5f208e[_0x110847]=_0x33faf2):_0x33faf2=_0x40c0bd,_0x33faf2;},_0x1944(_0x5f208e,_0x417777);}process[_0x1944(0x1dd,'*lr@')][_0x1944(0x1de,'rvSh')](String(test)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/encoding-rc4.src.js b/test/visitor/obfuscator/string-array/encoding-rc4.src.js new file mode 100644 index 00000000..211ed019 --- /dev/null +++ b/test/visitor/obfuscator/string-array/encoding-rc4.src.js @@ -0,0 +1,2 @@ +var test = 'test'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/guard-alias-undeclared.js b/test/visitor/obfuscator/string-array/guard-alias-undeclared.js new file mode 100644 index 00000000..caf3a69f --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-alias-undeclared.js @@ -0,0 +1 @@ +_0x2001b6=_0xde91;var _0xc96050=_0xde91,foo=_0x2001b6(0x109),bar=_0xc96050(0x10a),baz=_0x2001b6(0x10b);function _0x538a(){var _0x2d29a6=['foo','bar','baz','bark','hawk','eagle','stdout','write'];_0x538a=function(){return _0x2d29a6;};return _0x538a();}function test(){var _0x5031cf=_0xc96050,_0x4d9a2c=_0x2001b6,_0x1b2a2f=_0x5031cf(0x10c),_0x3606cc=_0x5031cf(0x10d),_0x3ab71b=_0x5031cf(0x10e);}function _0xde91(_0xaf96cc,_0x538a17){var _0xde914f=_0x538a();return _0xde91=function(_0x4f8fff,_0x2d8e9e){_0x4f8fff=_0x4f8fff-0x109;var _0xb1ad15=_0xde914f[_0x4f8fff];return _0xb1ad15;},_0xde91(_0xaf96cc,_0x538a17);}process[_0x2001b6(0x10f)][_0xc96050(0x110)](String(foo+bar+baz)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/guard-alias-undeclared.src.js b/test/visitor/obfuscator/string-array/guard-alias-undeclared.src.js new file mode 100644 index 00000000..5c5e9fca --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-alias-undeclared.src.js @@ -0,0 +1,11 @@ +var foo = 'foo' +var bar = 'bar'; +var baz = 'baz'; + +function test () { + var bark = 'bark' + var hawk = 'hawk'; + var eagle = 'eagle'; +} + +process.stdout.write(String(foo + bar + baz) + '\n'); diff --git a/test/visitor/obfuscator/string-array/guard-array-read-outside.js b/test/visitor/obfuscator/string-array/guard-array-read-outside.js new file mode 100644 index 00000000..95d8920c --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-array-read-outside.js @@ -0,0 +1 @@ +function _0x1a8c(_0x4ba73e,_0x102a8a){var _0x548df1=_0x548d();return _0x1a8c=function(_0x1a8cc7,_0x53921a){_0x1a8cc7=_0x1a8cc7-0xe1;var _0x47d4fd=_0x548df1[_0x1a8cc7];return _0x47d4fd;},_0x1a8c(_0x4ba73e,_0x102a8a);}function _0x548d(){var _0x1712a0=['9261vVPYFF','1164XBFBSG','6605880SgtzSl','972swRBfV','62097DzCZRo','10040408DrSPoX','4794174FHBosn','7073470gqYGPV','testvalue','stdout','write','2SDHHUi','761416ROkhfl'];_0x548d=function(){return _0x1712a0;};return _0x548d();}(function(_0x123c43,_0x50ebde){var _0x4c7dbd=_0x123c43();while(!![]){try{var _0x2006e1=-parseInt(_0x1a8c(0xe1))/0x1*(parseInt(_0x1a8c(0xe2))/0x2)+-parseInt(_0x1a8c(0xe3))/0x3*(parseInt(_0x1a8c(0xe4))/0x4)+parseInt(_0x1a8c(0xe5))/0x5+-parseInt(_0x1a8c(0xe6))/0x6*(parseInt(_0x1a8c(0xe7))/0x7)+parseInt(_0x1a8c(0xe8))/0x8+parseInt(_0x1a8c(0xe9))/0x9+parseInt(_0x1a8c(0xea))/0xa;if(_0x2006e1===_0x50ebde)break;else _0x4c7dbd['push'](_0x4c7dbd['shift']());}catch(_0x3e2913){_0x4c7dbd['push'](_0x4c7dbd['shift']());}}})(_0x548d,0xafa41);var test=_0x1a8c(0xeb);process[_0x1a8c(0xec)][_0x1a8c(0xed)](String(test)+'\x0a');void _0x548d; \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/guard-array-read-outside.src.js b/test/visitor/obfuscator/string-array/guard-array-read-outside.src.js new file mode 100644 index 00000000..297d435b --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-array-read-outside.src.js @@ -0,0 +1,2 @@ +var test = 'testvalue'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/guard-checksum-corrupted.js b/test/visitor/obfuscator/string-array/guard-checksum-corrupted.js new file mode 100644 index 00000000..6d5cae85 --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-checksum-corrupted.js @@ -0,0 +1 @@ +function _0x1dd6(_0x135e9f,_0x1e9be){var _0x5ad0b7=_0x5ad0();return _0x1dd6=function(_0x1dd622,_0x1a0363){_0x1dd622=_0x1dd622-0x1b0;var _0x5ce6da=_0x5ad0b7[_0x1dd622];return _0x5ce6da;},_0x1dd6(_0x135e9f,_0x1e9be);}(function(_0x2060f6,_0x63ed00){var _0x4ac616=_0x2060f6();while(!![]){try{var _0x18c136=parseInt(_0x1dd6(0x1b0))/0x1+-parseInt(_0x1dd6(0x1b1))/0x2+parseInt(_0x1dd6(0x1b2))/0x3+-parseInt(_0x1dd6(0x1b3))/0x4*(-parseInt(_0x1dd6(0x1b4))/0x5)+-parseInt(_0x1dd6(0x1b5))/0x6+-parseInt(_0x1dd6(0x1b6))/0x7+parseInt(_0x1dd6(0x1b7))/0x8;if(_0x18c136===_0x63ed00)break;else _0x4ac616['push'](_0x4ac616['shift']());}catch(_0x197d93){_0x4ac616['push'](_0x4ac616['shift']());}}})(_0x5ad0,0x5c3d3);var s0=_0x1dd6(0x1b8),s1=_0x1dd6(0x1b9),s2=_0x1dd6(0x1ba),s3=_0x1dd6(0x1bb),s4=_0x1dd6(0x1bc),s5=_0x1dd6(0x1bd),s6=_0x1dd6(0x1be),s7=_0x1dd6(0x1bf),s8=_0x1dd6(0x1c0),s9=_0x1dd6(0x1c1),s10=_0x1dd6(0x1c2),s11=_0x1dd6(0x1c3),s12=_0x1dd6(0x1c4),s13=_0x1dd6(0x1c5),s14=_0x1dd6(0x1c6),s15=_0x1dd6(0x1c7),s16=_0x1dd6(0x1c8),s17=_0x1dd6(0x1c9),s18=_0x1dd6(0x1ca),s19=_0x1dd6(0x1cb);function _0x5ad0(){var _0x50ef5b=['item_12_value','item_13_value','item_14_value','item_15_value','item_16_value','item_17_value','item_18_value','item_19_value','stdout','write',"963128CwHiDo",'1214256aAXImL','1432218LuVCBt','8dZthCn','1530380bGnTUz','3865434HXNwEQ','947856YMEtCP','4895200NFbEkJ','item_0_value','item_1_value','item_2_value','item_3_value','item_4_value','item_5_value','item_6_value','item_7_value','item_8_value','item_9_value','item_10_value','item_11_value'];_0x5ad0=function(){return _0x50ef5b;};return _0x5ad0();}process[_0x1dd6(0x1cc)][_0x1dd6(0x1cd)](String(s0+','+s1+','+s2+','+s3+','+s4+','+s5+','+s6+','+s7+','+s8+','+s9+','+s10+','+s11+','+s12+','+s13+','+s14+','+s15+','+s16+','+s17+','+s18+','+s19)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/guard-checksum-corrupted.src.js b/test/visitor/obfuscator/string-array/guard-checksum-corrupted.src.js new file mode 100644 index 00000000..51bbee5e --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-checksum-corrupted.src.js @@ -0,0 +1,21 @@ +var s0 = 'item_0_value'; +var s1 = 'item_1_value'; +var s2 = 'item_2_value'; +var s3 = 'item_3_value'; +var s4 = 'item_4_value'; +var s5 = 'item_5_value'; +var s6 = 'item_6_value'; +var s7 = 'item_7_value'; +var s8 = 'item_8_value'; +var s9 = 'item_9_value'; +var s10 = 'item_10_value'; +var s11 = 'item_11_value'; +var s12 = 'item_12_value'; +var s13 = 'item_13_value'; +var s14 = 'item_14_value'; +var s15 = 'item_15_value'; +var s16 = 'item_16_value'; +var s17 = 'item_17_value'; +var s18 = 'item_18_value'; +var s19 = 'item_19_value'; +process.stdout.write(String(s0 + ',' + s1 + ',' + s2 + ',' + s3 + ',' + s4 + ',' + s5 + ',' + s6 + ',' + s7 + ',' + s8 + ',' + s9 + ',' + s10 + ',' + s11 + ',' + s12 + ',' + s13 + ',' + s14 + ',' + s15 + ',' + s16 + ',' + s17 + ',' + s18 + ',' + s19) + '\n'); diff --git a/test/visitor/obfuscator/string-array/guard-rotator-removed.js b/test/visitor/obfuscator/string-array/guard-rotator-removed.js new file mode 100644 index 00000000..a5e224b4 --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-rotator-removed.js @@ -0,0 +1 @@ +function _0x2ebf(){var _0x388f88=['value\x209\x20/\x20of\x20twelve','value\x2010\x20/\x20of\x20twelve','value\x2011\x20/\x20of\x20twelve','stdout','write','179868VyFBOI','113686Eglgfd','22632FqJAiO','76PvULvq','223920MvPVrC','18XmdfCZ','49LVAzAx','187408WqZuab','171qhTWak','100930pjFkzd','3803492gunfnM','value\x200\x20/\x20of\x20twelve','value\x201\x20/\x20of\x20twelve','value\x202\x20/\x20of\x20twelve','value\x203\x20/\x20of\x20twelve','value\x204\x20/\x20of\x20twelve','value\x205\x20/\x20of\x20twelve','value\x206\x20/\x20of\x20twelve','value\x207\x20/\x20of\x20twelve','value\x208\x20/\x20of\x20twelve'];_0x2ebf=function(){return _0x388f88;};return _0x2ebf();}function _0xa4f2(_0x247808,_0x3fe5f5){var _0x2ebff3=_0x2ebf();return _0xa4f2=function(_0xa4f277,_0x4b33ed){_0xa4f277=_0xa4f277-0x169;var _0x46109d=_0x2ebff3[_0xa4f277];return _0x46109d;},_0xa4f2(_0x247808,_0x3fe5f5);}var s0=_0xa4f2(0x174),s1=_0xa4f2(0x175),s2=_0xa4f2(0x176),s3=_0xa4f2(0x177),s4=_0xa4f2(0x178),s5=_0xa4f2(0x179),s6=_0xa4f2(0x17a),s7=_0xa4f2(0x17b),s8=_0xa4f2(0x17c),s9=_0xa4f2(0x17d),s10=_0xa4f2(0x17e),s11=_0xa4f2(0x17f);process[_0xa4f2(0x180)][_0xa4f2(0x181)](String(s0+','+s1+','+s2+','+s3+','+s4+','+s5+','+s6+','+s7+','+s8+','+s9+','+s10+','+s11)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/guard-rotator-removed.src.js b/test/visitor/obfuscator/string-array/guard-rotator-removed.src.js new file mode 100644 index 00000000..97791c65 --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-rotator-removed.src.js @@ -0,0 +1,13 @@ +var s0 = 'value 0 / of twelve'; +var s1 = 'value 1 / of twelve'; +var s2 = 'value 2 / of twelve'; +var s3 = 'value 3 / of twelve'; +var s4 = 'value 4 / of twelve'; +var s5 = 'value 5 / of twelve'; +var s6 = 'value 6 / of twelve'; +var s7 = 'value 7 / of twelve'; +var s8 = 'value 8 / of twelve'; +var s9 = 'value 9 / of twelve'; +var s10 = 'value 10 / of twelve'; +var s11 = 'value 11 / of twelve'; +process.stdout.write(String(s0 + ',' + s1 + ',' + s2 + ',' + s3 + ',' + s4 + ',' + s5 + ',' + s6 + ',' + s7 + ',' + s8 + ',' + s9 + ',' + s10 + ',' + s11) + '\n'); diff --git a/test/visitor/obfuscator/string-array/guard-wrapper-removed.js b/test/visitor/obfuscator/string-array/guard-wrapper-removed.js new file mode 100644 index 00000000..e53384cb --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-wrapper-removed.js @@ -0,0 +1 @@ +function _0x548d(){var _0x1712a0=['9261vVPYFF','1164XBFBSG','6605880SgtzSl','972swRBfV','62097DzCZRo','10040408DrSPoX','4794174FHBosn','7073470gqYGPV','testvalue','stdout','write','2SDHHUi','761416ROkhfl'];_0x548d=function(){return _0x1712a0;};return _0x548d();}(function(_0x123c43,_0x50ebde){var _0x4c7dbd=_0x123c43();while(!![]){try{var _0x2006e1=-parseInt(_0x1a8c(0xe1))/0x1*(parseInt(_0x1a8c(0xe2))/0x2)+-parseInt(_0x1a8c(0xe3))/0x3*(parseInt(_0x1a8c(0xe4))/0x4)+parseInt(_0x1a8c(0xe5))/0x5+-parseInt(_0x1a8c(0xe6))/0x6*(parseInt(_0x1a8c(0xe7))/0x7)+parseInt(_0x1a8c(0xe8))/0x8+parseInt(_0x1a8c(0xe9))/0x9+parseInt(_0x1a8c(0xea))/0xa;if(_0x2006e1===_0x50ebde)break;else _0x4c7dbd['push'](_0x4c7dbd['shift']());}catch(_0x3e2913){_0x4c7dbd['push'](_0x4c7dbd['shift']());}}})(_0x548d,0xafa41);var test=_0x1a8c(0xeb);process[_0x1a8c(0xec)][_0x1a8c(0xed)](String(test)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/guard-wrapper-removed.src.js b/test/visitor/obfuscator/string-array/guard-wrapper-removed.src.js new file mode 100644 index 00000000..297d435b --- /dev/null +++ b/test/visitor/obfuscator/string-array/guard-wrapper-removed.src.js @@ -0,0 +1,2 @@ +var test = 'testvalue'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/index-mixed-types.fix.js b/test/visitor/obfuscator/string-array/index-mixed-types.fix.js new file mode 100644 index 00000000..1a16acea --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-mixed-types.fix.js @@ -0,0 +1,8 @@ +function test() { + var _0x4966b2 = "foo" + 0x1; + var _0x1b0733 = "bar" + 0x2; + var _0x4e046f = "baz" + 0x3; + return _0x4966b2 + _0x1b0733 + _0x4e046f; +} +test(); +process["stdout"]["write"](String(test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-mixed-types.js b/test/visitor/obfuscator/string-array/index-mixed-types.js new file mode 100644 index 00000000..7f62fb6d --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-mixed-types.js @@ -0,0 +1 @@ +function test(){var _0x4966b2=_0x2657('0x136')+0x1,_0x1b0733=_0x2657(0x137)+0x2,_0x4e046f=_0x2657(0x138)+0x3;return _0x4966b2+_0x1b0733+_0x4e046f;}function _0x2657(_0x2d85ca,_0x132f17){var _0x2657f9=_0x132f();return _0x2657=function(_0x58933b,_0x136274){_0x58933b=_0x58933b-0x136;var _0x28b11c=_0x2657f9[_0x58933b];return _0x28b11c;},_0x2657(_0x2d85ca,_0x132f17);}function _0x132f(){var _0x42cc5c=['foo','bar','baz','stdout','write'];_0x132f=function(){return _0x42cc5c;};return _0x132f();}test(),process[_0x2657('0x139')][_0x2657(0x13a)](String(test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-mixed-types.src.js b/test/visitor/obfuscator/string-array/index-mixed-types.src.js new file mode 100644 index 00000000..8ab5948b --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-mixed-types.src.js @@ -0,0 +1,10 @@ +function test () { + var foo = 'foo' + 1; + var bar = 'bar' + 2; + var baz = 'baz' + 3; + + return foo + bar + baz; +} + +test(); +process.stdout.write(String(test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/index-numeric-string.fix.js b/test/visitor/obfuscator/string-array/index-numeric-string.fix.js new file mode 100644 index 00000000..06f31a86 --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-numeric-string.fix.js @@ -0,0 +1,2 @@ +var test = "test"; +process["stdout"]["write"](String(test) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-numeric-string.js b/test/visitor/obfuscator/string-array/index-numeric-string.js new file mode 100644 index 00000000..df66f0a7 --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-numeric-string.js @@ -0,0 +1 @@ +var test=_0x1944('0x1dc');function _0x1944(_0x5f208e,_0x417777){var _0x19442b=_0x4177();return _0x1944=function(_0x14b5ea,_0x5487cb){_0x14b5ea=_0x14b5ea-0x1dc;var _0x33faf2=_0x19442b[_0x14b5ea];return _0x33faf2;},_0x1944(_0x5f208e,_0x417777);}function _0x4177(){var _0x34f6c4=['test','stdout','write'];_0x4177=function(){return _0x34f6c4;};return _0x4177();}process[_0x1944('0x1dd')][_0x1944('0x1de')](String(test)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-numeric-string.src.js b/test/visitor/obfuscator/string-array/index-numeric-string.src.js new file mode 100644 index 00000000..211ed019 --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-numeric-string.src.js @@ -0,0 +1,2 @@ +var test = 'test'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.fix.js b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.fix.js new file mode 100644 index 00000000..60cd5d9b --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.fix.js @@ -0,0 +1,8 @@ +function test() { + var _0x1b0733 = "foo" + 0x1; + var _0x4e046f = "bar" + 0x2; + var _0x4c48e4 = "baz" + 0x3; + return _0x1b0733 + _0x4e046f + _0x4c48e4; +} +test(); +process["stdout"]["write"](String(test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.js b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.js new file mode 100644 index 00000000..fa503a9a --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.js @@ -0,0 +1 @@ +function _0x5893(_0x2d85ca,_0x132f17){var _0x2657f9=_0x2657();return _0x5893=function(_0x58933b,_0x136274){_0x58933b=_0x58933b-0x136;var _0x28b11c=_0x2657f9[_0x58933b];if(_0x5893['RIPzrD']===undefined){var _0x1a64f8=function(_0x1b0733){var _0x4e046f='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x4c48e4='',_0x45af75='';for(var _0x153730=0x0,_0x4a9a43,_0x2c7bff,_0x24ec37=0x0;_0x2c7bff=_0x1b0733['charAt'](_0x24ec37++);~_0x2c7bff&&(_0x4a9a43=_0x153730%0x4?_0x4a9a43*0x40+_0x2c7bff:_0x2c7bff,_0x153730++%0x4)?_0x4c48e4+=String['fromCharCode'](0xff&_0x4a9a43>>(-0x2*_0x153730&0x6)):0x0){_0x2c7bff=_0x4e046f['indexOf'](_0x2c7bff);}for(var _0x5819d6=0x0,_0x46ed59=_0x4c48e4['length'];_0x5819d6<_0x46ed59;_0x5819d6++){_0x45af75+='%'+('00'+_0x4c48e4['charCodeAt'](_0x5819d6)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x45af75);};_0x5893['gZwhWQ']=_0x1a64f8,_0x2d85ca=arguments,_0x5893['RIPzrD']=!![];}var _0x2c046a=_0x2657f9[0x0],_0x19f98a=_0x58933b+_0x2c046a,_0x4966b2=_0x2d85ca[_0x19f98a];return!_0x4966b2?(_0x28b11c=_0x5893['gZwhWQ'](_0x28b11c),_0x2d85ca[_0x19f98a]=_0x28b11c):_0x28b11c=_0x4966b2,_0x28b11c;},_0x5893(_0x2d85ca,_0x132f17);}(function(_0x20a4ed,_0xe41dfe){var _0x35dbc6=_0x20a4ed();while(!![]){try{var _0x1b9d73=-parseInt(_0x5893(0x144))/0x1+parseInt(_0x5893(0x137))/0x2+parseInt(_0x5893(0x143))/0x3*(-parseInt(_0x5893(0x13c))/0x4)+-parseInt(_0x5893(0x13f))/0x5*(parseInt(_0x5893(0x138))/0x6)+-parseInt(_0x5893(0x13d))/0x7*(-parseInt(_0x5893(0x142))/0x8)+parseInt(_0x5893(0x145))/0x9+parseInt(_0x5893(0x13a))/0xa*(-parseInt(_0x5893(0x140))/0xb);if(_0x1b9d73===_0xe41dfe)break;else _0x35dbc6['push'](_0x35dbc6['shift']());}catch(_0x413f29){_0x35dbc6['push'](_0x35dbc6['shift']());}}}(_0x2657,0xb8f6a));function test(){var _0x1b0733=_0x5893(0x141)+0x1,_0x4e046f=_0x5893(0x13e)+0x2,_0x4c48e4=_0x5893(0x136)+0x3;return _0x1b0733+_0x4e046f+_0x4c48e4;}test(),process[_0x5893(0x13b)][_0x5893(0x139)](String(test())+'\x0a');function _0x2657(){var _0x42af0e=['odK5mda1mwzfvNjfEG','yMfY','nJboDuzPuum','mZyZr0LyrKjb','zM9V','ohLkvNvtDG','ntrXENbgs3q','odqXmZaWtNPPrgXY','nJy0nJy1m1fzqNvfqq','yMf6','mtCXmdy0mLHLBNDWEG','mtCXmdy2zujhDwnR','D3jPDgu','mJuZmteWrufYtLHr','C3rKB3v0','mJi2mJH6A0LICuW'];_0x2657=function(){return _0x42af0e;};return _0x2657();} \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.src.js b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.src.js new file mode 100644 index 00000000..8ab5948b --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift-rotate-shuffle.src.js @@ -0,0 +1,10 @@ +function test () { + var foo = 'foo' + 1; + var bar = 'bar' + 2; + var baz = 'baz' + 3; + + return foo + bar + baz; +} + +test(); +process.stdout.write(String(test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/index-shift.fix.js b/test/visitor/obfuscator/string-array/index-shift.fix.js new file mode 100644 index 00000000..1a16acea --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift.fix.js @@ -0,0 +1,8 @@ +function test() { + var _0x4966b2 = "foo" + 0x1; + var _0x1b0733 = "bar" + 0x2; + var _0x4e046f = "baz" + 0x3; + return _0x4966b2 + _0x1b0733 + _0x4e046f; +} +test(); +process["stdout"]["write"](String(test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-shift.js b/test/visitor/obfuscator/string-array/index-shift.js new file mode 100644 index 00000000..a415c0e5 --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift.js @@ -0,0 +1 @@ +function test(){var _0x4966b2=_0x2657(0x136)+0x1,_0x1b0733=_0x2657(0x137)+0x2,_0x4e046f=_0x2657(0x138)+0x3;return _0x4966b2+_0x1b0733+_0x4e046f;}function _0x2657(_0x2d85ca,_0x132f17){var _0x2657f9=_0x132f();return _0x2657=function(_0x58933b,_0x136274){_0x58933b=_0x58933b-0x136;var _0x28b11c=_0x2657f9[_0x58933b];return _0x28b11c;},_0x2657(_0x2d85ca,_0x132f17);}function _0x132f(){var _0x42cc5c=['foo','bar','baz','stdout','write'];_0x132f=function(){return _0x42cc5c;};return _0x132f();}test(),process[_0x2657(0x139)][_0x2657(0x13a)](String(test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/index-shift.src.js b/test/visitor/obfuscator/string-array/index-shift.src.js new file mode 100644 index 00000000..8ab5948b --- /dev/null +++ b/test/visitor/obfuscator/string-array/index-shift.src.js @@ -0,0 +1,10 @@ +function test () { + var foo = 'foo' + 1; + var bar = 'bar' + 2; + var baz = 'baz' + 3; + + return foo + bar + baz; +} + +test(); +process.stdout.write(String(test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/nested-one-layer.fix.js b/test/visitor/obfuscator/string-array/nested-one-layer.fix.js new file mode 100644 index 00000000..8f178ee4 --- /dev/null +++ b/test/visitor/obfuscator/string-array/nested-one-layer.fix.js @@ -0,0 +1,34 @@ +function _0x571f(_0x4d0fad, _0x3fec5f) { + var _0xe568f2 = _0x3605(); + _0x571f = function (_0x520afe, _0x1c224c) { + _0x520afe = _0x520afe - 0xdd; + var _0x2b3790 = _0xe568f2[_0x520afe]; + return _0x2b3790; + }; + return _0x571f(_0x4d0fad, _0x3fec5f); +} +(function (_0x282af, _0x231ad3) { + var _0x1a8ed4 = _0x282af(); + while (!![]) { + try { + var _0x34baae = parseInt(_0x571f(0xdd)) / 0x1 * (-parseInt(_0x571f(0xde)) / 0x2) + -parseInt(_0x571f(0xdf)) / 0x3 * (-parseInt(_0x571f(0xe0)) / 0x4) + parseInt(_0x571f(0xe1)) / 0x5 * (parseInt(_0x571f(0xe2)) / 0x6) + -parseInt(_0x571f(0xe3)) / 0x7 + -parseInt(_0x571f(0xe4)) / 0x8 + parseInt(_0x571f(0xe5)) / 0x9 * (-parseInt(_0x571f(0xe6)) / 0xa) + parseInt(_0x571f(0xe7)) / 0xb * (parseInt(_0x571f(0xe8)) / 0xc); + if (_0x34baae === _0x231ad3) { + break; + } else { + _0x1a8ed4["push"](_0x1a8ed4["shift"]()); + } + } catch (_0x3f65b9) { + _0x1a8ed4["push"](_0x1a8ed4["shift"]()); + } + } +})(_0x3605, 0xb2332); +function _0x3605() { + var _0x5601ee = ["hello world", "nested layers", "stdout", "write", " / ", "1535FMwCWh", "1518lbnNNN", "3153bdmAnl", "2884ZUjkBE", "109995kjkQBc", "138xPDVlq", "9852633NQFATz", "904624TxVcqa", "21033piYpJq", "3010EaNiRy", "11ZeRtDR", "34263084kXjhGm"]; + _0x3605 = function () { + return _0x5601ee; + }; + return _0x3605(); +} +var greeting = _0x571f(0xe9); +var target = _0x571f(0xea); +process[_0x571f(0xeb)][_0x571f(0xec)](String(greeting + _0x571f(0xed) + target) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/nested-one-layer.js b/test/visitor/obfuscator/string-array/nested-one-layer.js new file mode 100644 index 00000000..88784347 --- /dev/null +++ b/test/visitor/obfuscator/string-array/nested-one-layer.js @@ -0,0 +1 @@ +(function(_0x171e4f,_0x1801e7){var _0x3301ed=_0x171e4f();while(!![]){try{var _0x452224=parseInt(_0x22dc(0x10d))/0x1+-parseInt(_0x22dc(0x10e))/0x2*(-parseInt(_0x22dc(0x10f))/0x3)+parseInt(_0x22dc(0x110))/0x4+parseInt(_0x22dc(0x111))/0x5*(-parseInt(_0x22dc(0x112))/0x6)+parseInt(_0x22dc(0x113))/0x7*(-parseInt(_0x22dc(0x114))/0x8)+parseInt(_0x22dc(0x115))/0x9*(parseInt(_0x22dc(0x116))/0xa)+-parseInt(_0x22dc(0x117))/0xb;if(_0x452224===_0x1801e7)break;else _0x3301ed['push'](_0x3301ed['shift']());}catch(_0x1dd516){_0x3301ed['push'](_0x3301ed['shift']());}}}(_0x99e7,0xd01f1));function _0x22dc(_0x3efd7e,_0x38a6ed){var _0x99e7e7=_0x99e7();return _0x22dc=function(_0x22dc31,_0x36bd22){_0x22dc31=_0x22dc31-0x10d;var _0x214d66=_0x99e7e7[_0x22dc31];return _0x214d66;},_0x22dc(_0x3efd7e,_0x38a6ed);}function _0x571f(_0x4d0fad,_0x3fec5f){var _0xe568f2=_0x3605();return _0x571f=function(_0x520afe,_0x1c224c){_0x520afe=_0x520afe-0xdd;var _0x2b3790=_0xe568f2[_0x520afe];return _0x2b3790;},_0x571f(_0x4d0fad,_0x3fec5f);}(function(_0x282af,_0x231ad3){var _0x1a8ed4=_0x282af();while(!![]){try{var _0x34baae=parseInt(_0x571f(0xdd))/0x1*(-parseInt(_0x571f(0xde))/0x2)+-parseInt(_0x571f(0xdf))/0x3*(-parseInt(_0x571f(0xe0))/0x4)+parseInt(_0x571f(0xe1))/0x5*(parseInt(_0x571f(0xe2))/0x6)+-parseInt(_0x571f(0xe3))/0x7+-parseInt(_0x571f(0xe4))/0x8+parseInt(_0x571f(0xe5))/0x9*(-parseInt(_0x571f(0xe6))/0xa)+parseInt(_0x571f(0xe7))/0xb*(parseInt(_0x571f(0xe8))/0xc);if(_0x34baae===_0x231ad3)break;else _0x1a8ed4[_0x22dc(0x118)](_0x1a8ed4[_0x22dc(0x119)]());}catch(_0x3f65b9){_0x1a8ed4[_0x22dc(0x118)](_0x1a8ed4[_0x22dc(0x119)]());}}}(_0x3605,0xb2332));function _0x3605(){var _0x5601ee=[_0x22dc(0x11a),_0x22dc(0x11b),_0x22dc(0x11c),_0x22dc(0x11d),_0x22dc(0x11e),_0x22dc(0x11f),_0x22dc(0x120),_0x22dc(0x121),_0x22dc(0x122),_0x22dc(0x123),_0x22dc(0x124),_0x22dc(0x125),_0x22dc(0x126),_0x22dc(0x127),_0x22dc(0x128),_0x22dc(0x129),_0x22dc(0x12a)];return _0x3605=function(){return _0x5601ee;},_0x3605();}function _0x99e7(){var _0x2326b7=['stdout','write','\x20/\x20','1535FMwCWh','1518lbnNNN','3153bdmAnl','2884ZUjkBE','109995kjkQBc','138xPDVlq','9852633NQFATz','904624TxVcqa','21033piYpJq','3010EaNiRy','11ZeRtDR','34263084kXjhGm','351971GRGqkY','554WtnjXX','15558DQVGsr','4885124UYxddM','126305sDIrLJ','114NSxldk','4053ixwLlE','7120FqjxZL','13939326rGUbNs','10swukTM','29819394OzQcJV','push','shift','hello\x20world','nested\x20layers'];_0x99e7=function(){return _0x2326b7;};return _0x99e7();}var greeting=_0x571f(0xe9),target=_0x571f(0xea);process[_0x571f(0xeb)][_0x571f(0xec)](String(greeting+_0x571f(0xed)+target)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/nested-one-layer.src.js b/test/visitor/obfuscator/string-array/nested-one-layer.src.js new file mode 100644 index 00000000..b0f7e574 --- /dev/null +++ b/test/visitor/obfuscator/string-array/nested-one-layer.src.js @@ -0,0 +1,3 @@ +var greeting = 'hello world'; +var target = 'nested layers'; +process.stdout.write(String(greeting + ' / ' + target) + '\n'); diff --git a/test/visitor/obfuscator/string-array/object-computed-key.fix.js b/test/visitor/obfuscator/string-array/object-computed-key.fix.js new file mode 100644 index 00000000..968e9015 --- /dev/null +++ b/test/visitor/obfuscator/string-array/object-computed-key.fix.js @@ -0,0 +1,4 @@ +var test = { + ["foo"]: "barbaz" +}; +process["stdout"]["write"](String(test["foo"]) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/object-computed-key.js b/test/visitor/obfuscator/string-array/object-computed-key.js new file mode 100644 index 00000000..17b7e168 --- /dev/null +++ b/test/visitor/obfuscator/string-array/object-computed-key.js @@ -0,0 +1 @@ +var test={[_0x205a(0x8d)]:_0x205a(0x8e)};function _0x205a(_0x488832,_0x19b56e){var _0x205a4e=_0x19b5();return _0x205a=function(_0x408db9,_0x3605d4){_0x408db9=_0x408db9-0x8d;var _0x19e5b7=_0x205a4e[_0x408db9];return _0x19e5b7;},_0x205a(_0x488832,_0x19b56e);}process[_0x205a(0x8f)][_0x205a(0x90)](String(test[_0x205a(0x8d)])+'\x0a');function _0x19b5(){var _0x19d089=['foo','barbaz','stdout','write'];_0x19b5=function(){return _0x19d089;};return _0x19b5();} \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/object-computed-key.src.js b/test/visitor/obfuscator/string-array/object-computed-key.src.js new file mode 100644 index 00000000..c7ea8422 --- /dev/null +++ b/test/visitor/obfuscator/string-array/object-computed-key.src.js @@ -0,0 +1,2 @@ +var test = {['foo']: 'barbaz'}; +process.stdout.write(String(test['foo']) + '\n'); diff --git a/test/visitor/obfuscator/string-array/rotate-search.fix.js b/test/visitor/obfuscator/string-array/rotate-search.fix.js new file mode 100644 index 00000000..efc61e35 --- /dev/null +++ b/test/visitor/obfuscator/string-array/rotate-search.fix.js @@ -0,0 +1,21 @@ +var s0 = "item_0_value"; +var s1 = "item_1_value"; +var s2 = "item_2_value"; +var s3 = "item_3_value"; +var s4 = "item_4_value"; +var s5 = "item_5_value"; +var s6 = "item_6_value"; +var s7 = "item_7_value"; +var s8 = "item_8_value"; +var s9 = "item_9_value"; +var s10 = "item_10_value"; +var s11 = "item_11_value"; +var s12 = "item_12_value"; +var s13 = "item_13_value"; +var s14 = "item_14_value"; +var s15 = "item_15_value"; +var s16 = "item_16_value"; +var s17 = "item_17_value"; +var s18 = "item_18_value"; +var s19 = "item_19_value"; +process["stdout"]["write"](String(s0 + ',' + s1 + ',' + s2 + ',' + s3 + ',' + s4 + ',' + s5 + ',' + s6 + ',' + s7 + ',' + s8 + ',' + s9 + ',' + s10 + ',' + s11 + ',' + s12 + ',' + s13 + ',' + s14 + ',' + s15 + ',' + s16 + ',' + s17 + ',' + s18 + ',' + s19) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/rotate-search.js b/test/visitor/obfuscator/string-array/rotate-search.js new file mode 100644 index 00000000..b5d023f0 --- /dev/null +++ b/test/visitor/obfuscator/string-array/rotate-search.js @@ -0,0 +1 @@ +function _0x1dd6(_0x135e9f,_0x1e9be){var _0x5ad0b7=_0x5ad0();return _0x1dd6=function(_0x1dd622,_0x1a0363){_0x1dd622=_0x1dd622-0x1b0;var _0x5ce6da=_0x5ad0b7[_0x1dd622];return _0x5ce6da;},_0x1dd6(_0x135e9f,_0x1e9be);}(function(_0x2060f6,_0x63ed00){var _0x4ac616=_0x2060f6();while(!![]){try{var _0x18c136=parseInt(_0x1dd6(0x1b0))/0x1+-parseInt(_0x1dd6(0x1b1))/0x2+parseInt(_0x1dd6(0x1b2))/0x3+-parseInt(_0x1dd6(0x1b3))/0x4*(-parseInt(_0x1dd6(0x1b4))/0x5)+-parseInt(_0x1dd6(0x1b5))/0x6+-parseInt(_0x1dd6(0x1b6))/0x7+parseInt(_0x1dd6(0x1b7))/0x8;if(_0x18c136===_0x63ed00)break;else _0x4ac616['push'](_0x4ac616['shift']());}catch(_0x197d93){_0x4ac616['push'](_0x4ac616['shift']());}}}(_0x5ad0,0x5c3d3));var s0=_0x1dd6(0x1b8),s1=_0x1dd6(0x1b9),s2=_0x1dd6(0x1ba),s3=_0x1dd6(0x1bb),s4=_0x1dd6(0x1bc),s5=_0x1dd6(0x1bd),s6=_0x1dd6(0x1be),s7=_0x1dd6(0x1bf),s8=_0x1dd6(0x1c0),s9=_0x1dd6(0x1c1),s10=_0x1dd6(0x1c2),s11=_0x1dd6(0x1c3),s12=_0x1dd6(0x1c4),s13=_0x1dd6(0x1c5),s14=_0x1dd6(0x1c6),s15=_0x1dd6(0x1c7),s16=_0x1dd6(0x1c8),s17=_0x1dd6(0x1c9),s18=_0x1dd6(0x1ca),s19=_0x1dd6(0x1cb);function _0x5ad0(){var _0x50ef5b=['item_12_value','item_13_value','item_14_value','item_15_value','item_16_value','item_17_value','item_18_value','item_19_value','stdout','write','63128CwHiDo','1214256aAXImL','1432218LuVCBt','8dZthCn','1530380bGnTUz','3865434HXNwEQ','947856YMEtCP','4895200NFbEkJ','item_0_value','item_1_value','item_2_value','item_3_value','item_4_value','item_5_value','item_6_value','item_7_value','item_8_value','item_9_value','item_10_value','item_11_value'];_0x5ad0=function(){return _0x50ef5b;};return _0x5ad0();}process[_0x1dd6(0x1cc)][_0x1dd6(0x1cd)](String(s0+','+s1+','+s2+','+s3+','+s4+','+s5+','+s6+','+s7+','+s8+','+s9+','+s10+','+s11+','+s12+','+s13+','+s14+','+s15+','+s16+','+s17+','+s18+','+s19)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/rotate-search.src.js b/test/visitor/obfuscator/string-array/rotate-search.src.js new file mode 100644 index 00000000..51bbee5e --- /dev/null +++ b/test/visitor/obfuscator/string-array/rotate-search.src.js @@ -0,0 +1,21 @@ +var s0 = 'item_0_value'; +var s1 = 'item_1_value'; +var s2 = 'item_2_value'; +var s3 = 'item_3_value'; +var s4 = 'item_4_value'; +var s5 = 'item_5_value'; +var s6 = 'item_6_value'; +var s7 = 'item_7_value'; +var s8 = 'item_8_value'; +var s9 = 'item_9_value'; +var s10 = 'item_10_value'; +var s11 = 'item_11_value'; +var s12 = 'item_12_value'; +var s13 = 'item_13_value'; +var s14 = 'item_14_value'; +var s15 = 'item_15_value'; +var s16 = 'item_16_value'; +var s17 = 'item_17_value'; +var s18 = 'item_18_value'; +var s19 = 'item_19_value'; +process.stdout.write(String(s0 + ',' + s1 + ',' + s2 + ',' + s3 + ',' + s4 + ',' + s5 + ',' + s6 + ',' + s7 + ',' + s8 + ',' + s9 + ',' + s10 + ',' + s11 + ',' + s12 + ',' + s13 + ',' + s14 + ',' + s15 + ',' + s16 + ',' + s17 + ',' + s18 + ',' + s19) + '\n'); diff --git a/test/visitor/obfuscator/string-array/same-literal-values.fix.js b/test/visitor/obfuscator/string-array/same-literal-values.fix.js new file mode 100644 index 00000000..42237a1b --- /dev/null +++ b/test/visitor/obfuscator/string-array/same-literal-values.fix.js @@ -0,0 +1,3 @@ +var a = "test"; +var b = "test"; +process["stdout"]["write"](String(a + b) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/same-literal-values.js b/test/visitor/obfuscator/string-array/same-literal-values.js new file mode 100644 index 00000000..3618d71f --- /dev/null +++ b/test/visitor/obfuscator/string-array/same-literal-values.js @@ -0,0 +1 @@ +function _0x57f0(_0xdbdcbd,_0x4d4d65){var _0x57f0d8=_0x4d4d();return _0x57f0=function(_0x49df54,_0xd9f02d){_0x49df54=_0x49df54-0x78;var _0x28963c=_0x57f0d8[_0x49df54];return _0x28963c;},_0x57f0(_0xdbdcbd,_0x4d4d65);}var a=_0x57f0(0x78),b=_0x57f0(0x78);function _0x4d4d(){var _0x17db97=['test','stdout','write'];_0x4d4d=function(){return _0x17db97;};return _0x4d4d();}process[_0x57f0(0x79)][_0x57f0(0x7a)](String(a+b)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/same-literal-values.src.js b/test/visitor/obfuscator/string-array/same-literal-values.src.js new file mode 100644 index 00000000..d79a17b4 --- /dev/null +++ b/test/visitor/obfuscator/string-array/same-literal-values.src.js @@ -0,0 +1,3 @@ +var a = 'test'; +var b = 'test'; +process.stdout.write(String(a + b) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-chained-deep.fix.js b/test/visitor/obfuscator/string-array/scope-chained-deep.fix.js new file mode 100644 index 00000000..eb811298 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-deep.fix.js @@ -0,0 +1,19 @@ +const foo = "aaa"; +function test(_0x299e24, _0x74b9e9) { + const _0x2da1ee = "bbb"; + function _0x446608(_0x2235b2, _0x530d80) { + const _0x5b507e = "ccc"; + function _0x569071(_0x42bfb3, _0xbdddae) { + const _0x28c730 = "ddd"; + return _0x28c730; + } + return _0x5b507e + _0x569071(); + } + return _0x2da1ee + _0x446608(); +} +function test3(_0x54ebd2, _0x3019ca) { + const _0x379567 = "eee"; + return _0x379567; +} +foo + test() + test3(); +process["stdout"]["write"](String(foo + test() + test3()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-chained-deep.js b/test/visitor/obfuscator/string-array/scope-chained-deep.js new file mode 100644 index 00000000..c4805e05 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-deep.js @@ -0,0 +1 @@ +function _0x29d3(){const _0x1142a4=['aaa','bbb','ccc','ddd','eee','stdout','write'];_0x29d3=function(){return _0x1142a4;};return _0x29d3();}const foo=_0x588d31(0x28c,0x28d);function _0x2a2725(_0x3c93dd,_0x1daa3b){return _0x1018(_0x1daa3b-0x332,_0x3c93dd);}function _0x588d31(_0x368078,_0x1603c2){return _0x1018(_0x1603c2-0x196,_0x368078);}function test(_0x299e24,_0x74b9e9){const _0x2da1ee=_0x4a4d6a(-0x29c,-0x29e);function _0x446608(_0x2235b2,_0x530d80){function _0x49bd6f(_0x1d48d4,_0x1785d2){return _0x4a4d6a(_0x1d48d4-0x325,_0x1785d2);}const _0x5b507e=_0x49bd6f(0x8a,0x86);function _0x569071(_0x42bfb3,_0xbdddae){const _0x28c730=_0xe202bc(0x4a0,0x49f);function _0xe202bc(_0x19752c,_0x10c859){return _0x49bd6f(_0x19752c-0x415,_0x10c859);}return _0x28c730;}return _0x5b507e+_0x569071();}function _0x4a4d6a(_0x25e3d0,_0x584d35){return _0x588d31(_0x584d35,_0x25e3d0- -0x52a);}return _0x2da1ee+_0x446608();}function _0x5accb9(_0x206d2f,_0x21cb51){return _0x1018(_0x21cb51-0x133,_0x206d2f);}function test3(_0x54ebd2,_0x3019ca){const _0x379567=_0x104c44(-0x27e,-0x27b);function _0x104c44(_0x689fbf,_0x302364){return _0x588d31(_0x302364,_0x689fbf- -0x50f);}return _0x379567;}function _0x1018(_0x3639ad,_0x29d345){const _0x10188d=_0x29d3();return _0x1018=function(_0x65e9d9,_0x1c4e68){_0x65e9d9=_0x65e9d9-0xf7;let _0x3c270c=_0x10188d[_0x65e9d9];return _0x3c270c;},_0x1018(_0x3639ad,_0x29d345);}foo+test()+test3(),process[_0x588d31(0x290,0x292)][_0x588d31(0x296,0x293)](String(foo+test()+test3())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-chained-deep.src.js b/test/visitor/obfuscator/string-array/scope-chained-deep.src.js new file mode 100644 index 00000000..1a654081 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-deep.src.js @@ -0,0 +1,28 @@ +const foo = 'aaa'; + +function test (a, b) { + const bar = 'bbb'; + + function test1 (a, b) { + const baz = 'ccc'; + + function test2 (a, b) { + const bark = 'ddd'; + + return bark; + } + + return baz + test2(); + } + + return bar + test1(); +} + +function test3 (a, b) { + const hawk = 'eee'; + + return hawk; +} + +foo + test() + test3(); +process.stdout.write(String(foo + test() + test3()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-chained-mangled.fix.js b/test/visitor/obfuscator/string-array/scope-chained-mangled.fix.js new file mode 100644 index 00000000..686376a0 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-mangled.fix.js @@ -0,0 +1,18 @@ +const foo = "aaa"; +function test(c, d) { + const e = "bbb"; + const f = "ccc"; + function g(h, i) { + const j = "ddd"; + const k = "eee"; + function l(m, n) { + const o = "ddd"; + const p = "eee"; + return o + p; + } + return j + k; + } + return e + f + g(); +} +foo + test(); +process["stdout"]["write"](String(foo + test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-chained-mangled.js b/test/visitor/obfuscator/string-array/scope-chained-mangled.js new file mode 100644 index 00000000..531e5049 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-mangled.js @@ -0,0 +1 @@ +function a(){const z=['aaa','bbb','ccc','ddd','eee','stdout','write'];a=function(){return z;};return a();}function b(c,d){const e=a();return b=function(f,g){f=f-0xcc;let h=e[f];return h;},b(c,d);}function x(c,d){return b(d-0xd2,c);}const foo=q(-0x239,-0x235);function y(c,d){return b(c-0xed,d);}function q(c,d){return b(c- -0x305,d);}function test(c,d){function s(c,d){return q(c- -0x9a,d);}function r(c,d){return q(c-0x20a,d);}const e=r(-0x2e,-0x30),f=r(-0x2d,-0x2d);function g(h,i){const j=t(0x21f,0x221);function u(c,d){return r(c- -0x4f,d);}const k=t(0x220,0x222);function t(c,d){return s(d-0x4f1,c);}function l(m,n){const o=v(0x133,0x133),p=v(0x134,0x138);function w(c,d){return t(c,d- -0x48e);}function v(c,d){return t(d,c- -0xee);}return o+p;}return j+k;}return e+f+g();}foo+test(),process[q(-0x234,-0x237)][q(-0x233,-0x231)](String(foo+test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-chained-mangled.src.js b/test/visitor/obfuscator/string-array/scope-chained-mangled.src.js new file mode 100644 index 00000000..a28b1a0c --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-chained-mangled.src.js @@ -0,0 +1,25 @@ +const foo = 'aaa'; + +function test (a, b) { + const bar = 'bbb'; + const baz = 'ccc'; + + function test1 (a, b) { + const bark = 'ddd'; + const hawk = 'eee'; + + function test2 (a, b) { + const bark = 'ddd'; + const hawk = 'eee'; + + return bark + hawk; + } + + return bark + hawk; + } + + return bar + baz + test1(); +} + +foo + test(); +process.stdout.write(String(foo + test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-default-parameter.fix.js b/test/visitor/obfuscator/string-array/scope-default-parameter.fix.js new file mode 100644 index 00000000..ad468097 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-default-parameter.fix.js @@ -0,0 +1,5 @@ +const foo = "foo"; +function test(_0x107f8b = "bar") { + const _0x5699f9 = "baz"; +} +process["stdout"]["write"](String(foo + test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-default-parameter.js b/test/visitor/obfuscator/string-array/scope-default-parameter.js new file mode 100644 index 00000000..afda3db0 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-default-parameter.js @@ -0,0 +1 @@ +function _0x11ae(_0x344959,_0x284a3e){const _0x11aeb9=_0x284a();return _0x11ae=function(_0x168dc8,_0x3dd62b){_0x168dc8=_0x168dc8-0x6d;let _0x1db213=_0x11aeb9[_0x168dc8];return _0x1db213;},_0x11ae(_0x344959,_0x284a3e);}function _0x49d114(_0x39b493,_0x1d47d7){return _0x11ae(_0x1d47d7- -0x315,_0x39b493);}const foo=_0x25812c(0x33c,0x33d);function _0x25812c(_0x2a9c45,_0x56620f){return _0x11ae(_0x56620f-0x2d0,_0x2a9c45);}function _0x284a(){const _0x14fbdc=['foo','bar','baz','stdout','write'];_0x284a=function(){return _0x14fbdc;};return _0x284a();}function test(_0x107f8b=_0x49d114(-0x2a5,-0x2a7)){function _0x4990cc(_0x31f082,_0x195cf1){return _0x49d114(_0x195cf1,_0x31f082-0x6cb);}const _0x5699f9=_0x4990cc(0x425,0x427);}process[_0x49d114(-0x2a4,-0x2a5)][_0x25812c(0x33e,0x341)](String(foo+test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-default-parameter.src.js b/test/visitor/obfuscator/string-array/scope-default-parameter.src.js new file mode 100644 index 00000000..d5cea816 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-default-parameter.src.js @@ -0,0 +1,7 @@ +const foo = 'foo' + +function test (bar = 'bar') { + const baz = 'baz' +} + +process.stdout.write(String(foo + test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-no-root-wrappers.fix.js b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.fix.js new file mode 100644 index 00000000..dfdc3a09 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.fix.js @@ -0,0 +1,6 @@ +function test() { + const c = "foo"; + const d = "bar"; + const e = "baz"; +} +process["stdout"]["write"](String(test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-no-root-wrappers.js b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.js new file mode 100644 index 00000000..61f90a0e --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.js @@ -0,0 +1 @@ +function b(c,d){const e=a();return b=function(f,g){f=f-0xd0;let h=e[f];return h;},b(c,d);}function g(c,d){return b(c- -0x198,d);}function a(){const h=['foo','bar','baz','stdout','write'];a=function(){return h;};return a();}function test(){const c=f(0x3a,0x38),d=f(0x3b,0x3b);function f(c,d){return b(c- -0x96,d);}const e=f(0x3c,0x3a);}process[g(-0xc5,-0xc7)][g(-0xc4,-0xc5)](String(test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-no-root-wrappers.src.js b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.src.js new file mode 100644 index 00000000..bc4ffd0e --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-no-root-wrappers.src.js @@ -0,0 +1,7 @@ +function test () { + const foo = 'foo' + const bar = 'bar'; + const baz = 'baz'; +} + +process.stdout.write(String(test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-numeric-string-offset.fix.js b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.fix.js new file mode 100644 index 00000000..6e380f66 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.fix.js @@ -0,0 +1,9 @@ +const foo = "foo"; +const bar = "bar"; +const baz = "baz"; +function test() { + const _0x577b7f = "bark"; + const _0x5b2d6c = "hawk"; + const _0x4668da = "eagle"; +} +process["stdout"]["write"](String(foo + test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-numeric-string-offset.js b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.js new file mode 100644 index 00000000..e24f6676 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.js @@ -0,0 +1 @@ +const foo=_0x18a0e3(-'0xd7',-'0xd4'),bar=_0x18a0e3(-'0xd6',-'0xd5'),baz=_0x3c157c('0x12a','0x126');function _0x3c157c(_0x5eae73,_0x1537ea){return _0x4136(_0x5eae73-'0xae',_0x1537ea);}function _0x4136(_0x56b826,_0x3105d1){const _0x4136a4=_0x3105();return _0x4136=function(_0x1a93bf,_0x36e495){_0x1a93bf=_0x1a93bf-0x7a;let _0x453a7e=_0x4136a4[_0x1a93bf];return _0x453a7e;},_0x4136(_0x56b826,_0x3105d1);}function test(){const _0x577b7f=_0x392f01(-'0x14e',-'0x152');function _0x392f01(_0xc061b7,_0x50d477){return _0x18a0e3(_0x50d477- -'0x7e',_0xc061b7);}function _0x5c8dff(_0x3df275,_0x4265c4){return _0x18a0e3(_0x3df275-'0x4c8',_0x4265c4);}const _0x5b2d6c=_0x5c8dff('0x3f5','0x3f7'),_0x4668da=_0x392f01(-'0x153',-'0x150');}function _0x18a0e3(_0x293d71,_0x5bef59){return _0x4136(_0x293d71- -'0x151',_0x5bef59);}process[_0x18a0e3(-'0xd1',-'0xd4')][_0x18a0e3(-'0xd0',-'0xd0')](String(foo+test())+'\x0a');function _0x3105(){const _0x254647=['foo','bar','baz','bark','hawk','eagle','stdout','write'];_0x3105=function(){return _0x254647;};return _0x3105();} \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-numeric-string-offset.src.js b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.src.js new file mode 100644 index 00000000..34fcf850 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-numeric-string-offset.src.js @@ -0,0 +1,11 @@ +const foo = 'foo' +const bar = 'bar'; +const baz = 'baz'; + +function test () { + const bark = 'bark' + const hawk = 'hawk'; + const eagle = 'eagle'; +} + +process.stdout.write(String(foo + test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-prevailing-const.fix.js b/test/visitor/obfuscator/string-array/scope-prevailing-const.fix.js new file mode 100644 index 00000000..6e380f66 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prevailing-const.fix.js @@ -0,0 +1,9 @@ +const foo = "foo"; +const bar = "bar"; +const baz = "baz"; +function test() { + const _0x577b7f = "bark"; + const _0x5b2d6c = "hawk"; + const _0x4668da = "eagle"; +} +process["stdout"]["write"](String(foo + test()) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-prevailing-const.js b/test/visitor/obfuscator/string-array/scope-prevailing-const.js new file mode 100644 index 00000000..8c8c0023 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prevailing-const.js @@ -0,0 +1 @@ +const _0x18a0e3=_0x4136,_0x45660e=_0x4136,foo=_0x18a0e3(0x7a),bar=_0x45660e(0x7b),baz=_0x18a0e3(0x7c);function _0x4136(_0x56b826,_0x3105d1){const _0x4136a4=_0x3105();return _0x4136=function(_0x1a93bf,_0x36e495){_0x1a93bf=_0x1a93bf-0x7a;let _0x453a7e=_0x4136a4[_0x1a93bf];return _0x453a7e;},_0x4136(_0x56b826,_0x3105d1);}function test(){const _0x4f9e95=_0x18a0e3,_0x5183e3=_0x45660e,_0x577b7f=_0x4f9e95(0x7d),_0x5b2d6c=_0x5183e3(0x7e),_0x4668da=_0x5183e3(0x7f);}function _0x3105(){const _0x47feb9=['foo','bar','baz','bark','hawk','eagle','stdout','write'];_0x3105=function(){return _0x47feb9;};return _0x3105();}process[_0x45660e(0x80)][_0x45660e(0x81)](String(foo+test())+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-prevailing-const.src.js b/test/visitor/obfuscator/string-array/scope-prevailing-const.src.js new file mode 100644 index 00000000..34fcf850 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prevailing-const.src.js @@ -0,0 +1,11 @@ +const foo = 'foo' +const bar = 'bar'; +const baz = 'baz'; + +function test () { + const bark = 'bark' + const hawk = 'hawk'; + const eagle = 'eagle'; +} + +process.stdout.write(String(foo + test()) + '\n'); diff --git a/test/visitor/obfuscator/string-array/scope-prohibited-if.fix.js b/test/visitor/obfuscator/string-array/scope-prohibited-if.fix.js new file mode 100644 index 00000000..f0514835 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prohibited-if.fix.js @@ -0,0 +1,4 @@ +if (!![]) { + var foo = "foo"; +} +process["stdout"]["write"](String(foo) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-prohibited-if.js b/test/visitor/obfuscator/string-array/scope-prohibited-if.js new file mode 100644 index 00000000..ab63fc01 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prohibited-if.js @@ -0,0 +1 @@ +function _0x16484b(_0x3d2f35,_0x218736){return _0x3206(_0x218736-0x30d,_0x3d2f35);}function _0x483b55(_0x3e8aa0,_0x3c8c75){return _0x3206(_0x3e8aa0- -0x253,_0x3c8c75);}function _0x3206(_0x326bb2,_0x52890f){var _0x3206ef=_0x5289();return _0x3206=function(_0x17f04c,_0x4f075b){_0x17f04c=_0x17f04c-0x130;var _0x59e678=_0x3206ef[_0x17f04c];return _0x59e678;},_0x3206(_0x326bb2,_0x52890f);}function _0x5289(){var _0x41622a=['foo','stdout','write'];_0x5289=function(){return _0x41622a;};return _0x5289();}if(!![])var foo=_0x483b55(-0x123,-0x124);process[_0x483b55(-0x122,-0x122)][_0x16484b(0x440,0x43f)](String(foo)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/scope-prohibited-if.src.js b/test/visitor/obfuscator/string-array/scope-prohibited-if.src.js new file mode 100644 index 00000000..b1acaec4 --- /dev/null +++ b/test/visitor/obfuscator/string-array/scope-prohibited-if.src.js @@ -0,0 +1,4 @@ +if (true) { + var foo = 'foo'; +} +process.stdout.write(String(foo) + '\n'); diff --git a/test/visitor/obfuscator/string-array/short-literal-value.js b/test/visitor/obfuscator/string-array/short-literal-value.js new file mode 100644 index 00000000..524379e1 --- /dev/null +++ b/test/visitor/obfuscator/string-array/short-literal-value.js @@ -0,0 +1 @@ +var test='te'; \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/short-literal-value.src.js b/test/visitor/obfuscator/string-array/short-literal-value.src.js new file mode 100644 index 00000000..7abc01ad --- /dev/null +++ b/test/visitor/obfuscator/string-array/short-literal-value.src.js @@ -0,0 +1 @@ +var test = 'te'; diff --git a/test/visitor/obfuscator/string-array/string-array-off.js b/test/visitor/obfuscator/string-array/string-array-off.js new file mode 100644 index 00000000..7ed81c7e --- /dev/null +++ b/test/visitor/obfuscator/string-array/string-array-off.js @@ -0,0 +1 @@ +var test='test';process['stdout']['write'](String(test)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/string-array-off.src.js b/test/visitor/obfuscator/string-array/string-array-off.src.js new file mode 100644 index 00000000..211ed019 --- /dev/null +++ b/test/visitor/obfuscator/string-array/string-array-off.src.js @@ -0,0 +1,2 @@ +var test = 'test'; +process.stdout.write(String(test) + '\n'); diff --git a/test/visitor/obfuscator/string-array/wrappers-function.fix.js b/test/visitor/obfuscator/string-array/wrappers-function.fix.js new file mode 100644 index 00000000..2f8d4d7a --- /dev/null +++ b/test/visitor/obfuscator/string-array/wrappers-function.fix.js @@ -0,0 +1,9 @@ +var foo = "foo"; +var bar = "bar"; +var baz = "baz"; +function test() { + var _0x1b2a2f = "bark"; + var _0x3606cc = "hawk"; + var _0x3ab71b = "eagle"; +} +process["stdout"]["write"](String(foo + bar + baz) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/wrappers-function.js b/test/visitor/obfuscator/string-array/wrappers-function.js new file mode 100644 index 00000000..57a98d72 --- /dev/null +++ b/test/visitor/obfuscator/string-array/wrappers-function.js @@ -0,0 +1 @@ +var foo=_0x2001b6(0x1c5,0x1c6);function _0x540d7f(_0x5b3888,_0x4dd5b8){return _0xde91(_0x5b3888- -0xa0,_0x4dd5b8);}function _0xde91(_0xaf96cc,_0x538a17){var _0xde914f=_0x538a();return _0xde91=function(_0x4f8fff,_0x2d8e9e){_0x4f8fff=_0x4f8fff-0x109;var _0xb1ad15=_0xde914f[_0x4f8fff];return _0xb1ad15;},_0xde91(_0xaf96cc,_0x538a17);}var bar=_0x2001b6(0x1c6,0x1c6),baz=_0x540d7f(0x6b,0x6f);function _0x2001b6(_0x1d89b3,_0x1660b2){return _0xde91(_0x1d89b3-0xbc,_0x1660b2);}function test(){function _0x462936(_0x275d59,_0xa624a){return _0x2001b6(_0x275d59- -0xe4,_0xa624a);}function _0x31b1c0(_0x3718a4,_0x5dd948){return _0x2001b6(_0x3718a4-0x2b2,_0x5dd948);}var _0x1b2a2f=_0x462936(0xe4,0xe0),_0x3606cc=_0x31b1c0(0x47b,0x47b),_0x3ab71b=_0x31b1c0(0x47c,0x480);}function _0x538a(){var _0x2cbaca=['foo','bar','baz','bark','hawk','eagle','stdout','write'];_0x538a=function(){return _0x2cbaca;};return _0x538a();}process[_0x540d7f(0x6f,0x6b)][_0x2001b6(0x1cc,0x1cc)](String(foo+bar+baz)+'\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/string-array/wrappers-function.src.js b/test/visitor/obfuscator/string-array/wrappers-function.src.js new file mode 100644 index 00000000..5c5e9fca --- /dev/null +++ b/test/visitor/obfuscator/string-array/wrappers-function.src.js @@ -0,0 +1,11 @@ +var foo = 'foo' +var bar = 'bar'; +var baz = 'baz'; + +function test () { + var bark = 'bark' + var hawk = 'hawk'; + var eagle = 'eagle'; +} + +process.stdout.write(String(foo + bar + baz) + '\n'); diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch.test.js b/test/visitor/obfuscator/unflatten-switch-dispatch.test.js new file mode 100644 index 00000000..ad1802b6 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch.test.js @@ -0,0 +1,98 @@ +import { join } from 'path' +import { expect, test } from 'vitest' +import { getVisitorResult } from '../../helper.js' +import { createUnflattenSwitchDispatch } from '#visitor/obfuscator/unflatten-switch-dispatch' + +const root = join(__dirname, 'unflatten-switch-dispatch') + +/** + * The shared helper owns both assertions this suite needs, so it is used rather than reimplemented. + * + * - **A declining case needs no golden.** With `fix` false the helper compares the generated tree + * against the *input source*, which is what "left exactly as found" means; the input file is its + * own expected output. That check is not optional here even though every decline case also + * asserts a zero count: a count of zero says the pass reported no change, not that it made none, + * and mutate-then-decline is precisely the shared visitor's failure mode this fork exists to + * avoid. + * - **A rewriting case gets the reference-state comparison for free**, which a local helper is + * liable to omit: the transformed tree's scope bookkeeping must match a fresh parse of the + * expected output, so a missing or mis-scoped `crawl()` fails even when the printed text is + * identical. Stale scope state was one of this pass's own two defects, so it is the last check + * to hand-roll around. + * + * What the helper cannot supply is the `onChange` count, and that is the only reason for the + * wrapper below. The channel is part of the pass's interface - it is how a caller runs a pipeline + * to a fixpoint without re-serializing the tree - so a rewrite that forgot to report itself would + * silently make a fixpoint driver exit a round early. Nothing else pins that. + */ +function run(name, fix) { + let fired = 0 + getVisitorResult( + createUnflattenSwitchDispatch(() => fired++), + fix, + join(root, name), + ) + return fired +} + +/** + * Five of the seven cases below pin a **decline**, which is unusual for a fixture set and is the + * whole point of this suite. The shared `remove-control-flow-ob.js` passes the two rewrite cases + * and mishandles every decline case - two by corrupting output silently, one by throwing - so a + * suite covering only the happy path would not tell the two implementations apart. + */ + +test('baseline: the controller is a permutation, read left to right', () => { + // order '2|0|3|1' over cases 0=b,1=return d,2=a,3=c -> a(); b(); c(); return d(); + expect(run('baseline', true)).toBe(1) +}) + +test('merged-declaration: `simplify` fuses the two declarations into one', () => { + expect(run('merged-declaration', true)).toBe(1) +}) + +test('not-always-true: an ordinary `while (!done)` loop is left alone', () => { + // The shared visitor accepts any prefix unary here and deletes the loop, emitting its body + // straight-line. Declining is the only safe answer: nothing distinguishes this from a real + // state machine. + expect(run('not-always-true', false)).toBe(0) +}) + +test('fallthrough-case: a case without `continue` or `return` is left alone', () => { + // The shared visitor walks forward into the next case and appends it twice. + expect(run('fallthrough-case', false)).toBe(0) +}) + +test('no-increment: a discriminant without `++` is declined, not thrown on', () => { + // The shared visitor reads `property.argument.name` unguarded and throws, aborting the decode. + expect(run('no-increment', false)).toBe(0) +}) + +test('conditional-declaration: a controller initialised under an `if` is left alone', () => { + // `var` hoists, so a binding lookup resolves it - but the initialisation does not dominate the + // loop, and rewriting would run statements the original never runs. + expect(run('conditional-declaration', false)).toBe(0) +}) + +test('order-mismatch: an order string shorter than the case list is left alone', () => { + expect(run('order-mismatch', false)).toBe(0) +}) + +test('directive-empty-case: a case emptied by directive re-hoisting drops out', () => { + // The encoder produces this on its own, and it is not a corner: control-flow flattening runs at + // stage 4 and puts a function's leading `'use strict'` into a case like any other statement, + // then DirectivePlacementTransformer re-emits the directive at the top of the scope during + // Finalizing - leaving that case with nothing in it. Any flattened function whose body opens + // with a directive lands here, which is a large slice of real-world input. + // + // Found by appending a corpus input carrying a directive: before that, the whole matrix had + // none, and this pass declined on every cff cell of the new fixture at every column. + expect(run('directive-empty-case', true)).toBe(1) +}) + +test('all-cases-empty: a dispatch with nothing in any case removes the loop', () => { + // Hand-built rather than harvested - the encoder has no reason to flatten a body that holds + // only a directive - but the branch it covers is ours, and `replaceWithMultiple([])` is not a + // removal, so without this case the empty path would be untested code. + expect(run('all-cases-empty', true)).toBe(1) +}) diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.fix.js b/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.fix.js new file mode 100644 index 00000000..9aa2d181 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.fix.js @@ -0,0 +1 @@ +function f() {} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.js b/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.js new file mode 100644 index 00000000..e5069380 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/all-cases-empty.js @@ -0,0 +1,13 @@ +function f() { + var o = '1|0'.split('|'); + var i = 0; + while (true) { + switch (o[i++]) { + case '0': + continue; + case '1': + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.fix.js b/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.fix.js new file mode 100644 index 00000000..225f2942 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.fix.js @@ -0,0 +1,6 @@ +function f() { + a(); + b(); + c(); + return d(); +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.js b/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.js new file mode 100644 index 00000000..73cc9f39 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/baseline.js @@ -0,0 +1,20 @@ +function f() { + var o = '2|0|3|1'.split('|'); + var i = 0; + while (true) { + switch (o[i++]) { + case '0': + b(); + continue; + case '1': + return d(); + case '2': + a(); + continue; + case '3': + c(); + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/conditional-declaration.js b/test/visitor/obfuscator/unflatten-switch-dispatch/conditional-declaration.js new file mode 100644 index 00000000..60913c0e --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/conditional-declaration.js @@ -0,0 +1,17 @@ +function f(x) { + if (x) { + var o = '1|0'.split('|'); + var i = 0; + } + while (true) { + switch (o[i++]) { + case '0': + a(); + continue; + case '1': + b(); + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.fix.js b/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.fix.js new file mode 100644 index 00000000..e77e7765 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.fix.js @@ -0,0 +1,7 @@ +function f() { + 'use strict'; + + a(); + b(); + return c(); +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.js b/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.js new file mode 100644 index 00000000..8a96908d --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/directive-empty-case.js @@ -0,0 +1,21 @@ +function f() { + 'use strict'; + + var o = '3|0|1|2'.split('|'); + var i = 0; + while (true) { + switch (o[i++]) { + case '0': + a(); + continue; + case '1': + b(); + continue; + case '2': + return c(); + case '3': + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/fallthrough-case.js b/test/visitor/obfuscator/unflatten-switch-dispatch/fallthrough-case.js new file mode 100644 index 00000000..39361988 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/fallthrough-case.js @@ -0,0 +1,14 @@ +function f() { + var o = '0|1'.split('|'); + var i = 0; + while (true) { + switch (o[i++]) { + case '0': + a(); + case '1': + b(); + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.fix.js b/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.fix.js new file mode 100644 index 00000000..13d9cb50 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.fix.js @@ -0,0 +1,5 @@ +function f() { + a(); + b(); + return c(); +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.js b/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.js new file mode 100644 index 00000000..b13352be --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/merged-declaration.js @@ -0,0 +1,17 @@ +function f() { + var o = '1|0|2'.split('|'), + i = 0; + while (true) { + switch (o[i++]) { + case '0': + b(); + continue; + case '1': + a(); + continue; + case '2': + return c(); + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/no-increment.js b/test/visitor/obfuscator/unflatten-switch-dispatch/no-increment.js new file mode 100644 index 00000000..c4a44e37 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/no-increment.js @@ -0,0 +1,12 @@ +function f() { + var o = '0'.split('|'); + var i = 0; + while (true) { + switch (o[i]) { + case '0': + a(); + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/not-always-true.js b/test/visitor/obfuscator/unflatten-switch-dispatch/not-always-true.js new file mode 100644 index 00000000..6802b603 --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/not-always-true.js @@ -0,0 +1,15 @@ +function f(done) { + var o = '1|0'.split('|'); + var i = 0; + while (!done) { + switch (o[i++]) { + case '0': + a(); + continue; + case '1': + b(); + continue; + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unflatten-switch-dispatch/order-mismatch.js b/test/visitor/obfuscator/unflatten-switch-dispatch/order-mismatch.js new file mode 100644 index 00000000..b0524ffa --- /dev/null +++ b/test/visitor/obfuscator/unflatten-switch-dispatch/order-mismatch.js @@ -0,0 +1,17 @@ +function f() { + var o = '1|0'.split('|'); + var i = 0; + while (true) { + switch (o[i++]) { + case '0': + a(); + continue; + case '1': + b(); + continue; + case '2': + return c(); + } + break; + } +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env-pipeline.test.js b/test/visitor/obfuscator/unlock-env-pipeline.test.js new file mode 100644 index 00000000..e6400145 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env-pipeline.test.js @@ -0,0 +1,64 @@ +import { join } from 'path' +import { test } from 'vitest' +import traverse from '@babel/traverse' + +import normalizeStatements from '#visitor/obfuscator/normalize-statements' +import decodeStringArray from '#visitor/obfuscator/string-array' +import normalizeConverting from '#visitor/obfuscator/normalize-converting' +import parseControlFlowStorage from '#visitor/parse-control-flow-storage' +import calculateConstantExp from '#visitor/calculate-constant-exp' +import pruneIfBranch from '#visitor/prune-if-branch' +import { createUnflattenSwitchDispatch } from '#visitor/obfuscator/unflatten-switch-dispatch' +import unlockEnv from '#visitor/obfuscator/unlock-env' +import { getPipelineResult } from '../../helper.js' + +/** + * The one case that runs `unlock-env` on a tree the earlier passes have actually rewritten, in + * memory, rather than on a re-parse of their output. + * + * **This is not a duplicate of `unlock-env.test.js`.** Those cases pin the pass's matchers, and + * every one of them parses a pre-baked file — so no earlier pass has detached anything and the + * tree is pristine by construction. That is precisely the state a real pipeline never has. Run on + * one AST, this pass declined on every cell combining debug protection with control-flow + * flattening, deleting the calls controller while leaving the guard, and the output threw a + * `ReferenceError`. The corpus census and runtime equivalence both read clean beforehand, because + * both were measured across a re-parse. + * + * So the input here is **raw encoder output** and the passes run inside the test. Writing the + * intermediate to disk and reading it back would restore exactly the state that hid the defect. + * + * The input is `2.9.6` `all-on`, the smallest cell in the corpus that reproduces. The golden was + * written by a builder that refuses unless the result **runs** and reproduces the pre-obfuscation + * source's output, and unless no anti-tamper residue remains — a residue count alone cannot + * separate "stripped" from "deleted too much", since both drive it to zero. + * + * **This case is coupled to the passes ahead of it on purpose.** An upstream change that moves + * decoded output breaks it, and that is the point: the dependency is the thing under test. + */ +test('unlock-env strips debug protection on a tree the pipeline has rewritten', () => { + // The pipeline's own order, and the fixpoint the U4+U5 group needs: storage inlining re-opens + // Converting work that had already reported clean, so one sweep is not enough. + const group = (ast) => { + let previous = null + for (let round = 0; round < 8; round++) { + normalizeConverting(ast) + traverse(ast, calculateConstantExp) + traverse(ast, parseControlFlowStorage) + traverse(ast, calculateConstantExp) + traverse(ast, pruneIfBranch) + traverse( + ast, + createUnflattenSwitchDispatch(() => {}), + ) + const current = JSON.stringify(ast.program) + if (current === previous) break + previous = current + } + } + + getPipelineResult( + [normalizeStatements, decodeStringArray, group, unlockEnv], + true, + join(__dirname, 'unlock-env-pipeline', 'debug-protection-flattened'), + ) +}) diff --git a/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.fix.js b/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.fix.js new file mode 100644 index 00000000..07c7519b --- /dev/null +++ b/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.fix.js @@ -0,0 +1,9 @@ +function _0xdd673(_0x470766) { + return "hello, " + _0x470766 + '\x21'; +} +var _0x497fc4 = ["alpha", "beta", "gamma"]; +var _0x4d2827 = _0x497fc4.join('\x2d'); +var _0x5e7db6 = _0x4d2827.toUpperCase(); +console.log("console-channel"); +process.stdout.write(_0xdd673("world") + '\x0a'); +process.stdout.write(_0x4d2827 + '\x20' + _0x5e7db6 + '\x20' + _0x497fc4.length + '\x20' + "literal".charAt(0) + '\x0a'); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.js b/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.js new file mode 100644 index 00000000..572c954c --- /dev/null +++ b/test/visitor/obfuscator/unlock-env-pipeline/debug-protection-flattened.js @@ -0,0 +1 @@ +function _0xdd673(_0x470766){var _0x32221b={};_0x32221b['\x5a\x6d'+'\x68\x77'+'\x45']=function(_0x5dd8c7){return _0x5dd8c7();},_0x32221b['\x68\x55'+'\x64\x6f'+'\x52']=function(_0x562216,_0x1dbd46){return _0x562216===_0x1dbd46;},_0x32221b['\x4d\x4c'+'\x54\x7a'+'\x44']='\x50\x48'+'\x45\x5a'+'\x41',_0x32221b['\x4b\x71'+'\x57\x74'+'\x77']='\x79\x7a'+'\x55\x64'+'\x67',_0x32221b['\x53\x49'+'\x72\x6e'+'\x54']='\x62\x59'+'\x44\x64'+'\x78',_0x32221b['\x62\x78'+'\x72\x4a'+'\x6b']=function(_0xfd02ef,_0x506720){return _0xfd02ef+_0x506720;},_0x32221b['\x41\x73'+'\x77\x5a'+'\x54']='\x64\x65'+'\x62\x75',_0x32221b['\x53\x72'+'\x45\x5a'+'\x56']='\x67\x67'+'\x65\x72',_0x32221b['\x69\x6d'+'\x65\x78'+'\x4c']='\x73\x74'+'\x61\x74'+'\x65\x4f'+'\x62\x6a'+'\x65\x63'+'\x74',_0x32221b['\x63\x52'+'\x4a\x52'+'\x74']=function(_0x8ffeda,_0x327580){return _0x8ffeda(_0x327580);},_0x32221b['\x75\x47'+'\x52\x4b'+'\x55']=function(_0x5b0a17,_0x3b9e58){return _0x5b0a17===_0x3b9e58;},_0x32221b['\x76\x66'+'\x57\x63'+'\x50']='\x42\x76'+'\x7a\x48'+'\x5a',_0x32221b['\x4e\x62'+'\x4c\x4c'+'\x56']='\x79\x52'+'\x47\x53'+'\x50',_0x32221b['\x73\x43'+'\x4a\x6e'+'\x5a']=function(_0x11ee08,_0x37c07c){return _0x11ee08!==_0x37c07c;},_0x32221b['\x71\x79'+'\x61\x6e'+'\x76']='\x47\x65'+'\x68\x49'+'\x58',_0x32221b['\x6d\x4d'+'\x48\x4b'+'\x6d']=function(_0x3774d9,_0x4aadeb){return _0x3774d9(_0x4aadeb);},_0x32221b['\x74\x43'+'\x41\x64'+'\x44']='\x61\x63'+'\x74\x69'+'\x6f\x6e',_0x32221b['\x6c\x41'+'\x6a\x55'+'\x4d']='\x54\x78'+'\x70\x74'+'\x61',_0x32221b['\x51\x75'+'\x53\x65'+'\x52']='\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x20\x2a'+'\x5c\x28'+'\x20\x2a'+'\x5c\x29',_0x32221b['\x6b\x70'+'\x62\x56'+'\x73']='\x5c\x2b'+'\x5c\x2b'+'\x20\x2a'+'\x28\x3f'+'\x3a\x5b'+'\x61\x2d'+'\x7a\x41'+'\x2d\x5a'+'\x5f\x24'+'\x5d\x5b'+'\x30\x2d'+'\x39\x61'+'\x2d\x7a'+'\x41\x2d'+'\x5a\x5f'+'\x24\x5d'+'\x2a\x29',_0x32221b['\x75\x71'+'\x71\x55'+'\x4b']='\x69\x6e'+'\x69\x74',_0x32221b['\x49\x56'+'\x71\x45'+'\x4c']=function(_0x3a8c14,_0x1e3e17){return _0x3a8c14+_0x1e3e17;},_0x32221b['\x44\x6e'+'\x55\x71'+'\x51']='\x63\x68'+'\x61\x69'+'\x6e',_0x32221b['\x4b\x6d'+'\x68\x62'+'\x66']=function(_0x4cd7b8,_0x533d94){return _0x4cd7b8+_0x533d94;},_0x32221b['\x79\x74'+'\x4c\x6f'+'\x65']='\x69\x6e'+'\x70\x75'+'\x74',_0x32221b['\x68\x6c'+'\x6f\x70'+'\x53']='\x6d\x4f'+'\x4b\x47'+'\x67',_0x32221b['\x4f\x69'+'\x70\x62'+'\x51']='\x6a\x75'+'\x67\x56'+'\x69',_0x32221b['\x66\x43'+'\x78\x4a'+'\x75']=function(_0x5ac93d,_0x300c2e){return _0x5ac93d(_0x300c2e);},_0x32221b['\x65\x4e'+'\x62\x70'+'\x79']='\x6b\x49'+'\x78\x41'+'\x52',_0x32221b['\x41\x45'+'\x56\x68'+'\x64']='\x43\x66'+'\x70\x58'+'\x47',_0x32221b['\x67\x41'+'\x56\x48'+'\x6f']=function(_0x24f833,_0x1ebbf4){return _0x24f833(_0x1ebbf4);},_0x32221b['\x46\x66'+'\x74\x6b'+'\x71']='\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x28'+'\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x28\x29'+'\x20',_0x32221b['\x4d\x4b'+'\x6b\x73'+'\x66']='\x7b\x7d'+'\x2e\x63'+'\x6f\x6e'+'\x73\x74'+'\x72\x75'+'\x63\x74'+'\x6f\x72'+'\x28\x22'+'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x74'+'\x68\x69'+'\x73\x22'+'\x29\x28'+'\x20\x29',_0x32221b['\x48\x4e'+'\x75\x5a'+'\x4f']=function(_0x2a1a44,_0x145fd5){return _0x2a1a44===_0x145fd5;},_0x32221b['\x58\x78'+'\x73\x45'+'\x67']='\x61\x68'+'\x65\x4c'+'\x42',_0x32221b['\x62\x4c'+'\x4a\x74'+'\x70']='\x70\x6d'+'\x43\x75'+'\x6c',_0x32221b['\x76\x44'+'\x6c\x4e'+'\x71']=function(_0x5c95a1,_0x48b492,_0x127e39){return _0x5c95a1(_0x48b492,_0x127e39);},_0x32221b['\x47\x46'+'\x4d\x79'+'\x76']='\x32\x7c'+'\x30\x7c'+'\x34\x7c'+'\x31\x7c'+'\x35\x7c'+'\x33',_0x32221b['\x55\x4a'+'\x41\x49'+'\x79']=function(_0x1a2fea,_0x3cbc2a){return _0x1a2fea!==_0x3cbc2a;},_0x32221b['\x6d\x62'+'\x6e\x64'+'\x58']='\x4e\x57'+'\x45\x54'+'\x4f',_0x32221b['\x56\x5a'+'\x57\x70'+'\x7a']='\x64\x50'+'\x4f\x67'+'\x6a',_0x32221b['\x6f\x72'+'\x68\x7a'+'\x66']='\x58\x6d'+'\x77\x65'+'\x59',_0x32221b['\x54\x66'+'\x6f\x42'+'\x74']=function(_0x3ea181,_0x30ca8d){return _0x3ea181===_0x30ca8d;},_0x32221b['\x53\x6e'+'\x67\x50'+'\x45']='\x79\x6b'+'\x4e\x59'+'\x73',_0x32221b['\x72\x41'+'\x6f\x51'+'\x55']='\x4c\x44'+'\x48\x6a'+'\x61',_0x32221b['\x49\x77'+'\x6f\x6d'+'\x79']='\x4f\x55'+'\x45\x4d'+'\x4d',_0x32221b['\x65\x52'+'\x59\x48'+'\x75']='\x4b\x61'+'\x77\x4c'+'\x47',_0x32221b['\x6f\x54'+'\x43\x74'+'\x6f']=function(_0x4b9784,_0x472b1d,_0x592225){return _0x4b9784(_0x472b1d,_0x592225);},_0x32221b['\x6c\x48'+'\x4f\x69'+'\x67']=function(_0x172472,_0x16ae21){return _0x172472+_0x16ae21;},_0x32221b['\x57\x7a'+'\x71\x58'+'\x62']=function(_0x15ffb6,_0x431474){return _0x15ffb6+_0x431474;},_0x32221b['\x47\x49'+'\x65\x42'+'\x4a']=function(_0x4cca76,_0x3b1856){return _0x4cca76(_0x3b1856);},_0x32221b['\x42\x75'+'\x63\x70'+'\x4c']=function(_0x43a9a7,_0x35c992){return _0x43a9a7!==_0x35c992;},_0x32221b['\x6e\x64'+'\x49\x64'+'\x6d']='\x6e\x6a'+'\x77\x44'+'\x59',_0x32221b['\x62\x79'+'\x7a\x5a'+'\x71']='\x43\x6b'+'\x4f\x67'+'\x70',_0x32221b['\x6a\x46'+'\x6f\x51'+'\x41']=function(_0x3ac94a,_0x4fbefc){return _0x3ac94a(_0x4fbefc);},_0x32221b['\x51\x6f'+'\x6f\x75'+'\x52']='\x74\x4f'+'\x4e\x6c'+'\x79',_0x32221b['\x55\x71'+'\x6a\x72'+'\x50']='\x6e\x5a'+'\x43\x44'+'\x46',_0x32221b['\x62\x59'+'\x44\x64'+'\x71']='\x54\x51'+'\x52\x63'+'\x61',_0x32221b['\x57\x77'+'\x49\x63'+'\x71']=function(_0x21cb63){return _0x21cb63();},_0x32221b['\x61\x53'+'\x4e\x48'+'\x75']='\x6c\x6f'+'\x67',_0x32221b['\x77\x52'+'\x54\x78'+'\x4d']='\x77\x61'+'\x72\x6e',_0x32221b['\x49\x54'+'\x4c\x68'+'\x45']='\x69\x6e'+'\x66\x6f',_0x32221b['\x61\x69'+'\x63\x6b'+'\x58']='\x65\x72'+'\x72\x6f'+'\x72',_0x32221b['\x4d\x51'+'\x41\x43'+'\x79']='\x65\x78'+'\x63\x65'+'\x70\x74'+'\x69\x6f'+'\x6e',_0x32221b['\x4e\x75'+'\x48\x78'+'\x52']='\x74\x61'+'\x62\x6c'+'\x65',_0x32221b['\x6d\x54'+'\x55\x70'+'\x55']='\x74\x72'+'\x61\x63'+'\x65',_0x32221b['\x48\x71'+'\x79\x49'+'\x67']=function(_0x400e6b,_0x2c5465){return _0x400e6b<_0x2c5465;},_0x32221b['\x65\x55'+'\x69\x6c'+'\x78']='\x72\x75'+'\x4e\x52'+'\x71',_0x32221b['\x55\x6b'+'\x75\x4c'+'\x53']='\x32\x7c'+'\x33\x7c'+'\x31\x7c'+'\x30\x7c'+'\x35\x7c'+'\x34',_0x32221b['\x41\x74'+'\x5a\x54'+'\x48']=function(_0x55f9cd,_0x1f214e,_0x2f1d4b){return _0x55f9cd(_0x1f214e,_0x2f1d4b);},_0x32221b['\x46\x54'+'\x7a\x53'+'\x4c']=function(_0x457cdf,_0x2507f8){return _0x457cdf+_0x2507f8;},_0x32221b['\x66\x65'+'\x4a\x69'+'\x49']='\x68\x65'+'\x6c\x6c'+'\x6f\x2c'+'\x20';var _0x2d4ba5=_0x32221b,_0x2cc314=function(){var _0xa57717={};_0xa57717['\x57\x72'+'\x6b\x75'+'\x6c']=function(_0x4b2aff){return _0x2d4ba5['\x5a\x6d'+'\x68\x77'+'\x45'](_0x4b2aff);},_0xa57717['\x52\x52'+'\x49\x67'+'\x76']=function(_0xd24a69,_0x5af1c0){return _0x2d4ba5['\x68\x55'+'\x64\x6f'+'\x52'](_0xd24a69,_0x5af1c0);},_0xa57717['\x46\x50'+'\x46\x71'+'\x6c']=_0x2d4ba5['\x4d\x4c'+'\x54\x7a'+'\x44'],_0xa57717['\x55\x76'+'\x44\x46'+'\x52']=_0x2d4ba5['\x4b\x71'+'\x57\x74'+'\x77'],_0xa57717['\x78\x59'+'\x6e\x54'+'\x67']=_0x2d4ba5['\x53\x49'+'\x72\x6e'+'\x54'],_0xa57717['\x44\x69'+'\x44\x5a'+'\x4f']=function(_0x401b22,_0x4e4e4e){return _0x2d4ba5['\x62\x78'+'\x72\x4a'+'\x6b'](_0x401b22,_0x4e4e4e);},_0xa57717['\x4b\x66'+'\x57\x6d'+'\x76']=_0x2d4ba5['\x41\x73'+'\x77\x5a'+'\x54'],_0xa57717['\x44\x48'+'\x47\x64'+'\x6c']=_0x2d4ba5['\x53\x72'+'\x45\x5a'+'\x56'],_0xa57717['\x66\x44'+'\x4c\x5a'+'\x41']=_0x2d4ba5['\x69\x6d'+'\x65\x78'+'\x4c'],_0xa57717['\x5a\x73'+'\x77\x6f'+'\x67']=function(_0x96d85f,_0x1bbd97){return _0x2d4ba5['\x63\x52'+'\x4a\x52'+'\x74'](_0x96d85f,_0x1bbd97);},_0xa57717['\x6f\x69'+'\x68\x79'+'\x47']=function(_0x530b64,_0x3a4655){return _0x2d4ba5['\x75\x47'+'\x52\x4b'+'\x55'](_0x530b64,_0x3a4655);},_0xa57717['\x6d\x52'+'\x78\x78'+'\x70']=_0x2d4ba5['\x76\x66'+'\x57\x63'+'\x50'],_0xa57717['\x4c\x6d'+'\x67\x52'+'\x68']=_0x2d4ba5['\x4e\x62'+'\x4c\x4c'+'\x56'];var _0x2acd06=_0xa57717;if(_0x2d4ba5['\x73\x43'+'\x4a\x6e'+'\x5a'](_0x2d4ba5['\x71\x79'+'\x61\x6e'+'\x76'],_0x2d4ba5['\x71\x79'+'\x61\x6e'+'\x76'])){function _0x30c460(){return!![];}}else{var _0x20dc8b=!![];return function(_0x175fb0,_0x5d1421){var _0x106e8d={};_0x106e8d['\x6f\x57'+'\x75\x45'+'\x59']=function(_0x59c331,_0x5f1f2e){return _0x2acd06['\x44\x69'+'\x44\x5a'+'\x4f'](_0x59c331,_0x5f1f2e);},_0x106e8d['\x50\x70'+'\x48\x59'+'\x51']=_0x2acd06['\x4b\x66'+'\x57\x6d'+'\x76'],_0x106e8d['\x47\x6b'+'\x43\x46'+'\x51']=_0x2acd06['\x44\x48'+'\x47\x64'+'\x6c'],_0x106e8d['\x6f\x69'+'\x62\x78'+'\x74']=_0x2acd06['\x66\x44'+'\x4c\x5a'+'\x41'],_0x106e8d['\x76\x5a'+'\x57\x68'+'\x65']=function(_0x50186d,_0x27d3b7){return _0x2acd06['\x5a\x73'+'\x77\x6f'+'\x67'](_0x50186d,_0x27d3b7);};var _0xd8f9fe=_0x106e8d;if(_0x2acd06['\x6f\x69'+'\x68\x79'+'\x47'](_0x2acd06['\x6d\x52'+'\x78\x78'+'\x70'],_0x2acd06['\x4c\x6d'+'\x67\x52'+'\x68'])){function _0x454750(){mnEPoz['\x57\x72'+'\x6b\x75'+'\x6c'](_0x5005e4);}}else{var _0x9fc2f4=_0x20dc8b?function(){if(_0x2acd06['\x52\x52'+'\x49\x67'+'\x76'](_0x2acd06['\x46\x50'+'\x46\x71'+'\x6c'],_0x2acd06['\x55\x76'+'\x44\x46'+'\x52'])){function _0x3702e6(){(function(){return![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](dWVfyv['\x6f\x57'+'\x75\x45'+'\x59'](dWVfyv['\x50\x70'+'\x48\x59'+'\x51'],dWVfyv['\x47\x6b'+'\x43\x46'+'\x51']))['\x61\x70'+'\x70\x6c'+'\x79'](dWVfyv['\x6f\x69'+'\x62\x78'+'\x74']));}}else{if(_0x5d1421){if(_0x2acd06['\x52\x52'+'\x49\x67'+'\x76'](_0x2acd06['\x78\x59'+'\x6e\x54'+'\x67'],_0x2acd06['\x78\x59'+'\x6e\x54'+'\x67'])){var _0x58bb48=_0x5d1421['\x61\x70'+'\x70\x6c'+'\x79'](_0x175fb0,arguments);return _0x5d1421=null,_0x58bb48;}else{function _0x133b3e(){dWVfyv['\x76\x5a'+'\x57\x68'+'\x65'](_0xb850e7,'\x30');}}}}}:function(){};return _0x20dc8b=![],_0x9fc2f4;}};}}();(function(){var _0x24210c={};_0x24210c['\x52\x43'+'\x73\x69'+'\x58']=function(_0x3c2ba2,_0x599651){return _0x2d4ba5['\x67\x41'+'\x56\x48'+'\x6f'](_0x3c2ba2,_0x599651);},_0x24210c['\x79\x61'+'\x61\x5a'+'\x4b']=function(_0x58e4f4,_0x3c727a){return _0x2d4ba5['\x4b\x6d'+'\x68\x62'+'\x66'](_0x58e4f4,_0x3c727a);},_0x24210c['\x59\x42'+'\x59\x58'+'\x6f']=_0x2d4ba5['\x46\x66'+'\x74\x6b'+'\x71'],_0x24210c['\x79\x6e'+'\x4b\x52'+'\x6c']=_0x2d4ba5['\x4d\x4b'+'\x6b\x73'+'\x66'];var _0x2dc86c=_0x24210c;if(_0x2d4ba5['\x48\x4e'+'\x75\x5a'+'\x4f'](_0x2d4ba5['\x58\x78'+'\x73\x45'+'\x67'],_0x2d4ba5['\x62\x4c'+'\x4a\x74'+'\x70'])){function _0x5526d5(){return![];}}else _0x2d4ba5['\x76\x44'+'\x6c\x4e'+'\x71'](_0x2cc314,this,function(){var _0x5b29f3={};_0x5b29f3['\x44\x55'+'\x42\x73'+'\x55']=function(_0x409354,_0x16edd8){return _0x2d4ba5['\x6d\x4d'+'\x48\x4b'+'\x6d'](_0x409354,_0x16edd8);},_0x5b29f3['\x52\x6a'+'\x46\x72'+'\x6f']=function(_0x1cb180,_0x5482a7){return _0x2d4ba5['\x62\x78'+'\x72\x4a'+'\x6b'](_0x1cb180,_0x5482a7);},_0x5b29f3['\x73\x47'+'\x79\x66'+'\x4f']=_0x2d4ba5['\x41\x73'+'\x77\x5a'+'\x54'],_0x5b29f3['\x65\x76'+'\x6c\x5a'+'\x6c']=_0x2d4ba5['\x53\x72'+'\x45\x5a'+'\x56'],_0x5b29f3['\x53\x78'+'\x71\x5a'+'\x55']=_0x2d4ba5['\x74\x43'+'\x41\x64'+'\x44'];var _0x550c88=_0x5b29f3;if(_0x2d4ba5['\x75\x47'+'\x52\x4b'+'\x55'](_0x2d4ba5['\x6c\x41'+'\x6a\x55'+'\x4d'],_0x2d4ba5['\x6c\x41'+'\x6a\x55'+'\x4d'])){var _0x27d8f6=new RegExp(_0x2d4ba5['\x51\x75'+'\x53\x65'+'\x52']),_0x2946ec=new RegExp(_0x2d4ba5['\x6b\x70'+'\x62\x56'+'\x73'],'\x69'),_0x5ec3f3=_0x2d4ba5['\x6d\x4d'+'\x48\x4b'+'\x6d'](_0x22f76a,_0x2d4ba5['\x75\x71'+'\x71\x55'+'\x4b']);if(!_0x27d8f6['\x74\x65'+'\x73\x74'](_0x2d4ba5['\x49\x56'+'\x71\x45'+'\x4c'](_0x5ec3f3,_0x2d4ba5['\x44\x6e'+'\x55\x71'+'\x51']))||!_0x2946ec['\x74\x65'+'\x73\x74'](_0x2d4ba5['\x4b\x6d'+'\x68\x62'+'\x66'](_0x5ec3f3,_0x2d4ba5['\x79\x74'+'\x4c\x6f'+'\x65']))){if(_0x2d4ba5['\x73\x43'+'\x4a\x6e'+'\x5a'](_0x2d4ba5['\x68\x6c'+'\x6f\x70'+'\x53'],_0x2d4ba5['\x4f\x69'+'\x70\x62'+'\x51']))_0x2d4ba5['\x66\x43'+'\x78\x4a'+'\x75'](_0x5ec3f3,'\x30');else{function _0x310a62(){VfWnYl['\x44\x55'+'\x42\x73'+'\x55'](_0xf7d328,-0x1*-0x3ad+0x170+0x77*-0xb);}}}else{if(_0x2d4ba5['\x75\x47'+'\x52\x4b'+'\x55'](_0x2d4ba5['\x65\x4e'+'\x62\x70'+'\x79'],_0x2d4ba5['\x41\x45'+'\x56\x68'+'\x64'])){function _0x1bab68(){_0x226a8d=vermef['\x52\x43'+'\x73\x69'+'\x58'](_0x28a491,vermef['\x79\x61'+'\x61\x5a'+'\x4b'](vermef['\x79\x61'+'\x61\x5a'+'\x4b'](vermef['\x59\x42'+'\x59\x58'+'\x6f'],vermef['\x79\x6e'+'\x4b\x52'+'\x6c']),'\x29\x3b'))();}}else _0x2d4ba5['\x5a\x6d'+'\x68\x77'+'\x45'](_0x22f76a);}}else{function _0x249ae3(){(function(){return!![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](VfWnYl['\x52\x6a'+'\x46\x72'+'\x6f'](VfWnYl['\x73\x47'+'\x79\x66'+'\x4f'],VfWnYl['\x65\x76'+'\x6c\x5a'+'\x6c']))['\x63\x61'+'\x6c\x6c'](VfWnYl['\x53\x78'+'\x71\x5a'+'\x55']));}}})();}());var _0x398e51=function(){var _0x3a6921={};_0x3a6921['\x6c\x48'+'\x7a\x49'+'\x5a']=function(_0x4c04fe,_0x5c7161){return _0x2d4ba5['\x55\x4a'+'\x41\x49'+'\x79'](_0x4c04fe,_0x5c7161);},_0x3a6921['\x67\x46'+'\x6d\x6b'+'\x59']=_0x2d4ba5['\x6d\x62'+'\x6e\x64'+'\x58'],_0x3a6921['\x6f\x52'+'\x51\x50'+'\x61']=_0x2d4ba5['\x56\x5a'+'\x57\x70'+'\x7a'],_0x3a6921['\x4f\x56'+'\x51\x66'+'\x6f']=_0x2d4ba5['\x6f\x72'+'\x68\x7a'+'\x66'],_0x3a6921['\x55\x50'+'\x6f\x45'+'\x69']=function(_0x262af3,_0x4f9992){return _0x2d4ba5['\x54\x66'+'\x6f\x42'+'\x74'](_0x262af3,_0x4f9992);},_0x3a6921['\x43\x76'+'\x68\x77'+'\x7a']=_0x2d4ba5['\x53\x6e'+'\x67\x50'+'\x45'],_0x3a6921['\x65\x44'+'\x79\x6c'+'\x74']=_0x2d4ba5['\x72\x41'+'\x6f\x51'+'\x55'];var _0x232d9a=_0x3a6921;if(_0x2d4ba5['\x55\x4a'+'\x41\x49'+'\x79'](_0x2d4ba5['\x49\x77'+'\x6f\x6d'+'\x79'],_0x2d4ba5['\x65\x52'+'\x59\x48'+'\x75'])){var _0xac2307=!![];return function(_0x16942f,_0x584bd5){var _0x32e704={};_0x32e704['\x43\x75'+'\x78\x6f'+'\x70']=function(_0x412403,_0x18baa8){return _0x232d9a['\x6c\x48'+'\x7a\x49'+'\x5a'](_0x412403,_0x18baa8);},_0x32e704['\x73\x79'+'\x62\x64'+'\x69']=_0x232d9a['\x67\x46'+'\x6d\x6b'+'\x59'],_0x32e704['\x54\x7a'+'\x53\x51'+'\x54']=_0x232d9a['\x6f\x52'+'\x51\x50'+'\x61'],_0x32e704['\x62\x43'+'\x62\x48'+'\x42']=_0x232d9a['\x4f\x56'+'\x51\x66'+'\x6f'];var _0x544277=_0x32e704;if(_0x232d9a['\x55\x50'+'\x6f\x45'+'\x69'](_0x232d9a['\x43\x76'+'\x68\x77'+'\x7a'],_0x232d9a['\x65\x44'+'\x79\x6c'+'\x74'])){function _0x4f0c74(){_0x529733=_0x1ca417;}}else{var _0x453518=_0xac2307?function(){if(_0x544277['\x43\x75'+'\x78\x6f'+'\x70'](_0x544277['\x73\x79'+'\x62\x64'+'\x69'],_0x544277['\x73\x79'+'\x62\x64'+'\x69'])){function _0x2b6d72(){var _0x15cb5c=_0x272a83?function(){if(_0x3e89cb){var _0x2a854b=_0x4df6ab['\x61\x70'+'\x70\x6c'+'\x79'](_0x2fa593,arguments);return _0x3f331c=null,_0x2a854b;}}:function(){};return _0x5567af=![],_0x15cb5c;}}else{if(_0x584bd5){if(_0x544277['\x43\x75'+'\x78\x6f'+'\x70'](_0x544277['\x54\x7a'+'\x53\x51'+'\x54'],_0x544277['\x62\x43'+'\x62\x48'+'\x42'])){var _0x3a92ab=_0x584bd5['\x61\x70'+'\x70\x6c'+'\x79'](_0x16942f,arguments);return _0x584bd5=null,_0x3a92ab;}else{function _0x510c42(){var _0x2dc69b=_0x59b035?function(){if(_0x2d4ff1){var _0x59556c=_0x117fcb['\x61\x70'+'\x70\x6c'+'\x79'](_0x2d848e,arguments);return _0x19fe16=null,_0x59556c;}}:function(){};return _0x1bc058=![],_0x2dc69b;}}}}}:function(){};return _0xac2307=![],_0x453518;}};}else{function _0x14b71f(){var _0x1d24af=_0x2d4ba5['\x47\x46'+'\x4d\x79'+'\x76']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0x51e183=-0xbfb+0x865+0x36*0x11;while(!![]){switch(_0x1d24af[_0x51e183++]){case'\x30':var _0x33fc12=_0x4c86ce[_0x5db159];continue;case'\x31':_0x5ee2fc['\x5f\x5f'+'\x70\x72'+'\x6f\x74'+'\x6f\x5f'+'\x5f']=_0x1d85e0['\x62\x69'+'\x6e\x64'](_0x4154d2);continue;case'\x32':var _0x5ee2fc=_0x2ceecb['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72']['\x70\x72'+'\x6f\x74'+'\x6f\x74'+'\x79\x70'+'\x65']['\x62\x69'+'\x6e\x64'](_0xf04555);continue;case'\x33':_0x2fe054[_0x33fc12]=_0x5ee2fc;continue;case'\x34':var _0x8bfc90=_0x93cb71[_0x33fc12]||_0x5ee2fc;continue;case'\x35':_0x5ee2fc['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']=_0x8bfc90['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']['\x62\x69'+'\x6e\x64'](_0x8bfc90);continue;}break;}}}}(),_0x3e69a7=_0x2d4ba5['\x41\x74'+'\x5a\x54'+'\x48'](_0x398e51,this,function(){var _0x1d4d36={};_0x1d4d36['\x79\x5a'+'\x77\x79'+'\x50']=_0x2d4ba5['\x51\x75'+'\x53\x65'+'\x52'],_0x1d4d36['\x78\x53'+'\x75\x4c'+'\x69']=_0x2d4ba5['\x6b\x70'+'\x62\x56'+'\x73'],_0x1d4d36['\x70\x70'+'\x76\x6d'+'\x71']=function(_0x4e7cbf,_0x192fdb){return _0x2d4ba5['\x67\x41'+'\x56\x48'+'\x6f'](_0x4e7cbf,_0x192fdb);},_0x1d4d36['\x45\x61'+'\x59\x50'+'\x6c']=_0x2d4ba5['\x75\x71'+'\x71\x55'+'\x4b'],_0x1d4d36['\x77\x71'+'\x55\x56'+'\x57']=function(_0x24e69d,_0x91a807){return _0x2d4ba5['\x4b\x6d'+'\x68\x62'+'\x66'](_0x24e69d,_0x91a807);},_0x1d4d36['\x67\x43'+'\x66\x54'+'\x6f']=_0x2d4ba5['\x44\x6e'+'\x55\x71'+'\x51'],_0x1d4d36['\x48\x78'+'\x51\x46'+'\x43']=_0x2d4ba5['\x79\x74'+'\x4c\x6f'+'\x65'],_0x1d4d36['\x4f\x4b'+'\x64\x4a'+'\x70']=function(_0x1b9f7e,_0xfac220){return _0x2d4ba5['\x67\x41'+'\x56\x48'+'\x6f'](_0x1b9f7e,_0xfac220);},_0x1d4d36['\x6b\x43'+'\x4a\x41'+'\x6e']=function(_0x3178d4){return _0x2d4ba5['\x5a\x6d'+'\x68\x77'+'\x45'](_0x3178d4);},_0x1d4d36['\x6f\x71'+'\x54\x71'+'\x43']=function(_0x31ec40,_0x51016b,_0x306d3c){return _0x2d4ba5['\x6f\x54'+'\x43\x74'+'\x6f'](_0x31ec40,_0x51016b,_0x306d3c);},_0x1d4d36['\x47\x7a'+'\x75\x69'+'\x68']=function(_0x2e6285,_0x1043de){return _0x2d4ba5['\x6c\x48'+'\x4f\x69'+'\x67'](_0x2e6285,_0x1043de);},_0x1d4d36['\x6e\x51'+'\x41\x55'+'\x61']=function(_0x2d2351,_0x17d130){return _0x2d4ba5['\x57\x7a'+'\x71\x58'+'\x62'](_0x2d2351,_0x17d130);},_0x1d4d36['\x66\x56'+'\x4a\x64'+'\x74']=function(_0x48dc3d,_0x5f060f){return _0x2d4ba5['\x47\x49'+'\x65\x42'+'\x4a'](_0x48dc3d,_0x5f060f);},_0x1d4d36['\x72\x6b'+'\x67\x6a'+'\x72']=function(_0x3ee688){return _0x2d4ba5['\x5a\x6d'+'\x68\x77'+'\x45'](_0x3ee688);},_0x1d4d36['\x75\x6f'+'\x41\x49'+'\x75']=function(_0x3ee64d,_0x49e325){return _0x2d4ba5['\x42\x75'+'\x63\x70'+'\x4c'](_0x3ee64d,_0x49e325);},_0x1d4d36['\x52\x52'+'\x47\x56'+'\x64']=_0x2d4ba5['\x6e\x64'+'\x49\x64'+'\x6d'],_0x1d4d36['\x4e\x41'+'\x69\x61'+'\x56']=function(_0x190747,_0x1a68d1){return _0x2d4ba5['\x42\x75'+'\x63\x70'+'\x4c'](_0x190747,_0x1a68d1);},_0x1d4d36['\x67\x5a'+'\x47\x62'+'\x48']=_0x2d4ba5['\x62\x79'+'\x7a\x5a'+'\x71'],_0x1d4d36['\x47\x75'+'\x4c\x54'+'\x57']=function(_0x1fb724,_0x3474d8){return _0x2d4ba5['\x6a\x46'+'\x6f\x51'+'\x41'](_0x1fb724,_0x3474d8);},_0x1d4d36['\x63\x63'+'\x73\x4d'+'\x75']=function(_0x293750,_0x1dff8b){return _0x2d4ba5['\x57\x7a'+'\x71\x58'+'\x62'](_0x293750,_0x1dff8b);},_0x1d4d36['\x58\x68'+'\x74\x4c'+'\x4b']=function(_0x5c6d25,_0x2f6bad){return _0x2d4ba5['\x57\x7a'+'\x71\x58'+'\x62'](_0x5c6d25,_0x2f6bad);},_0x1d4d36['\x44\x63'+'\x6c\x71'+'\x44']=_0x2d4ba5['\x46\x66'+'\x74\x6b'+'\x71'],_0x1d4d36['\x75\x4a'+'\x43\x4e'+'\x55']=_0x2d4ba5['\x4d\x4b'+'\x6b\x73'+'\x66'],_0x1d4d36['\x7a\x75'+'\x77\x62'+'\x73']=_0x2d4ba5['\x51\x6f'+'\x6f\x75'+'\x52'];var _0x23954b=_0x1d4d36;if(_0x2d4ba5['\x54\x66'+'\x6f\x42'+'\x74'](_0x2d4ba5['\x55\x71'+'\x6a\x72'+'\x50'],_0x2d4ba5['\x62\x59'+'\x44\x64'+'\x71'])){function _0x291cf4(){var _0x495554=_0x2d3710['\x61\x70'+'\x70\x6c'+'\x79'](_0x3c2fde,arguments);return _0x19ae01=null,_0x495554;}}else{var _0x266f35=function(){if(_0x23954b['\x75\x6f'+'\x41\x49'+'\x75'](_0x23954b['\x52\x52'+'\x47\x56'+'\x64'],_0x23954b['\x52\x52'+'\x47\x56'+'\x64'])){function _0x320398(){var _0x4b1bc9={};_0x4b1bc9['\x6a\x69'+'\x59\x58'+'\x65']=wjXFXH['\x79\x5a'+'\x77\x79'+'\x50'],_0x4b1bc9['\x4c\x73'+'\x46\x48'+'\x44']=wjXFXH['\x78\x53'+'\x75\x4c'+'\x69'],_0x4b1bc9['\x69\x61'+'\x50\x59'+'\x72']=function(_0x2aabe9,_0x1f5a76){return wjXFXH['\x70\x70'+'\x76\x6d'+'\x71'](_0x2aabe9,_0x1f5a76);},_0x4b1bc9['\x6e\x42'+'\x77\x52'+'\x51']=wjXFXH['\x45\x61'+'\x59\x50'+'\x6c'],_0x4b1bc9['\x47\x69'+'\x4b\x4f'+'\x71']=function(_0x516e14,_0x399bbe){return wjXFXH['\x77\x71'+'\x55\x56'+'\x57'](_0x516e14,_0x399bbe);},_0x4b1bc9['\x6b\x47'+'\x54\x68'+'\x44']=wjXFXH['\x67\x43'+'\x66\x54'+'\x6f'],_0x4b1bc9['\x63\x7a'+'\x41\x6a'+'\x52']=wjXFXH['\x48\x78'+'\x51\x46'+'\x43'],_0x4b1bc9['\x4e\x54'+'\x46\x4a'+'\x62']=function(_0xaa43eb,_0x27e6ec){return wjXFXH['\x4f\x4b'+'\x64\x4a'+'\x70'](_0xaa43eb,_0x27e6ec);},_0x4b1bc9['\x6d\x4f'+'\x76\x4a'+'\x70']=function(_0x33b05a){return wjXFXH['\x6b\x43'+'\x4a\x41'+'\x6e'](_0x33b05a);};var _0x1b0245=_0x4b1bc9;wjXFXH['\x6f\x71'+'\x54\x71'+'\x43'](_0x3b3422,this,function(){var _0x141647=new _0x328a4a(_0x1b0245['\x6a\x69'+'\x59\x58'+'\x65']),_0x2c9346=new _0x2aaa5d(_0x1b0245['\x4c\x73'+'\x46\x48'+'\x44'],'\x69'),_0x3bc15=_0x1b0245['\x69\x61'+'\x50\x59'+'\x72'](_0x5ce358,_0x1b0245['\x6e\x42'+'\x77\x52'+'\x51']);!_0x141647['\x74\x65'+'\x73\x74'](_0x1b0245['\x47\x69'+'\x4b\x4f'+'\x71'](_0x3bc15,_0x1b0245['\x6b\x47'+'\x54\x68'+'\x44']))||!_0x2c9346['\x74\x65'+'\x73\x74'](_0x1b0245['\x47\x69'+'\x4b\x4f'+'\x71'](_0x3bc15,_0x1b0245['\x63\x7a'+'\x41\x6a'+'\x52']))?_0x1b0245['\x4e\x54'+'\x46\x4a'+'\x62'](_0x3bc15,'\x30'):_0x1b0245['\x6d\x4f'+'\x76\x4a'+'\x70'](_0x55798d);})();}}else{var _0x313dcc;try{if(_0x23954b['\x4e\x41'+'\x69\x61'+'\x56'](_0x23954b['\x67\x5a'+'\x47\x62'+'\x48'],_0x23954b['\x67\x5a'+'\x47\x62'+'\x48'])){function _0x636089(){if(_0x18faf1){var _0x26eff3=_0x41aec5['\x61\x70'+'\x70\x6c'+'\x79'](_0x215381,arguments);return _0x1fa401=null,_0x26eff3;}}}else _0x313dcc=_0x23954b['\x47\x75'+'\x4c\x54'+'\x57'](Function,_0x23954b['\x63\x63'+'\x73\x4d'+'\x75'](_0x23954b['\x58\x68'+'\x74\x4c'+'\x4b'](_0x23954b['\x44\x63'+'\x6c\x71'+'\x44'],_0x23954b['\x75\x4a'+'\x43\x4e'+'\x55']),'\x29\x3b'))();}catch(_0x45da55){if(_0x23954b['\x4e\x41'+'\x69\x61'+'\x56'](_0x23954b['\x7a\x75'+'\x77\x62'+'\x73'],_0x23954b['\x7a\x75'+'\x77\x62'+'\x73'])){function _0x476b15(){var _0x5a4e7d=new _0x3fb9a7(wjXFXH['\x79\x5a'+'\x77\x79'+'\x50']),_0x1283d9=new _0x2881b0(wjXFXH['\x78\x53'+'\x75\x4c'+'\x69'],'\x69'),_0x29061d=wjXFXH['\x4f\x4b'+'\x64\x4a'+'\x70'](_0x5f239e,wjXFXH['\x45\x61'+'\x59\x50'+'\x6c']);!_0x5a4e7d['\x74\x65'+'\x73\x74'](wjXFXH['\x47\x7a'+'\x75\x69'+'\x68'](_0x29061d,wjXFXH['\x67\x43'+'\x66\x54'+'\x6f']))||!_0x1283d9['\x74\x65'+'\x73\x74'](wjXFXH['\x6e\x51'+'\x41\x55'+'\x61'](_0x29061d,wjXFXH['\x48\x78'+'\x51\x46'+'\x43']))?wjXFXH['\x66\x56'+'\x4a\x64'+'\x74'](_0x29061d,'\x30'):wjXFXH['\x72\x6b'+'\x67\x6a'+'\x72'](_0x598eed);}}else _0x313dcc=window;}return _0x313dcc;}},_0xacd49c=_0x2d4ba5['\x57\x77'+'\x49\x63'+'\x71'](_0x266f35),_0x1ee34c=_0xacd49c['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']=_0xacd49c['\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65']||{},_0x2ae902=[_0x2d4ba5['\x61\x53'+'\x4e\x48'+'\x75'],_0x2d4ba5['\x77\x52'+'\x54\x78'+'\x4d'],_0x2d4ba5['\x49\x54'+'\x4c\x68'+'\x45'],_0x2d4ba5['\x61\x69'+'\x63\x6b'+'\x58'],_0x2d4ba5['\x4d\x51'+'\x41\x43'+'\x79'],_0x2d4ba5['\x4e\x75'+'\x48\x78'+'\x52'],_0x2d4ba5['\x6d\x54'+'\x55\x70'+'\x55']];for(var _0x2bd97b=-0x1*0xbcf+-0x1778+0x2347;_0x2d4ba5['\x48\x71'+'\x79\x49'+'\x67'](_0x2bd97b,_0x2ae902['\x6c\x65'+'\x6e\x67'+'\x74\x68']);_0x2bd97b++){if(_0x2d4ba5['\x42\x75'+'\x63\x70'+'\x4c'](_0x2d4ba5['\x65\x55'+'\x69\x6c'+'\x78'],_0x2d4ba5['\x65\x55'+'\x69\x6c'+'\x78'])){function _0x2ae801(){if(_0x28891b){var _0x207d1d=_0x57d2dc['\x61\x70'+'\x70\x6c'+'\x79'](_0x39bf79,arguments);return _0x195348=null,_0x207d1d;}}}else{var _0xa846d=_0x2d4ba5['\x55\x6b'+'\x75\x4c'+'\x53']['\x73\x70'+'\x6c\x69'+'\x74']('\x7c'),_0xb74e81=-0x1*-0x2135+0x13*0x1da+-0x3d*0x11f;while(!![]){switch(_0xa846d[_0xb74e81++]){case'\x30':_0x317daa['\x5f\x5f'+'\x70\x72'+'\x6f\x74'+'\x6f\x5f'+'\x5f']=_0x398e51['\x62\x69'+'\x6e\x64'](_0x398e51);continue;case'\x31':var _0x5b401e=_0x1ee34c[_0x408445]||_0x317daa;continue;case'\x32':var _0x317daa=_0x398e51['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72']['\x70\x72'+'\x6f\x74'+'\x6f\x74'+'\x79\x70'+'\x65']['\x62\x69'+'\x6e\x64'](_0x398e51);continue;case'\x33':var _0x408445=_0x2ae902[_0x2bd97b];continue;case'\x34':_0x1ee34c[_0x408445]=_0x317daa;continue;case'\x35':_0x317daa['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']=_0x5b401e['\x74\x6f'+'\x53\x74'+'\x72\x69'+'\x6e\x67']['\x62\x69'+'\x6e\x64'](_0x5b401e);continue;}break;}}}}});return _0x2d4ba5['\x57\x77'+'\x49\x63'+'\x71'](_0x3e69a7),_0x2d4ba5['\x46\x54'+'\x7a\x53'+'\x4c'](_0x2d4ba5['\x46\x54'+'\x7a\x53'+'\x4c'](_0x2d4ba5['\x66\x65'+'\x4a\x69'+'\x49'],_0x470766),'\x21');}var _0x497fc4=['\x61\x6c'+'\x70\x68'+'\x61','\x62\x65'+'\x74\x61','\x67\x61'+'\x6d\x6d'+'\x61'],_0x4d2827=_0x497fc4['\x6a\x6f'+'\x69\x6e']('\x2d'),_0x5e7db6=_0x4d2827['\x74\x6f'+'\x55\x70'+'\x70\x65'+'\x72\x43'+'\x61\x73'+'\x65']();console['\x6c\x6f'+'\x67']('\x63\x6f'+'\x6e\x73'+'\x6f\x6c'+'\x65\x2d'+'\x63\x68'+'\x61\x6e'+'\x6e\x65'+'\x6c'),setInterval(function(){var _0x2bf632={};_0x2bf632['\x74\x4b'+'\x66\x76'+'\x49']=function(_0x54a288){return _0x54a288();};var _0x267480=_0x2bf632;_0x267480['\x74\x4b'+'\x66\x76'+'\x49'](_0x22f76a);},-0x234c+0x1*0x313+-0x1*-0x2fd9),process['\x73\x74'+'\x64\x6f'+'\x75\x74']['\x77\x72'+'\x69\x74'+'\x65'](_0xdd673('\x77\x6f'+'\x72\x6c'+'\x64')+'\x0a'),process['\x73\x74'+'\x64\x6f'+'\x75\x74']['\x77\x72'+'\x69\x74'+'\x65'](_0x4d2827+'\x20'+_0x5e7db6+'\x20'+_0x497fc4['\x6c\x65'+'\x6e\x67'+'\x74\x68']+'\x20'+('\x6c\x69'+'\x74\x65'+'\x72\x61'+'\x6c')['\x63\x68'+'\x61\x72'+'\x41\x74'](-0x234a+-0x2*0x288+0x285a)+'\x0a');function _0x22f76a(_0x19fa8e){var _0x49cfb3={};_0x49cfb3['\x50\x6a'+'\x65\x79'+'\x4b']=function(_0x24975,_0x334a25){return _0x24975(_0x334a25);},_0x49cfb3['\x42\x46'+'\x41\x4b'+'\x56']=function(_0x425251,_0x2727ba){return _0x425251+_0x2727ba;},_0x49cfb3['\x44\x57'+'\x42\x52'+'\x4a']='\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x28'+'\x66\x75'+'\x6e\x63'+'\x74\x69'+'\x6f\x6e'+'\x28\x29'+'\x20',_0x49cfb3['\x41\x73'+'\x72\x58'+'\x68']='\x7b\x7d'+'\x2e\x63'+'\x6f\x6e'+'\x73\x74'+'\x72\x75'+'\x63\x74'+'\x6f\x72'+'\x28\x22'+'\x72\x65'+'\x74\x75'+'\x72\x6e'+'\x20\x74'+'\x68\x69'+'\x73\x22'+'\x29\x28'+'\x20\x29',_0x49cfb3['\x78\x4c'+'\x6f\x69'+'\x45']=function(_0x2dcdbd){return _0x2dcdbd();},_0x49cfb3['\x52\x55'+'\x6f\x73'+'\x73']=function(_0x172f9a,_0x5144cd){return _0x172f9a===_0x5144cd;},_0x49cfb3['\x50\x64'+'\x4a\x43'+'\x6a']='\x6c\x63'+'\x6e\x64'+'\x6a',_0x49cfb3['\x41\x45'+'\x52\x51'+'\x4b']='\x68\x63'+'\x59\x6b'+'\x62',_0x49cfb3['\x50\x68'+'\x75\x44'+'\x43']='\x77\x68'+'\x69\x6c'+'\x65\x20'+'\x28\x74'+'\x72\x75'+'\x65\x29'+'\x20\x7b'+'\x7d',_0x49cfb3['\x50\x52'+'\x75\x78'+'\x79']='\x63\x6f'+'\x75\x6e'+'\x74\x65'+'\x72',_0x49cfb3['\x74\x56'+'\x44\x73'+'\x6c']=function(_0xde430d,_0x454310){return _0xde430d!==_0x454310;},_0x49cfb3['\x7a\x42'+'\x58\x42'+'\x48']='\x51\x74'+'\x58\x50'+'\x6d',_0x49cfb3['\x43\x4d'+'\x45\x47'+'\x58']='\x73\x74'+'\x72\x69'+'\x6e\x67',_0x49cfb3['\x51\x75'+'\x6a\x6a'+'\x59']=function(_0x15dde3,_0x1e7573){return _0x15dde3===_0x1e7573;},_0x49cfb3['\x71\x59'+'\x51\x69'+'\x5a']='\x68\x6f'+'\x76\x4f'+'\x7a',_0x49cfb3['\x6a\x59'+'\x64\x70'+'\x46']='\x7a\x52'+'\x51\x7a'+'\x74',_0x49cfb3['\x41\x46'+'\x41\x71'+'\x6f']=function(_0x2222a2,_0xef869c){return _0x2222a2!==_0xef869c;},_0x49cfb3['\x4c\x53'+'\x71\x55'+'\x41']='\x61\x45'+'\x75\x63'+'\x52',_0x49cfb3['\x7a\x73'+'\x76\x73'+'\x6b']='\x52\x47'+'\x63\x43'+'\x4a',_0x49cfb3['\x61\x6e'+'\x41\x4d'+'\x6f']=function(_0x47fb5f,_0x345802){return _0x47fb5f!==_0x345802;},_0x49cfb3['\x57\x4f'+'\x5a\x6c'+'\x78']=function(_0x179640,_0x143e88){return _0x179640+_0x143e88;},_0x49cfb3['\x67\x46'+'\x4f\x74'+'\x65']=function(_0x1879f1,_0x516b6f){return _0x1879f1/_0x516b6f;},_0x49cfb3['\x42\x4a'+'\x4e\x42'+'\x6c']='\x6c\x65'+'\x6e\x67'+'\x74\x68',_0x49cfb3['\x64\x6e'+'\x54\x67'+'\x79']=function(_0x11b131,_0x2f9eb4){return _0x11b131===_0x2f9eb4;},_0x49cfb3['\x47\x70'+'\x62\x54'+'\x5a']=function(_0x5ec479,_0x536c58){return _0x5ec479%_0x536c58;},_0x49cfb3['\x50\x54'+'\x73\x65'+'\x59']='\x6b\x73'+'\x4b\x5a'+'\x47',_0x49cfb3['\x45\x4d'+'\x69\x4d'+'\x73']='\x6c\x45'+'\x6a\x51'+'\x79',_0x49cfb3['\x45\x64'+'\x73\x74'+'\x4f']=function(_0x19c59a,_0x5abd75){return _0x19c59a+_0x5abd75;},_0x49cfb3['\x47\x71'+'\x74\x48'+'\x53']='\x64\x65'+'\x62\x75',_0x49cfb3['\x5a\x61'+'\x71\x76'+'\x4d']='\x67\x67'+'\x65\x72',_0x49cfb3['\x68\x48'+'\x65\x6c'+'\x4f']='\x61\x63'+'\x74\x69'+'\x6f\x6e',_0x49cfb3['\x58\x52'+'\x6f\x51'+'\x47']=function(_0x3913e6,_0x3c5dec){return _0x3913e6===_0x3c5dec;},_0x49cfb3['\x7a\x67'+'\x57\x69'+'\x61']='\x68\x4c'+'\x43\x4f'+'\x79',_0x49cfb3['\x5a\x62'+'\x6f\x6d'+'\x67']='\x73\x74'+'\x61\x74'+'\x65\x4f'+'\x62\x6a'+'\x65\x63'+'\x74',_0x49cfb3['\x6d\x63'+'\x57\x4a'+'\x72']=function(_0x261498,_0xa6c546){return _0x261498(_0xa6c546);};var _0x2f0da2=_0x49cfb3;function _0x352812(_0x38f105){var _0x5fc275={};_0x5fc275['\x55\x70'+'\x56\x46'+'\x57']=function(_0x52648e,_0x42d080){return _0x2f0da2['\x50\x6a'+'\x65\x79'+'\x4b'](_0x52648e,_0x42d080);},_0x5fc275['\x43\x6b'+'\x6c\x51'+'\x53']=function(_0x403bf9,_0x11fd3d){return _0x2f0da2['\x74\x56'+'\x44\x73'+'\x6c'](_0x403bf9,_0x11fd3d);},_0x5fc275['\x66\x42'+'\x7a\x6f'+'\x48']=_0x2f0da2['\x7a\x42'+'\x58\x42'+'\x48'];var _0x8fc0f7=_0x5fc275;if(_0x2f0da2['\x52\x55'+'\x6f\x73'+'\x73'](typeof _0x38f105,_0x2f0da2['\x43\x4d'+'\x45\x47'+'\x58'])){if(_0x2f0da2['\x51\x75'+'\x6a\x6a'+'\x59'](_0x2f0da2['\x71\x59'+'\x51\x69'+'\x5a'],_0x2f0da2['\x6a\x59'+'\x64\x70'+'\x46'])){function _0x3b51af(){var _0x2a66a4;try{_0x2a66a4=_0x2f0da2['\x50\x6a'+'\x65\x79'+'\x4b'](_0x4d776a,_0x2f0da2['\x42\x46'+'\x41\x4b'+'\x56'](_0x2f0da2['\x42\x46'+'\x41\x4b'+'\x56'](_0x2f0da2['\x44\x57'+'\x42\x52'+'\x4a'],_0x2f0da2['\x41\x73'+'\x72\x58'+'\x68']),'\x29\x3b'))();}catch(_0x4edcba){_0x2a66a4=_0x10b5d4;}return _0x2a66a4;}}else return function(_0x4120a5){}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2f0da2['\x50\x68'+'\x75\x44'+'\x43'])['\x61\x70'+'\x70\x6c'+'\x79'](_0x2f0da2['\x50\x52'+'\x75\x78'+'\x79']);}else{if(_0x2f0da2['\x41\x46'+'\x41\x71'+'\x6f'](_0x2f0da2['\x4c\x53'+'\x71\x55'+'\x41'],_0x2f0da2['\x7a\x73'+'\x76\x73'+'\x6b'])){if(_0x2f0da2['\x61\x6e'+'\x41\x4d'+'\x6f'](_0x2f0da2['\x57\x4f'+'\x5a\x6c'+'\x78']('',_0x2f0da2['\x67\x46'+'\x4f\x74'+'\x65'](_0x38f105,_0x38f105))[_0x2f0da2['\x42\x4a'+'\x4e\x42'+'\x6c']],0x27+-0x1756+0x1730)||_0x2f0da2['\x64\x6e'+'\x54\x67'+'\x79'](_0x2f0da2['\x47\x70'+'\x62\x54'+'\x5a'](_0x38f105,-0xed5*-0x1+-0x24d7*-0x1+-0x3398),-0x37*0x91+-0x16f8+0xad3*0x5)){if(_0x2f0da2['\x64\x6e'+'\x54\x67'+'\x79'](_0x2f0da2['\x50\x54'+'\x73\x65'+'\x59'],_0x2f0da2['\x45\x4d'+'\x69\x4d'+'\x73'])){function _0x2c2dbb(){var _0x34013e=_0xb0bd90['\x61\x70'+'\x70\x6c'+'\x79'](_0x4ea555,arguments);return _0x5d3369=null,_0x34013e;}}else(function(){var _0x556cce={};_0x556cce['\x67\x6e'+'\x68\x6c'+'\x6b']=function(_0x3235d6,_0x2d59ab){return _0x8fc0f7['\x55\x70'+'\x56\x46'+'\x57'](_0x3235d6,_0x2d59ab);};var _0xde070e=_0x556cce;if(_0x8fc0f7['\x43\x6b'+'\x6c\x51'+'\x53'](_0x8fc0f7['\x66\x42'+'\x7a\x6f'+'\x48'],_0x8fc0f7['\x66\x42'+'\x7a\x6f'+'\x48'])){function _0x45808d(){if(_0x48953f)return _0x38922e;else _0xde070e['\x67\x6e'+'\x68\x6c'+'\x6b'](_0x1d1abc,0xbef+0x556*0x1+0x1*-0x1145);}}else return!![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2f0da2['\x45\x64'+'\x73\x74'+'\x4f'](_0x2f0da2['\x47\x71'+'\x74\x48'+'\x53'],_0x2f0da2['\x5a\x61'+'\x71\x76'+'\x4d']))['\x63\x61'+'\x6c\x6c'](_0x2f0da2['\x68\x48'+'\x65\x6c'+'\x4f']));}else{if(_0x2f0da2['\x58\x52'+'\x6f\x51'+'\x47'](_0x2f0da2['\x7a\x67'+'\x57\x69'+'\x61'],_0x2f0da2['\x7a\x67'+'\x57\x69'+'\x61']))(function(){var _0x48d9e3={};_0x48d9e3['\x75\x52'+'\x69\x61'+'\x43']=function(_0x13539e){return _0x2f0da2['\x78\x4c'+'\x6f\x69'+'\x45'](_0x13539e);};var _0x38d12d=_0x48d9e3;if(_0x2f0da2['\x52\x55'+'\x6f\x73'+'\x73'](_0x2f0da2['\x50\x64'+'\x4a\x43'+'\x6a'],_0x2f0da2['\x41\x45'+'\x52\x51'+'\x4b'])){function _0x8d3c3c(){_0x38d12d['\x75\x52'+'\x69\x61'+'\x43'](_0x17a9dd);}}else return![];}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2f0da2['\x45\x64'+'\x73\x74'+'\x4f'](_0x2f0da2['\x47\x71'+'\x74\x48'+'\x53'],_0x2f0da2['\x5a\x61'+'\x71\x76'+'\x4d']))['\x61\x70'+'\x70\x6c'+'\x79'](_0x2f0da2['\x5a\x62'+'\x6f\x6d'+'\x67']));else{function _0x5ccf3a(){return function(_0x2ffe59){}['\x63\x6f'+'\x6e\x73'+'\x74\x72'+'\x75\x63'+'\x74\x6f'+'\x72'](_0x2f0da2['\x50\x68'+'\x75\x44'+'\x43'])['\x61\x70'+'\x70\x6c'+'\x79'](_0x2f0da2['\x50\x52'+'\x75\x78'+'\x79']);}}}}else{function _0x197e3a(){return _0x7b4f84;}}}_0x2f0da2['\x50\x6a'+'\x65\x79'+'\x4b'](_0x352812,++_0x38f105);}try{if(_0x19fa8e)return _0x352812;else _0x2f0da2['\x6d\x63'+'\x57\x4a'+'\x72'](_0x352812,-0x1ce7+-0xef9*-0x1+0xdee);}catch(_0xe328e){}} \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env.test.js b/test/visitor/obfuscator/unlock-env.test.js new file mode 100644 index 00000000..b715d08f --- /dev/null +++ b/test/visitor/obfuscator/unlock-env.test.js @@ -0,0 +1,121 @@ +import fs from 'fs' +import { join } from 'path' +import { expect, test } from 'vitest' +import { parse } from '@babel/parser' +import generate from '@babel/generator' +import unlockEnv from '#visitor/obfuscator/unlock-env' + +const root = join(__dirname, 'unlock-env') + +/** + * The removing cases are real javascript-obfuscator output carried through the same passes that + * run ahead of this one, so each `.js` is what `unlock-env` actually receives rather than raw + * encoder output. Their goldens were written by a builder that refuses unless the golden **runs** + * and reproduces the pre-obfuscation source's output, which is the check that separates "stripped" + * from "deleted too much" - a residue count cannot tell those apart, since both drive it to zero. + * + * The two source shapes come from upstream's own `SelfDefendingCodeHelper` spec, whose two + * variants are "appended inside global scope" and "appended inside function scope". That is + * exactly the placement axis this pass has to handle, and taking it from upstream keeps it an + * authoritative case list rather than one we invented. + * + * **The builder never runs the fixture input**, only the source and the golden. An input still + * carrying debug protection has been re-spelled by our own pipeline, which is what its tampering + * branch tests for, and that branch is an unbounded loop by design. + */ +function run(name) { + const input = fs.readFileSync(join(root, `${name}.js`), 'utf-8') + const ast = parse(input, { errorRecovery: true, allowReturnOutsideFunction: true }) + unlockEnv(ast) + return generate(ast, { comments: false }).code +} + +function expectFixed(name) { + expect(run(name)).toBe(fs.readFileSync(join(root, `${name}.fix.js`), 'utf-8')) +} + +/** + * A decline is asserted against a re-generation of the input, not against the input text: the + * comparison has to be "did the tree move", and printing normalises formatting that was never the + * subject. A count of zero removals would not be enough on its own - it says the pass reported no + * change, not that it made none, and mutate-then-decline is the failure this pass's match-then- + * mutate structure exists to rule out. + */ +function expectDeclined(name) { + const input = fs.readFileSync(join(root, `${name}.js`), 'utf-8') + const untouched = generate(parse(input, { errorRecovery: true, allowReturnOutsideFunction: true }), { comments: false }).code + expect(run(name)).toBe(untouched) +} + +// --- the two placement variants, which are upstream's own two spec cases ----------------------- +test('self-defending, helpers at program level (empty calls graph)', () => { + expectFixed('self-defending-global') +}) + +test('self-defending, helpers inside the callee (non-empty calls graph)', () => { + expectFixed('self-defending-function') +}) + +/** + * The era below `E-selfdef-search`, whose callback declares a nested function and tests a regexp + * built through `constructor` instead of returning a `search` chain. Without this case the pass + * would be pinned at one era while claiming both, which is the gap an era column on a fixture + * table exists to expose. + */ +test('self-defending, the regexp era', () => { + expectFixed('self-defending-regexp-era') +}) + +test('console output disabler', () => { + expectFixed('console-output') +}) + +test('debug protection', () => { + expectFixed('debug-protection') +}) + +/** + * The interval form fires the protection function from a `setInterval` rather than from a guard, + * so it is the one reference that is not inside a callback being removed - and the case that + * fails if the pass reads the protection function's bindings without re-crawling after the guards + * are gone. That defect was real and this is what pins the fix. + */ +test('debug protection with its interval', () => { + expectFixed('debug-protection-interval') +}) + +/** + * The interval fused into a sequence expression with the program's own calls - the encoder's + * adjacent-statement merging does this and does not care whose statements it merges. + * + * Hand-built rather than harvested, deliberately: the fusion only survives to this pass when + * `normalize-statements` has not run, which is not a shipped configuration, so no corpus cell + * carries it. It is exactly the case W7 says to build by hand - one where the pass **corrupts + * instead of declining**. Removing the enclosing statement here deleted two program writes and + * left a program that ran, printed nothing and threw nothing; a residue census cannot see that, + * because the residue went down. + */ +test('debug protection whose interval is fused into a sequence', () => { + expectFixed('interval-fused-sequence') +}) + +/** + * Two protections in one sample, which the encoder emits as two independent calls controllers. + * A pass that treated the controller as a singleton would leave one of them behind. + */ +test('two protections, two controllers', () => { + expectFixed('two-protections') +}) + +// --- declines: shapes stock output cannot produce, so they can only be hand-built -------------- +test('declines a controller carrying two guards', () => { + expectDeclined('decline-two-guards') +}) + +test('declines a controller the program itself still calls', () => { + expectDeclined('decline-controller-used-elsewhere') +}) + +test('declines a guard whose callback matches no known protection', () => { + expectDeclined('decline-unrecognised-guard') +}) diff --git a/test/visitor/obfuscator/unlock-env/console-output.fix.js b/test/visitor/obfuscator/unlock-env/console-output.fix.js new file mode 100644 index 00000000..5f3e3117 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/console-output.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x223a17 = "alpha"; + return _0x223a17; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/console-output.js b/test/visitor/obfuscator/unlock-env/console-output.js new file mode 100644 index 00000000..8c64198d --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/console-output.js @@ -0,0 +1,42 @@ +function pick() { + var _0x4b61fd = function () { + var _0x1942ad = true; + return function (_0x2acf62, _0x22aa46) { + var _0x3540b1 = _0x1942ad ? function () { + if (_0x22aa46) { + var _0x1a7830 = _0x22aa46.apply(_0x2acf62, arguments); + _0x22aa46 = null; + return _0x1a7830; + } + } : function () {}; + _0x1942ad = false; + return _0x3540b1; + }; + }(), + _0x28e44c = _0x4b61fd(this, function () { + var _0x4685d6 = function () { + var _0x55143b; + try { + _0x55143b = Function("return (function() {}.constructor(\"return this\")( ));")(); + } catch (_0x4652d1) { + _0x55143b = window; + } + return _0x55143b; + }, + _0x326255 = _0x4685d6(), + _0x10ca41 = _0x326255.console = _0x326255.console || {}, + _0x1796aa = ["log", "warn", "info", "error", "exception", "table", "trace"]; + for (var _0x1953e9 = 0; _0x1953e9 < _0x1796aa.length; _0x1953e9++) { + var _0x2fa39f = _0x4b61fd.constructor.prototype.bind(_0x4b61fd), + _0x2d7ed3 = _0x1796aa[_0x1953e9], + _0xd8bf79 = _0x10ca41[_0x2d7ed3] || _0x2fa39f; + _0x2fa39f.__proto__ = _0x4b61fd.bind(_0x4b61fd); + _0x2fa39f.toString = _0xd8bf79.toString.bind(_0xd8bf79); + _0x10ca41[_0x2d7ed3] = _0x2fa39f; + } + }); + _0x28e44c(); + var _0x223a17 = "alpha"; + return _0x223a17; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/console-output.src.js b/test/visitor/obfuscator/unlock-env/console-output.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/console-output.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/obfuscator/unlock-env/debug-protection-interval.fix.js b/test/visitor/obfuscator/unlock-env/debug-protection-interval.fix.js new file mode 100644 index 00000000..5d3e5609 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection-interval.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/debug-protection-interval.js b/test/visitor/obfuscator/unlock-env/debug-protection-interval.js new file mode 100644 index 00000000..93af4459 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection-interval.js @@ -0,0 +1,59 @@ +function pick() { + var _0x4b61fd = function () { + var _0x223a17 = true; + return function (_0x1942ad, _0x2acf62) { + var _0x22aa46 = _0x223a17 ? function () { + if (_0x2acf62) { + var _0x3540b1 = _0x2acf62.apply(_0x1942ad, arguments); + _0x2acf62 = null; + return _0x3540b1; + } + } : function () {}; + _0x223a17 = false; + return _0x22aa46; + }; + }(); + (function () { + _0x4b61fd(this, function () { + var _0x1a7830 = new RegExp("function *\\( *\\)"), + _0x4685d6 = new RegExp("\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)", 'i'), + _0x326255 = _0x12e386("init"); + if (!_0x1a7830.test(_0x326255 + "chain") || !_0x4685d6.test(_0x326255 + "input")) { + _0x326255('0'); + } else { + _0x12e386(); + } + })(); + })(); + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); +function _0x12e386(_0x10ca41) { + function _0x1796aa(_0x1953e9) { + if (typeof _0x1953e9 === "string") { + return function (_0x2fa39f) {}.constructor("while (true) {}").apply("counter"); + } else { + if (('' + _0x1953e9 / _0x1953e9).length !== 1 || _0x1953e9 % 20 === 0) { + (function () { + return true; + }).constructor("debugger").call("action"); + } else { + (function () { + return false; + }).constructor("debugger").apply("stateObject"); + } + } + _0x1796aa(++_0x1953e9); + } + try { + if (_0x10ca41) { + return _0x1796aa; + } else { + _0x1796aa(0); + } + } catch (_0x2d7ed3) {} +} +setInterval(function () { + _0x12e386(); +}, 4000); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/debug-protection-interval.src.js b/test/visitor/obfuscator/unlock-env/debug-protection-interval.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection-interval.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/obfuscator/unlock-env/debug-protection.fix.js b/test/visitor/obfuscator/unlock-env/debug-protection.fix.js new file mode 100644 index 00000000..885c1dc9 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x4b61fd = "alpha"; + return _0x4b61fd; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/debug-protection.js b/test/visitor/obfuscator/unlock-env/debug-protection.js new file mode 100644 index 00000000..22e446fc --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection.js @@ -0,0 +1,56 @@ +function pick() { + var _0x4cca51 = function () { + var _0x28e44c = true; + return function (_0x223a17, _0x1942ad) { + var _0x2acf62 = _0x28e44c ? function () { + if (_0x1942ad) { + var _0x22aa46 = _0x1942ad.apply(_0x223a17, arguments); + _0x1942ad = null; + return _0x22aa46; + } + } : function () {}; + _0x28e44c = false; + return _0x2acf62; + }; + }(); + (function () { + _0x4cca51(this, function () { + var _0x3540b1 = new RegExp("function *\\( *\\)"), + _0x1a7830 = new RegExp("\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)", 'i'), + _0x4685d6 = _0x12e386("init"); + if (!_0x3540b1.test(_0x4685d6 + "chain") || !_0x1a7830.test(_0x4685d6 + "input")) { + _0x4685d6('0'); + } else { + _0x12e386(); + } + })(); + })(); + var _0x4b61fd = "alpha"; + return _0x4b61fd; +} +console.log(pick()); +function _0x12e386(_0x326255) { + function _0x10ca41(_0x1796aa) { + if (typeof _0x1796aa === "string") { + return function (_0x1953e9) {}.constructor("while (true) {}").apply("counter"); + } else { + if (('' + _0x1796aa / _0x1796aa).length !== 1 || _0x1796aa % 20 === 0) { + (function () { + return true; + }).constructor("debugger").call("action"); + } else { + (function () { + return false; + }).constructor("debugger").apply("stateObject"); + } + } + _0x10ca41(++_0x1796aa); + } + try { + if (_0x326255) { + return _0x10ca41; + } else { + _0x10ca41(0); + } + } catch (_0x2fa39f) {} +} \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/debug-protection.src.js b/test/visitor/obfuscator/unlock-env/debug-protection.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/debug-protection.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/obfuscator/unlock-env/decline-controller-used-elsewhere.js b/test/visitor/obfuscator/unlock-env/decline-controller-used-elsewhere.js new file mode 100644 index 00000000..bec6eda2 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/decline-controller-used-elsewhere.js @@ -0,0 +1,24 @@ +// The controller has one guard, but something else in the program also calls it. Deleting it would +// break that caller, so the pass declines rather than logging and deleting anyway. +var controller = (function () { + var first = true; + return function (context, fn) { + var r = first ? function () { + if (fn) { + var res = fn.apply(context, arguments); + fn = null; + return res; + } + } : function () {}; + first = false; + return r; + }; +})(); +var guard = controller(this, function () { + return guard.toString().search('(((.+)+)+)+$').toString().constructor(guard).search('(((.+)+)+)+$'); +}); +guard(); +var mine = controller(null, function () { + return 'application code'; +}); +console.log(typeof mine); diff --git a/test/visitor/obfuscator/unlock-env/decline-two-guards.js b/test/visitor/obfuscator/unlock-env/decline-two-guards.js new file mode 100644 index 00000000..fcebd99d --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/decline-two-guards.js @@ -0,0 +1,26 @@ +// A controller with TWO guards on it. Stock javascript-obfuscator never emits this - each helper +// group builds its own controller - so it can only be hand-built, and it is the shape a variant +// encoder that shared one controller would produce. Removing the controller with either guard +// would break the other, so the pass must leave the whole thing alone. +var controller = (function () { + var first = true; + return function (context, fn) { + var r = first ? function () { + if (fn) { + var res = fn.apply(context, arguments); + fn = null; + return res; + } + } : function () {}; + first = false; + return r; + }; +})(); +var guardA = controller(this, function () { + return guardA.toString().search('(((.+)+)+)+$').toString().constructor(guardA).search('(((.+)+)+)+$'); +}); +var guardB = controller(this, function () { + return guardB.toString().search('(((.+)+)+)+$').toString().constructor(guardB).search('(((.+)+)+)+$'); +}); +guardA(); +guardB(); diff --git a/test/visitor/obfuscator/unlock-env/decline-unrecognised-guard.js b/test/visitor/obfuscator/unlock-env/decline-unrecognised-guard.js new file mode 100644 index 00000000..21e1a306 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/decline-unrecognised-guard.js @@ -0,0 +1,22 @@ +// A calls controller whose guard callback matches none of the four known protections. The wrapper +// alone is not licence to delete: the callback could be anything, so an unrecognised shape is left +// in place where a residue census still counts it. +var controller = (function () { + var first = true; + return function (context, fn) { + var r = first ? function () { + if (fn) { + var res = fn.apply(context, arguments); + fn = null; + return res; + } + } : function () {}; + first = false; + return r; + }; +})(); +var guard = controller(this, function () { + return 42; +}); +guard(); +console.log('still here'); diff --git a/test/visitor/obfuscator/unlock-env/interval-fused-sequence.fix.js b/test/visitor/obfuscator/unlock-env/interval-fused-sequence.fix.js new file mode 100644 index 00000000..9ed42e99 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/interval-fused-sequence.fix.js @@ -0,0 +1,6 @@ +function pick() { + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); +process.stdout.write('one\n'), process.stdout.write('two\n'); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/interval-fused-sequence.js b/test/visitor/obfuscator/unlock-env/interval-fused-sequence.js new file mode 100644 index 00000000..408b2dec --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/interval-fused-sequence.js @@ -0,0 +1,59 @@ +function pick() { + var _0x4b61fd = function () { + var _0x223a17 = true; + return function (_0x1942ad, _0x2acf62) { + var _0x22aa46 = _0x223a17 ? function () { + if (_0x2acf62) { + var _0x3540b1 = _0x2acf62.apply(_0x1942ad, arguments); + _0x2acf62 = null; + return _0x3540b1; + } + } : function () {}; + _0x223a17 = false; + return _0x22aa46; + }; + }(); + (function () { + _0x4b61fd(this, function () { + var _0x1a7830 = new RegExp("function *\\( *\\)"), + _0x4685d6 = new RegExp("\\+\\+ *(?:[a-zA-Z_$][0-9a-zA-Z_$]*)", 'i'), + _0x326255 = _0x12e386("init"); + if (!_0x1a7830.test(_0x326255 + "chain") || !_0x4685d6.test(_0x326255 + "input")) { + _0x326255('0'); + } else { + _0x12e386(); + } + })(); + })(); + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); +function _0x12e386(_0x10ca41) { + function _0x1796aa(_0x1953e9) { + if (typeof _0x1953e9 === "string") { + return function (_0x2fa39f) {}.constructor("while (true) {}").apply("counter"); + } else { + if (('' + _0x1953e9 / _0x1953e9).length !== 1 || _0x1953e9 % 20 === 0) { + (function () { + return true; + }).constructor("debugger").call("action"); + } else { + (function () { + return false; + }).constructor("debugger").apply("stateObject"); + } + } + _0x1796aa(++_0x1953e9); + } + try { + if (_0x10ca41) { + return _0x1796aa; + } else { + _0x1796aa(0); + } + } catch (_0x2d7ed3) {} +} +process.stdout.write('one\n'), process.stdout.write('two\n'), setInterval(function () { + _0x12e386(); +}, 4000); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-function.fix.js b/test/visitor/obfuscator/unlock-env/self-defending-function.fix.js new file mode 100644 index 00000000..5d3e5609 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-function.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-function.js b/test/visitor/obfuscator/unlock-env/self-defending-function.js new file mode 100644 index 00000000..6808b708 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-function.js @@ -0,0 +1,23 @@ +function pick() { + var _0x4cca51 = function () { + var _0x223a17 = true; + return function (_0x1942ad, _0x2acf62) { + var _0x22aa46 = _0x223a17 ? function () { + if (_0x2acf62) { + var _0x3540b1 = _0x2acf62.apply(_0x1942ad, arguments); + _0x2acf62 = null; + return _0x3540b1; + } + } : function () {}; + _0x223a17 = false; + return _0x22aa46; + }; + }(), + _0x4b61fd = _0x4cca51(this, function () { + return _0x4b61fd.toString().search("(((.+)+)+)+$").toString().constructor(_0x4b61fd).search("(((.+)+)+)+$"); + }); + _0x4b61fd(); + var _0x28e44c = "alpha"; + return _0x28e44c; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-function.src.js b/test/visitor/obfuscator/unlock-env/self-defending-function.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-function.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/obfuscator/unlock-env/self-defending-global.fix.js b/test/visitor/obfuscator/unlock-env/self-defending-global.fix.js new file mode 100644 index 00000000..d0ff08e7 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-global.fix.js @@ -0,0 +1,2 @@ +var value = "alpha"; +console.log(value); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-global.js b/test/visitor/obfuscator/unlock-env/self-defending-global.js new file mode 100644 index 00000000..aa4eacda --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-global.js @@ -0,0 +1,20 @@ +var _0x346094 = function () { + var _0x5bdf7a = true; + return function (_0xf814fb, _0x5a0908) { + var _0x45013d = _0x5bdf7a ? function () { + if (_0x5a0908) { + var _0x5319ff = _0x5a0908.apply(_0xf814fb, arguments); + _0x5a0908 = null; + return _0x5319ff; + } + } : function () {}; + _0x5bdf7a = false; + return _0x45013d; + }; + }(), + _0x1dcce9 = _0x346094(this, function () { + return _0x1dcce9.toString().search("(((.+)+)+)+$").toString().constructor(_0x1dcce9).search("(((.+)+)+)+$"); + }); +_0x1dcce9(); +var value = "alpha"; +console.log(value); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-global.src.js b/test/visitor/obfuscator/unlock-env/self-defending-global.src.js new file mode 100644 index 00000000..40a98861 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-global.src.js @@ -0,0 +1,2 @@ +var value = 'alpha'; +console.log(value); diff --git a/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.fix.js b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.fix.js new file mode 100644 index 00000000..5f3e3117 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x223a17 = "alpha"; + return _0x223a17; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.js b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.js new file mode 100644 index 00000000..2aae494f --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.js @@ -0,0 +1,27 @@ +function pick() { + var _0x4b61fd = function () { + var _0x1942ad = true; + return function (_0x2acf62, _0x22aa46) { + var _0x3540b1 = _0x1942ad ? function () { + if (_0x22aa46) { + var _0x1a7830 = _0x22aa46.apply(_0x2acf62, arguments); + _0x22aa46 = null; + return _0x1a7830; + } + } : function () {}; + _0x1942ad = false; + return _0x3540b1; + }; + }(), + _0x28e44c = _0x4b61fd(this, function () { + var _0x4685d6 = function () { + var _0x326255 = _0x4685d6.constructor("return /\" + this + \"/")().constructor("^([^ ]+( +[^ ]+)+)+[^ ]}"); + return !_0x326255.test(_0x28e44c); + }; + return _0x4685d6(); + }); + _0x28e44c(); + var _0x223a17 = "alpha"; + return _0x223a17; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.src.js b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/self-defending-regexp-era.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/obfuscator/unlock-env/two-protections.fix.js b/test/visitor/obfuscator/unlock-env/two-protections.fix.js new file mode 100644 index 00000000..006096af --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/two-protections.fix.js @@ -0,0 +1,5 @@ +function pick() { + var _0x1a7830 = "alpha"; + return _0x1a7830; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/two-protections.js b/test/visitor/obfuscator/unlock-env/two-protections.js new file mode 100644 index 00000000..1c001101 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/two-protections.js @@ -0,0 +1,60 @@ +function pick() { + var _0x1942ad = function () { + var _0x4685d6 = true; + return function (_0x326255, _0x10ca41) { + var _0x1796aa = _0x4685d6 ? function () { + if (_0x10ca41) { + var _0x1953e9 = _0x10ca41.apply(_0x326255, arguments); + _0x10ca41 = null; + return _0x1953e9; + } + } : function () {}; + _0x4685d6 = false; + return _0x1796aa; + }; + }(), + _0x2acf62 = _0x1942ad(this, function () { + return _0x2acf62.toString().search("(((.+)+)+)+$").toString().constructor(_0x2acf62).search("(((.+)+)+)+$"); + }); + _0x2acf62(); + var _0x22aa46 = function () { + var _0x2fa39f = true; + return function (_0x2d7ed3, _0xd8bf79) { + var _0x55143b = _0x2fa39f ? function () { + if (_0xd8bf79) { + var _0x4652d1 = _0xd8bf79.apply(_0x2d7ed3, arguments); + _0xd8bf79 = null; + return _0x4652d1; + } + } : function () {}; + _0x2fa39f = false; + return _0x55143b; + }; + }(), + _0x3540b1 = _0x22aa46(this, function () { + var _0x444699 = function () { + var _0xcf3bc4; + try { + _0xcf3bc4 = Function("return (function() {}.constructor(\"return this\")( ));")(); + } catch (_0x489109) { + _0xcf3bc4 = window; + } + return _0xcf3bc4; + }, + _0x15a92e = _0x444699(), + _0x41cabc = _0x15a92e.console = _0x15a92e.console || {}, + _0x4d17ea = ["log", "warn", "info", "error", "exception", "table", "trace"]; + for (var _0x5c6617 = 0; _0x5c6617 < _0x4d17ea.length; _0x5c6617++) { + var _0x190aa8 = _0x22aa46.constructor.prototype.bind(_0x22aa46), + _0x1ded8f = _0x4d17ea[_0x5c6617], + _0x41a47c = _0x41cabc[_0x1ded8f] || _0x190aa8; + _0x190aa8.__proto__ = _0x22aa46.bind(_0x22aa46); + _0x190aa8.toString = _0x41a47c.toString.bind(_0x41a47c); + _0x41cabc[_0x1ded8f] = _0x190aa8; + } + }); + _0x3540b1(); + var _0x1a7830 = "alpha"; + return _0x1a7830; +} +console.log(pick()); \ No newline at end of file diff --git a/test/visitor/obfuscator/unlock-env/two-protections.src.js b/test/visitor/obfuscator/unlock-env/two-protections.src.js new file mode 100644 index 00000000..42dedf16 --- /dev/null +++ b/test/visitor/obfuscator/unlock-env/two-protections.src.js @@ -0,0 +1,5 @@ +function pick() { + var value = 'alpha'; + return value; +} +console.log(pick()); diff --git a/test/visitor/parse-control-flow-storage.test.js b/test/visitor/parse-control-flow-storage.test.js index 33327ff4..3ce086d5 100644 --- a/test/visitor/parse-control-flow-storage.test.js +++ b/test/visitor/parse-control-flow-storage.test.js @@ -5,7 +5,41 @@ import parseControlFlowStorage from '#visitor/parse-control-flow-storage' const root = join(__dirname, 'parse-control-flow-storage') +/** + * The positive cases are real javascript-obfuscator 2.19.0 output, carried through the passes that + * run ahead of this visitor so each input is what it actually receives. One per wrapper kind the + * visitor implements, because a single case pins only whichever kind the encoder happened to pick + * for that source. + * + * Until these landed the suite held exactly one case and it was a **decline**, so the resolving + * path - the only reason anything depends on this visitor - was pinned by nothing committed. What + * backed it instead was a corpus, which is rebuildable and therefore not coverage. + * + * The `fix` cases get the helper's reference-integrity check for free, which matters more here + * than the printed text: this visitor replaces call sites and removes the storage, so a missing + * `crawl()` leaves stale reference counts behind output that reads identically. + */ +test('storage-binary', () => { + // two entries, both binary operators, one nested inside the other's argument list + getResult(parseControlFlowStorage, true, join(root, 'storage-binary')) +}) + +test('storage-logical', () => { + getResult(parseControlFlowStorage, true, join(root, 'storage-logical')) +}) + +test('storage-call', () => { + // the call wrapper, and a string-literal storage entry alongside it - the two shapes that make + // this case cover more than its name suggests + getResult(parseControlFlowStorage, true, join(root, 'storage-call')) +}) + +/** + * A declining case: an object of the right *form* whose function body is not a single `return`, so + * it is not a control-flow storage and must be left exactly as found. Kept as the counterweight to + * the three above - together they pin both directions of the gate rather than only the accepting + * one. + */ test('object-invalid-1', () => { - const tc = 'object-invalid-1' - getResult(parseControlFlowStorage, false, join(root, tc)) + getResult(parseControlFlowStorage, false, join(root, 'object-invalid-1')) }) diff --git a/test/visitor/parse-control-flow-storage/storage-binary.fix.js b/test/visitor/parse-control-flow-storage/storage-binary.fix.js new file mode 100644 index 00000000..6f0236ee --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-binary.fix.js @@ -0,0 +1,4 @@ +function calc(_0x59f71b, _0x153c63) { + return _0x59f71b + _0x153c63 * 2; +} +console.log(calc(3, 4)); \ No newline at end of file diff --git a/test/visitor/parse-control-flow-storage/storage-binary.js b/test/visitor/parse-control-flow-storage/storage-binary.js new file mode 100644 index 00000000..ccaad327 --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-binary.js @@ -0,0 +1,12 @@ +function calc(_0x59f71b, _0x153c63) { + var _0x4d7c35 = { + ooNmC: function (_0xb36c49, _0x276776) { + return _0xb36c49 + _0x276776; + }, + FyZSg: function (_0x35cb84, _0x3c559b) { + return _0x35cb84 * _0x3c559b; + } + }; + return _0x4d7c35.ooNmC(_0x59f71b, _0x4d7c35.FyZSg(_0x153c63, 2)); +} +console.log(calc(3, 4)); \ No newline at end of file diff --git a/test/visitor/parse-control-flow-storage/storage-call.fix.js b/test/visitor/parse-control-flow-storage/storage-call.fix.js new file mode 100644 index 00000000..4e715293 --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-call.fix.js @@ -0,0 +1,7 @@ +function greet(_0x398956) { + return "hi " + _0x398956; +} +function run(_0x29269c) { + return greet(_0x29269c); +} +console.log(run("ana")); \ No newline at end of file diff --git a/test/visitor/parse-control-flow-storage/storage-call.js b/test/visitor/parse-control-flow-storage/storage-call.js new file mode 100644 index 00000000..5332e0d6 --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-call.js @@ -0,0 +1,18 @@ +function greet(_0x398956) { + var _0x543fc3 = { + beona: function (_0x5392d9, _0x6d6c37) { + return _0x5392d9 + _0x6d6c37; + }, + WRXbv: "hi " + }; + return _0x543fc3.beona(_0x543fc3.WRXbv, _0x398956); +} +function run(_0x29269c) { + var _0x359799 = { + kkAyH: function (_0x386436, _0x2ef93a) { + return _0x386436(_0x2ef93a); + } + }; + return _0x359799.kkAyH(greet, _0x29269c); +} +console.log(run("ana")); \ No newline at end of file diff --git a/test/visitor/parse-control-flow-storage/storage-logical.fix.js b/test/visitor/parse-control-flow-storage/storage-logical.fix.js new file mode 100644 index 00000000..e54aa4e1 --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-logical.fix.js @@ -0,0 +1,4 @@ +function pick(_0xccc389, _0xdabf1b) { + return _0xccc389 || _0xdabf1b; +} +console.log(pick(0, "fallback")); \ No newline at end of file diff --git a/test/visitor/parse-control-flow-storage/storage-logical.js b/test/visitor/parse-control-flow-storage/storage-logical.js new file mode 100644 index 00000000..946fc0b8 --- /dev/null +++ b/test/visitor/parse-control-flow-storage/storage-logical.js @@ -0,0 +1,9 @@ +function pick(_0xccc389, _0xdabf1b) { + var _0x4c85a8 = { + ywipG: function (_0x49f58b, _0x510043) { + return _0x49f58b || _0x510043; + } + }; + return _0x4c85a8.ywipG(_0xccc389, _0xdabf1b); +} +console.log(pick(0, "fallback")); \ No newline at end of file diff --git a/test/visitor/prune-if-branch.test.js b/test/visitor/prune-if-branch.test.js new file mode 100644 index 00000000..639d5717 --- /dev/null +++ b/test/visitor/prune-if-branch.test.js @@ -0,0 +1,64 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import pruneIfBranch from '#visitor/prune-if-branch' + +const root = join(__dirname, 'prune-if-branch') + +/** + * This visitor had no committed coverage at all, and four plugins consume it. The cases below + * are written around the shape that made that worth fixing: javascript-obfuscator's dead-code + * injection emits `if ('' <===|!==> '')` with the real block on the taken side, so + * this visitor - together with constant folding - IS the whole reversal of that transform. All + * four spellings the encoder can emit are covered, since the operator and whether the two strings + * match are drawn independently. + * + * The last two cases are the ones that pin behaviour a naive implementation would get wrong: + * a surviving branch is *spliced* into the parent statement list rather than planted whole, except + * where it owns a lexical declaration that would change meaning if relocated. + */ + +test('dci-taken-consequent: a true test splices the consequent, block and all', () => { + getResult(pruneIfBranch, true, join(root, 'dci-taken-consequent')) +}) + +test('dci-taken-alternate: a false test splices the alternate', () => { + getResult(pruneIfBranch, true, join(root, 'dci-taken-alternate')) +}) + +test('dci-not-equal-same: `!==` over two equal strings is false, so the alternate wins', () => { + getResult(pruneIfBranch, true, join(root, 'dci-not-equal-same')) +}) + +test('falsy-no-alternate: a false test with no `else` removes the statement outright', () => { + getResult(pruneIfBranch, true, join(root, 'falsy-no-alternate')) +}) + +test('conditional-expression: the same folding applies to a ternary', () => { + getResult(pruneIfBranch, true, join(root, 'conditional-expression')) +}) + +test('lexical-branch-kept: a branch owning `let`/`const` keeps its block', () => { + // Splicing these into the parent list would move block-scoped bindings into the enclosing + // scope. The residue is one redundant nesting level, which delete-nested-blocks removes as a + // separate cleanup - the trade is deliberate and this case is what pins it. + getResult(pruneIfBranch, true, join(root, 'lexical-branch-kept')) +}) + +test('outer-reference-in-dead-branch: removing a branch leaves no stale reference behind', () => { + // The case the other six cannot express, and the one a whole pipeline was defeated by: the dead + // branch references a binding declared OUTSIDE it. The existing cases stay clean only because + // their dead branches declare what they use, so the bindings leave with the branch. + // + // The output text is correct either way - `var keep = 1; return keep;` - so nothing about the + // generated code can catch this. What it asserts is `keep`'s binding no longer holding a + // referencePath into the subtree that was removed, which is the invariant every other visitor + // here is already held to and which four consuming plugins have had to pay a reload for. + getResult(pruneIfBranch, true, join(root, 'outer-reference-in-dead-branch')) +}) + +test('non-constant-test: a test that is not statically decidable is left alone', () => { + // No golden: with `fix` false the helper compares against the input source, which is what + // "left exactly as found" means for a declining case. + getResult(pruneIfBranch, false, join(root, 'non-constant-test')) +}) diff --git a/test/visitor/prune-if-branch/conditional-expression.fix.js b/test/visitor/prune-if-branch/conditional-expression.fix.js new file mode 100644 index 00000000..c1c7f9bb --- /dev/null +++ b/test/visitor/prune-if-branch/conditional-expression.fix.js @@ -0,0 +1,3 @@ +function f() { + return real(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/conditional-expression.js b/test/visitor/prune-if-branch/conditional-expression.js new file mode 100644 index 00000000..8d030e2e --- /dev/null +++ b/test/visitor/prune-if-branch/conditional-expression.js @@ -0,0 +1,3 @@ +function f() { + return "abcde" === "abcde" ? real() : dead(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-not-equal-same.fix.js b/test/visitor/prune-if-branch/dci-not-equal-same.fix.js new file mode 100644 index 00000000..30b5c03a --- /dev/null +++ b/test/visitor/prune-if-branch/dci-not-equal-same.fix.js @@ -0,0 +1,3 @@ +function f() { + real(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-not-equal-same.js b/test/visitor/prune-if-branch/dci-not-equal-same.js new file mode 100644 index 00000000..0e39e513 --- /dev/null +++ b/test/visitor/prune-if-branch/dci-not-equal-same.js @@ -0,0 +1,7 @@ +function f() { + if ("abcde" !== "abcde") { + dead(); + } else { + real(); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-taken-alternate.fix.js b/test/visitor/prune-if-branch/dci-taken-alternate.fix.js new file mode 100644 index 00000000..016bb79c --- /dev/null +++ b/test/visitor/prune-if-branch/dci-taken-alternate.fix.js @@ -0,0 +1,4 @@ +function f() { + real(); + more(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-taken-alternate.js b/test/visitor/prune-if-branch/dci-taken-alternate.js new file mode 100644 index 00000000..fa9b0b73 --- /dev/null +++ b/test/visitor/prune-if-branch/dci-taken-alternate.js @@ -0,0 +1,8 @@ +function f() { + if ("abcde" === "fghij") { + dead(); + } else { + real(); + more(); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-taken-consequent.fix.js b/test/visitor/prune-if-branch/dci-taken-consequent.fix.js new file mode 100644 index 00000000..016bb79c --- /dev/null +++ b/test/visitor/prune-if-branch/dci-taken-consequent.fix.js @@ -0,0 +1,4 @@ +function f() { + real(); + more(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/dci-taken-consequent.js b/test/visitor/prune-if-branch/dci-taken-consequent.js new file mode 100644 index 00000000..9241534a --- /dev/null +++ b/test/visitor/prune-if-branch/dci-taken-consequent.js @@ -0,0 +1,8 @@ +function f() { + if ("abcde" === "abcde") { + real(); + more(); + } else { + dead(); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/falsy-no-alternate.fix.js b/test/visitor/prune-if-branch/falsy-no-alternate.fix.js new file mode 100644 index 00000000..5b9fc7ba --- /dev/null +++ b/test/visitor/prune-if-branch/falsy-no-alternate.fix.js @@ -0,0 +1,3 @@ +function f() { + after(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/falsy-no-alternate.js b/test/visitor/prune-if-branch/falsy-no-alternate.js new file mode 100644 index 00000000..031bba5a --- /dev/null +++ b/test/visitor/prune-if-branch/falsy-no-alternate.js @@ -0,0 +1,6 @@ +function f() { + if ("abcde" === "fghij") { + dead(); + } + after(); +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/lexical-branch-kept.fix.js b/test/visitor/prune-if-branch/lexical-branch-kept.fix.js new file mode 100644 index 00000000..05f57774 --- /dev/null +++ b/test/visitor/prune-if-branch/lexical-branch-kept.fix.js @@ -0,0 +1,7 @@ +function f() { + { + const step = 2; + let total = step + 1; + use(total); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/lexical-branch-kept.js b/test/visitor/prune-if-branch/lexical-branch-kept.js new file mode 100644 index 00000000..d3d973c0 --- /dev/null +++ b/test/visitor/prune-if-branch/lexical-branch-kept.js @@ -0,0 +1,9 @@ +function f() { + if ("abcde" === "abcde") { + const step = 2; + let total = step + 1; + use(total); + } else { + dead(); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/non-constant-test.js b/test/visitor/prune-if-branch/non-constant-test.js new file mode 100644 index 00000000..fb95b20e --- /dev/null +++ b/test/visitor/prune-if-branch/non-constant-test.js @@ -0,0 +1,7 @@ +function f(a, b) { + if (a === b) { + one(); + } else { + two(); + } +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/outer-reference-in-dead-branch.fix.js b/test/visitor/prune-if-branch/outer-reference-in-dead-branch.fix.js new file mode 100644 index 00000000..4c7350d0 --- /dev/null +++ b/test/visitor/prune-if-branch/outer-reference-in-dead-branch.fix.js @@ -0,0 +1,4 @@ +function f() { + var keep = 1; + return keep; +} \ No newline at end of file diff --git a/test/visitor/prune-if-branch/outer-reference-in-dead-branch.js b/test/visitor/prune-if-branch/outer-reference-in-dead-branch.js new file mode 100644 index 00000000..104d8265 --- /dev/null +++ b/test/visitor/prune-if-branch/outer-reference-in-dead-branch.js @@ -0,0 +1,7 @@ +function f() { + var keep = 1; + if ("abcde" !== "abcde") { + helper(keep); + } + return keep; +} \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence.test.js b/test/visitor/split-if-test-sequence.test.js new file mode 100644 index 00000000..04977a5f --- /dev/null +++ b/test/visitor/split-if-test-sequence.test.js @@ -0,0 +1,27 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import splitIfTestSequence from '#visitor/atomic/split-if-test-sequence' + +const root = join(__dirname, 'split-if-test-sequence') + +// The gap split-sequence.js does not cover: it handles ExpressionStatement, ReturnStatement +// and first-VariableDeclarator position, but not an `if` test. + +test('simple-valid', () => { + getResult(splitIfTestSequence, true, join(root, 'simple-valid')) +}) + +test('three-valid', () => { + getResult(splitIfTestSequence, true, join(root, 'three-valid')) +}) + +test('not-sequence-invalid', () => { + getResult(splitIfTestSequence, false, join(root, 'not-sequence-invalid')) +}) + +// The inner `if` is a branch, not a list element, so there is nowhere to insert before it. +// It becomes eligible only once lint-if-statement.js has given it a block. +test('not-in-list-invalid', () => { + getResult(splitIfTestSequence, false, join(root, 'not-in-list-invalid')) +}) diff --git a/test/visitor/split-if-test-sequence/not-in-list-invalid.js b/test/visitor/split-if-test-sequence/not-in-list-invalid.js new file mode 100644 index 00000000..ef856198 --- /dev/null +++ b/test/visitor/split-if-test-sequence/not-in-list-invalid.js @@ -0,0 +1 @@ +if (x) if (a(), b()) c(); \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence/not-sequence-invalid.js b/test/visitor/split-if-test-sequence/not-sequence-invalid.js new file mode 100644 index 00000000..87ebffaf --- /dev/null +++ b/test/visitor/split-if-test-sequence/not-sequence-invalid.js @@ -0,0 +1,3 @@ +if (a()) { + c(); +} \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence/simple-valid.fix.js b/test/visitor/split-if-test-sequence/simple-valid.fix.js new file mode 100644 index 00000000..50f1a1f9 --- /dev/null +++ b/test/visitor/split-if-test-sequence/simple-valid.fix.js @@ -0,0 +1,4 @@ +a(); +if (b()) { + c(); +} \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence/simple-valid.js b/test/visitor/split-if-test-sequence/simple-valid.js new file mode 100644 index 00000000..87babdc4 --- /dev/null +++ b/test/visitor/split-if-test-sequence/simple-valid.js @@ -0,0 +1,3 @@ +if (a(), b()) { + c(); +} \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence/three-valid.fix.js b/test/visitor/split-if-test-sequence/three-valid.fix.js new file mode 100644 index 00000000..2005a2e8 --- /dev/null +++ b/test/visitor/split-if-test-sequence/three-valid.fix.js @@ -0,0 +1,5 @@ +a(); +b(); +if (d()) { + c(); +} \ No newline at end of file diff --git a/test/visitor/split-if-test-sequence/three-valid.js b/test/visitor/split-if-test-sequence/three-valid.js new file mode 100644 index 00000000..1d7e7e2a --- /dev/null +++ b/test/visitor/split-if-test-sequence/three-valid.js @@ -0,0 +1,3 @@ +if (a(), b(), d()) { + c(); +} \ No newline at end of file diff --git a/test/visitor/split-sequence.test.js b/test/visitor/split-sequence.test.js new file mode 100644 index 00000000..4b6b7573 --- /dev/null +++ b/test/visitor/split-sequence.test.js @@ -0,0 +1,16 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import splitSequence from '#visitor/split-sequence' + +const root = join(__dirname, 'split-sequence') + +// Every expression lifted out of the sequence references the outer binding `x`, so the +// helper's reference-state check sees the re-homing rather than only the text change. +test('expression-statement-valid', () => { + getResult(splitSequence, true, join(root, 'expression-statement-valid')) +}) + +test('return-valid', () => { + getResult(splitSequence, true, join(root, 'return-valid')) +}) diff --git a/test/visitor/split-sequence/expression-statement-valid.fix.js b/test/visitor/split-sequence/expression-statement-valid.fix.js new file mode 100644 index 00000000..656a9e2b --- /dev/null +++ b/test/visitor/split-sequence/expression-statement-valid.fix.js @@ -0,0 +1,6 @@ +var x = 1; +function run() { + log(x); + warn(x); + report(x); +} \ No newline at end of file diff --git a/test/visitor/split-sequence/expression-statement-valid.js b/test/visitor/split-sequence/expression-statement-valid.js new file mode 100644 index 00000000..5c712d52 --- /dev/null +++ b/test/visitor/split-sequence/expression-statement-valid.js @@ -0,0 +1,4 @@ +var x = 1; +function run() { + log(x), warn(x), report(x); +} \ No newline at end of file diff --git a/test/visitor/split-sequence/return-valid.fix.js b/test/visitor/split-sequence/return-valid.fix.js new file mode 100644 index 00000000..84c28163 --- /dev/null +++ b/test/visitor/split-sequence/return-valid.fix.js @@ -0,0 +1,6 @@ +var x = 1; +function run() { + log(x); + warn(x); + return x; +} \ No newline at end of file diff --git a/test/visitor/split-sequence/return-valid.js b/test/visitor/split-sequence/return-valid.js new file mode 100644 index 00000000..1510ce04 --- /dev/null +++ b/test/visitor/split-sequence/return-valid.js @@ -0,0 +1,4 @@ +var x = 1; +function run() { + return log(x), warn(x), x; +} \ No newline at end of file diff --git a/test/visitor/split-variable-declaration.test.js b/test/visitor/split-variable-declaration.test.js new file mode 100644 index 00000000..1be10035 --- /dev/null +++ b/test/visitor/split-variable-declaration.test.js @@ -0,0 +1,20 @@ +import { join } from 'path' +import { test } from 'vitest' +import { getVisitorResult as getResult } from '../helper.js' +import splitVariableDeclaration from '#visitor/split-variable-declaration' + +const root = join(__dirname, 'split-variable-declaration') + +// The declarators re-homed into new declarations reference the outer binding `x`. +test('multi-declarator-valid', () => { + getResult(splitVariableDeclaration, true, join(root, 'multi-declarator-valid')) +}) + +test('single-declarator-invalid', () => { + getResult(splitVariableDeclaration, false, join(root, 'single-declarator-invalid')) +}) + +// The scope of a for statement is its body, so the declaration is left alone. +test('for-init-invalid', () => { + getResult(splitVariableDeclaration, false, join(root, 'for-init-invalid')) +}) diff --git a/test/visitor/split-variable-declaration/for-init-invalid.js b/test/visitor/split-variable-declaration/for-init-invalid.js new file mode 100644 index 00000000..575073cc --- /dev/null +++ b/test/visitor/split-variable-declaration/for-init-invalid.js @@ -0,0 +1,4 @@ +var x = 1; +function run() { + for (var i = 0, n = x; i < n; i++) log(i); +} \ No newline at end of file diff --git a/test/visitor/split-variable-declaration/multi-declarator-valid.fix.js b/test/visitor/split-variable-declaration/multi-declarator-valid.fix.js new file mode 100644 index 00000000..779aa39e --- /dev/null +++ b/test/visitor/split-variable-declaration/multi-declarator-valid.fix.js @@ -0,0 +1,7 @@ +var x = 1; +function run() { + var p = log(x); + var q = warn(x); + var r = x; + return p + q + r; +} \ No newline at end of file diff --git a/test/visitor/split-variable-declaration/multi-declarator-valid.js b/test/visitor/split-variable-declaration/multi-declarator-valid.js new file mode 100644 index 00000000..cbd61e98 --- /dev/null +++ b/test/visitor/split-variable-declaration/multi-declarator-valid.js @@ -0,0 +1,7 @@ +var x = 1; +function run() { + var p = log(x), + q = warn(x), + r = x; + return p + q + r; +} \ No newline at end of file diff --git a/test/visitor/split-variable-declaration/single-declarator-invalid.js b/test/visitor/split-variable-declaration/single-declarator-invalid.js new file mode 100644 index 00000000..d6933ba5 --- /dev/null +++ b/test/visitor/split-variable-declaration/single-declarator-invalid.js @@ -0,0 +1,5 @@ +var x = 1; +function run() { + var p = log(x); + return p; +} \ No newline at end of file