diff --git a/packages/webcrack/src/deobfuscate/merge-object-assignments.ts b/packages/webcrack/src/deobfuscate/merge-object-assignments.ts index a65e4d8e..b6a97b03 100644 --- a/packages/webcrack/src/deobfuscate/merge-object-assignments.ts +++ b/packages/webcrack/src/deobfuscate/merge-object-assignments.ts @@ -1,8 +1,8 @@ -import type { Binding } from '@babel/traverse'; +import type { Binding, NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import type { Transform } from '../ast-utils'; -import { constObjectProperty, safeLiteral } from '../ast-utils'; +import { constObjectProperty, findParent, safeLiteral } from '../ast-utils'; /** * Merges object assignments into the object expression. @@ -78,7 +78,8 @@ export default { // Example: const obj = { foo: 'bar' }; return obj; -> return { foo: 'bar' }; if ( binding.references === 1 && - inlineableObject.match(object.current) + inlineableObject.match(object.current) && + !isRepeatedCallReference(binding, binding.referencePaths[0]) ) { binding.referencePaths[0].replaceWith(object.current); path.remove(); @@ -103,6 +104,28 @@ function hasCircularReference(node: t.Node, binding: Binding) { ); } +const repeatedCallMatcher = m.or( + m.forStatement(), + m.forOfStatement(), + m.forInStatement(), + m.whileStatement(), + m.doWhileStatement(), + m.function(), + m.objectMethod(), + m.classBody(), +); + +/** + * Returns true when the reference can be evaluated multiple times. + * In that case, the object should not be inlined to avoid creating multiple instances. + * Structure: Block{ binding, Repeatable{reference} } + */ +function isRepeatedCallReference(binding: Binding, reference: NodePath) { + const block = binding.scope.getBlockParent().path; + const repeatable = findParent(reference, repeatedCallMatcher); + return repeatable?.isDescendant(block); +} + /** * Only literals, arrays and objects are allowed because variable values * might be different in the place the object will be inlined. diff --git a/packages/webcrack/src/deobfuscate/test/merge-object-assignments.test.ts b/packages/webcrack/src/deobfuscate/test/merge-object-assignments.test.ts index 1391b2ae..1e5d337a 100644 --- a/packages/webcrack/src/deobfuscate/test/merge-object-assignments.test.ts +++ b/packages/webcrack/src/deobfuscate/test/merge-object-assignments.test.ts @@ -61,3 +61,143 @@ test('ignore call with possible circular reference', () => const obj = {}; obj.foo = fn(); `)); + +test('do not inline object into function', () => + expectJS(` + const obj = {}; + obj.foo = 1; + function f() { + return obj; + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + function f() { + return obj; + } + `)); + +test('do not inline object into arrow function', () => + expectJS(` + const obj = {}; + obj.foo = 1; + const f = () => obj; + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + const f = () => obj; + `)); + +test('do not inline object into method', () => + expectJS(` + const obj = {}; + obj.foo = 1; + const obj2 = { f() { return obj; } }; + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + const obj2 = { + f() { + return obj; + } + }; + `)); + +test('do not inline object into class', () => + expectJS(` + const obj = {}; + obj.foo = 1; + class C { + f = obj; + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + class C { + f = obj; + } + `)); + +test('do not inline object into while-loop', () => + expectJS(` + const obj = {}; + obj.foo = 1; + while (i < 2) { + arr.push(obj); + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + while (i < 2) { + arr.push(obj); + } + `)); + +test('do not inline object into do-while-loop', () => + expectJS(` + const obj = {}; + obj.foo = 1; + do { + arr.push(obj); + } while (i < 2); + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + do { + arr.push(obj); + } while (i < 2); + `)); + +test('do not inline object into for-loop', () => + expectJS(` + const obj = {}; + obj.foo = 1; + for (let i = 0; i < 2; i++) { + arr.push(obj); + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + for (let i = 0; i < 2; i++) { + arr.push(obj); + } + `)); + +test('do not inline object into for-of-loop', () => + expectJS(` + const obj = {}; + obj.foo = 1; + for (const item of items) { + arr.push(obj); + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + for (const item of items) { + arr.push(obj); + } + `)); + +test('do not inline object into for-in-loop', () => + expectJS(` + const obj = {}; + obj.foo = 1; + for (const key in [1, 2]) { + arr.push(obj); + } + `).toMatchInlineSnapshot(` + const obj = { + foo: 1 + }; + for (const key in [1, 2]) { + arr.push(obj); + } + `));