MichaelXF / js-confuser

JS-Confuser is a JavaScript obfuscation tool to make your programs *impossible* to read.
https://js-confuser.com
MIT License
193 stars 29 forks source link

Get Global Improvements #134

Open MichaelXF opened 1 month ago

MichaelXF commented 1 month ago

Implement @doctor8296's trick to detect tampering to the global object. Securing the global object is a great protection for fighting against monkey patching.

let check = false;
eval("check = true")
if (!check) {
    throw new Error("Eval was redefined")
}

const root = eval("this");

Note: This only works when Eval is enabled and not in Strict mode.

New configuration settings will need to be added like javascript-obfuscator's target option, indicating if Eval is allowed and Strict Mode. Additionally, the lock.countermeasures function should be called upon tamper detection.

doctor8296 commented 1 month ago

It is not quite helping a lot against monkey patching in general, it is helping against very specific rare case when hacker try to proxify global object.

By the way it can be written differently:

let root = {};
eval("root=this"); // note that variable name can be changed by "rename variables" property 
// if eval wasn't redefined root will be a global object

If you want to learn a bit more about monkey patching I can give you access to my "old" repository with my client side anit-cheat solution with detailed description of some of the protection methods and attack methods. Also I can give you a link to the huge discussion about monkey patching and proxy testing.

Also this eval "trick" can pretty much help with safely creating any code with Function (RGF).

doctor8296 commented 1 month ago

Here eval that pretty much acts as Function with no prototype (cannot use "new" and other stuff that prototype does)

// Function
// CANNOT be used for getting prototype
// does NOT protect your source from reading if eval was redefined, but source cannot be rewritten
function Function(...args) {
    let s = "function anonymous(";
    for (let i=0; i<args.length - 2; i++) {
        s += args[i] + ',';
    }
    s += args[args.length - 2] + "\n) {\n" + args[args.length - 1] + "\n}";
    eval(s);
    return anonymous; // can cause errors because editors cannot actually see where it is defined
}

// Function V2
// CANNOT be used for getting prototype
// Protects source code from reading and rewriting, but if eval was redefined will return null;
function Function(...args) {
    let c = false;
    eval((_ => {for (const key in {c}) return key})() + '=!0'); // prevent variable name change by obfuscator
    if (!c) {
        return null;
    }
    let s = "function anonymous(";
    for (let i=0; i<args.length - 2; i++) {
        s += args[i] + ',';
    }
    s += args[args.length - 2] + "\n) {\n" + args[args.length - 1] + "\n}";
    eval(s);
    return anonymous;
}

image

MichaelXF commented 1 month ago

If you want to learn a bit more about monkey patching I can give you access to my "old" repository with my client side anit-cheat solution with detailed description of some of the protection methods and attack methods. Also I can give you a link to the huge discussion about monkey patching and proxy testing.

That would be great!

Also this eval "trick" can pretty much help with safely creating any code with Function (RGF).

You are right. Using this method we can pass the runtime generated into the secure eval. This prevents any chance for a reverse engineer to inspect the code. I'm thinking of creating a lock.tamperProtection option that will implement these features.

Here is the prototype: https://github.com/MichaelXF/js-confuser/blob/dev/docs/TamperProtection.md

j4k0xb commented 3 weeks ago

This monkey-patch can be detected by inspecting the fetch.toString() value

can be bypassed:

const { toString } = Function.prototype;
const hiddenFunctions = new Map();
Function.prototype.toString = function () {
  return toString.call(hiddenFunctions.get(this) || this);
};
hiddenFunctions.set(Function.prototype.toString, toString);

const _fetch = fetch;
fetch = (url, ...args) => {
  console.log("Fetch request intercepted!", url, ...args);
  return _fetch(url, ...args);
};
hiddenFunctions.set(fetch, _fetch);

console.log(Function.prototype.toString.toString()); // function toString() { [native code] }
console.log(fetch.toString()); // function fetch() { [native code] }
MichaelXF commented 3 weeks ago

@j4k0xb Great find, at least there is some protection. The error stack changes so maybe we have to looks for patterns in it.

// Untampered
Function.prototype.toString.call(undefined)
//index.js:161 Uncaught TypeError: Function.prototype.toString requires that 'this' be a Function
//    at toString (<anonymous>)
//    at index.js:161:71
//    at <anonymous>:1:29

// Tampered
Function.prototype.toString.call(undefined)
//index.js:161 Uncaught TypeError: Function.prototype.toString requires that 'this' be a Function
//    at toString (<anonymous>)
//    at index.js:161:71
//    at Function.toString (<anonymous>:4:19)  // <- Unusual trace here 
//    at <anonymous>:1:29
doctor8296 commented 3 weeks ago

Indeed. But there's work arounds. Stack is not consistent thing and changes from browser to browser since it is experimental feature. So we unlikely can be 100% sure what is going on, if not mapping all possible ways of stack check.

Call can be tempered.

Stack can be tempered on FireFox.

We don't know how signature should really look like. For example FireFox has different signatures for native functions.