webcrack icon indicating copy to clipboard operation
webcrack copied to clipboard

transform assignment expressions into variable declarations if the variable is declared using a function argument

Open Le0Developer opened this issue 8 months ago • 8 comments

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:

Le0Developer avatar Mar 08 '25 14:03 Le0Developer

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)

j4k0xb avatar Mar 08 '25 14:03 j4k0xb

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.

Le0Developer avatar Mar 08 '25 14:03 Le0Developer

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.

j4k0xb avatar Mar 08 '25 15:03 j4k0xb

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:

  1. the obfuscator does not modify function arguments => users can rely on f.length
  2. the obfuscator does modify function arguments but does not fix f.length => users cannot rely on f.length
  3. the obfuscator does modify function arguments and fixes f.length (using Object.defineProperty) => users can rely on f.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.

Le0Developer avatar Mar 08 '25 15:03 Le0Developer

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:

Image

function f(a) {
  !this.aimFov && (a = 1);   
}

j4k0xb avatar Mar 08 '25 15:03 j4k0xb

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?

0xdevalias avatar Mar 10 '25 05:03 0xdevalias

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

Le0Developer avatar Mar 10 '25 06:03 Le0Developer

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:

  1. 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, and c are function parameters.
      • Parameters act like locally declared variables within the function.
      • Assigning a new value to b or c does not create a new variable; it simply reassigns the existing parameter.
  2. Minimization of Function Parameters

    • If c is assigned but never used as a parameter, it means it does not need to be a function parameter at all.
    • The request suggests removing c from the function's parameter list.
  3. Declaring c as a Local Variable

    • When c is removed from the parameter list but is still assigned a value within the function, it needs to be explicitly declared as a local variable using var:
      var c = 2;
      
    • This prevents c from becoming an implicit global variable (which would happen in non-strict mode).
  4. Keeping b in the Parameter List

    • b is referenced before being reassigned (console.log(b);), meaning it must remain a function parameter.
    • If b were 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 c remains local by declaring it with var.

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, 30

Since c is not used as a parameter (just reassigned), it can be removed from the function signature and declared as var 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 obj refers 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 obj was 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 arr were removed as a parameter and declared as a var inside 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 var is 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?

0xdevalias avatar Mar 10 '25 07:03 0xdevalias