From 27724266882dcd3796833110c7a80f894c56e30b Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Sun, 18 Aug 2024 20:14:45 +0200 Subject: [PATCH] This one simple trick unlocked the next level of obfuscation --- src/normalize/normalize.mjs | 25 +++++++- .../builtins_cases/empty_string_replace.md | 62 +++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 tests/cases/builtins_cases/empty_string_replace.md diff --git a/src/normalize/normalize.mjs b/src/normalize/normalize.mjs index 697131c112..359aa05d79 100644 --- a/src/normalize/normalize.mjs +++ b/src/normalize/normalize.mjs @@ -2826,6 +2826,29 @@ export function phaseNormalize(fdata, fname, { allowEval = true }) { } } } + + if (!callee.computed && AST.isPrimitive(callee.object) && AST.getPrimitiveValue(callee.object) === '') { + // Property access on empty string... Some silly low hanging fruit cases. + if (ASSUME_BUILTINS) { + // Targeting a specific obfuscation: ``.replace(/^/, String) + if (callee.property.name === 'replace' && node.arguments[1].type === 'Identifier' && node.arguments[1].name === 'String') { + // This will invariably return the empty string + + riskyRule('Calling .replace on an empty string with irrelevant function always results in empty string'); + example('"".replace(/$/, String)', '$coerce(/$/); ""'); + before(body[i]); + + const finalNode = AST.primitive(''); + const finalParent = wrapExpressionAs(wrapKind, varInitAssignKind, varInitAssignId, wrapLhs, varOrAssignKind, finalNode); + body.splice(i, 1, AST.expressionStatement(AST.callExpression('$coerce', [node.arguments[1], AST.primitive('string')])), finalParent); + + after(body[i]); + after(body[i + 1]); + + return true; + } + } + } } // Simple member expression is atomic callee. Can't break down further since the object can change the context. @@ -3621,7 +3644,7 @@ export function phaseNormalize(fdata, fname, { allowEval = true }) { ); const finalParent = wrapExpressionAs(wrapKind, varInitAssignKind, varInitAssignId, wrapLhs, varOrAssignKind, finalNode); - body.splice(i, 1, finalParent); + body[i] = finalParent; after(body[i]); return true; diff --git a/tests/cases/builtins_cases/empty_string_replace.md b/tests/cases/builtins_cases/empty_string_replace.md new file mode 100644 index 0000000000..ff36c42680 --- /dev/null +++ b/tests/cases/builtins_cases/empty_string_replace.md @@ -0,0 +1,62 @@ +# Preval test case + +# empty_string_replace.md + +> Builtins cases > Empty string replace +> +> + +## Input + +`````js filename=intro +$(''.replace(/^/, String)); +````` + +## Pre Normal + + +`````js filename=intro +$(``.replace(/^/, String)); +````` + +## Normalized + + +`````js filename=intro +const tmpCallCallee = $; +const tmpCalleeParam$1 = /^/; +const tmpCalleeParam$3 = String; +const tmpCalleeParam = ``.replace(tmpCalleeParam$1, tmpCalleeParam$3); +tmpCallCallee(tmpCalleeParam); +````` + +## Output + + +`````js filename=intro +$(``); +````` + +## PST Output + +With rename=true + +`````js filename=intro +$( "" ); +````` + +## Globals + +None + +## Result + +Should call `$` with: + - 1: '' + - eval returned: undefined + +Pre normalization calls: Same + +Normalized calls: Same + +Final output calls: Same