webcrack
webcrack copied to clipboard
transform assignment expressions into variable declarations if the variable is declared using a function argument
Example:
function f(a, b, c) {
console.log(b)
b = 1;
c = 2
}
Should produce:
function f(a, b) {
console.log(b)
b = 1;
var c = 2
}
I'll try to implement this myself c:
just a heads-up: I can assist with doing this with babel in general, but it will likely not end up in webcrack directly because it's from a different obfuscator and can't be undone safely 100% of the time (e.g. when someone calls the function with 3 params, or f.length)
This should be possible to safely implement by renaming the 3rd argument to a unused variable. Also anything is unsafe if you consider the fact that functions can just be stringified.
toString() is one of the few exceptions because most build tools, obfuscators, formatters, etc ignore it as well. Safety is a lot harder than it seems when considering scope, control flow, etc:
function f(a) {
setTimeout(() => {
a = 1;
})
console.log(a);
}
function f(a) {
if (condition) {
a = 1;
}
console.log(a);
}
And if it can be done safely somehow, how would it affect readability of all other scripts that weren't obfuscated this way? Remember webcrack is primarily for minified/bundled/obfuscator.io'd scripts which have thousands of normal parameters that can't be distinguished from these ones.
toString() is one of the few exceptions because all build tools, obfuscators, formatters, etc ignore it as well. Well there's 3 cases in regards to
f.length:
- the obfuscator does not modify function arguments => users can rely on
f.length - the obfuscator does modify function arguments but does not fix
f.length=> users cannot rely onf.length - the obfuscator does modify function arguments and fixes
f.length(using Object.defineProperty) => users can rely onf.length
In cases 2 and 3, we can modify the function arguments as we wish since either the user can't rely on them anyway or the obfuscator fixes it for us. js-confuser does number 3.
Safety is a lot harder than it seems because of scope, control flow, etc:
That's true, the "isUsedBefore" check I currently have is a bit naive regarding scopes. Going to convert to draft until I think of a better way than what I currently have.
And if it can be done safely somehow, how would it affect readability of all other scripts that weren't obfuscated this way?
As of now, it affects not a single test. I do not think it will affect a lot of scripts since normally you don't take an argument just to not use it and overwrite it with something else.
In cases 2 and 3, we can modify the function arguments as we wish since either the user can't rely on them anyway or the obfuscator fixes it for us
The problem is we can't reliably distinguish case 1 from 2 and there may not be an obfuscator, so we have to assume the user relies on it.
As of now, it affects not a single test.
Because most of them are unit tests (for a single transform), they won't be affected when adding a new one. And the few integration tests use very short code snippets where parameters aren't reassigned. I recommend trying it with a few Mb of real-world scripts and then looking at the diff with the master branch, can help with finding unintended side-effects:
function f(a) {
!this.aimFov && (a = 1);
}
Example:
function f(a, b, c) { console.log(b) b = 1; c = 2 }Should produce:
function f(a, b) { console.log(b) b = 1; var c = 2 }
@Le0Developer Curious, is there deeper context to this somewhere else? As from face value of the original issue description as described.. I don't understand why that would be a valid generalised transformation to make?
Theres no context, an obfuscator does this and it‘s interfering with webcrack because it only scans for VariableDeclaration, not these AssigntmentExpressions.
Since theres a lot of edge cases, I’m currently testing matching on assignment expressions in each transformer instead: https://github.com/Le0Developer/webcrack/commit/27d6a34d554bad81845c677f1005e9d3ad37278f
PR based on that way follow and then we can close this and #161
Theres no context, an obfuscator does this and it‘s interfering with webcrack
@Le0Developer Fair enough. I guess what I was trying to understand is why, based on that example the c should be removed from the params and turned into a var.
I'm sure I'm missing some potentially basic reasoning/understanding here that isn't explicitly spelled out in this issue; but that's the part I'm asking about as the 'context'.
Edit: Attempting to infer the context with ChatGPT 4o:
The logic behind their request seems to be related to JavaScript’s function parameter handling and scope behavior.
Reasoning:
JavaScript Does Not Re-declare Function Parameters
- In the original function:
function f(a, b, c) { console.log(b) b = 1; c = 2 }
a,b, andcare function parameters.- Parameters act like locally declared variables within the function.
- Assigning a new value to
borcdoes not create a new variable; it simply reassigns the existing parameter.Minimization of Function Parameters
- If
cis assigned but never used as a parameter, it means it does not need to be a function parameter at all.- The request suggests removing
cfrom the function's parameter list.Declaring
cas a Local Variable
- When
cis removed from the parameter list but is still assigned a value within the function, it needs to be explicitly declared as a local variable usingvar:var c = 2;- This prevents
cfrom becoming an implicit global variable (which would happen in non-strict mode).Keeping
bin the Parameter List
bis referenced before being reassigned (console.log(b);), meaning it must remain a function parameter.- If
bwere removed,console.log(b)would reference an undefined variable.Final Outcome:
Thus, they propose transforming:
function f(a, b, c) { console.log(b) b = 1; c = 2 }Into:
function f(a, b) { console.log(b) b = 1; var c = 2 }This change optimizes the function by:
- Removing unused parameters (
c).- Ensuring
cremains local by declaring it withvar.This is a logical optimization because it avoids redundant parameters while maintaining the same functionality.
This behavior applies specifically to primitive parameters (like numbers, strings, and booleans), but not to objects or arrays. Let's break it down:
1. Primitive Parameters (Numbers, Strings, Booleans)
- When you pass a primitive value as an argument, JavaScript copies it.
- Reassigning the parameter inside the function does not affect the original argument outside.
Example:
function f(a, b, c) { console.log(b); // Prints original `b` b = 1; // Reassigns `b` locally (does not affect the argument) c = 2; // Reassigns `c` locally (does not affect the argument) } let x = 10; let y = 20; let z = 30; f(x, y, z); console.log(x, y, z); // Still prints: 10, 20, 30Since
cis not used as a parameter (just reassigned), it can be removed from the function signature and declared asvar c = 2.2. Object and Array Parameters
- Objects and arrays are passed by reference (or, more precisely, by reference to their memory location).
- Reassigning an object or array parameter does affect the original value if you modify its properties, but not if you reassign the parameter itself.
Example: Modifying an Object's Property (Affects the Original)
function modifyObject(obj) { obj.name = "Modified"; // Modifies the original object } let person = { name: "Original" }; modifyObject(person); console.log(person.name); // "Modified"
- The change persists because
objrefers to the same memory location.Example: Reassigning an Object (Does NOT Affect the Original)
function reassignObject(obj) { obj = { name: "New Object" }; // This creates a new object (does not affect the original) } let person = { name: "Original" }; reassignObject(person); console.log(person.name); // "Original"
- The change does not persist because
objwas reassigned inside the function, but this does not modify the original object.Implication for the Unminimiser
- The rule of removing parameters only applies to primitives, where reassigning them inside the function does not affect the original argument.
- For objects or arrays, removing parameters could change behavior, since modifying their properties does persist outside the function.
Example Where Removing a Parameter Would Be Incorrect
function modifyArray(arr) { arr.push(4); // Modifies the original array } let numbers = [1, 2, 3]; modifyArray(numbers); console.log(numbers); // [1, 2, 3, 4]
- If
arrwere removed as a parameter and declared as avarinside the function, this would completely change the behavior by creating a new local array instead of modifying the original.Conclusion
- ✅ For primitives: Removing unused parameters and declaring them with
varis safe.- ❌ For objects and arrays: Removing parameters could break the function’s behavior if it modifies the original object or array.
Would you like me to help refine the logic for your unminimiser based on these findings?