Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detecting tamper via strict comparison to native function pulled from iframe #1

Open
asger-finding opened this issue Jul 4, 2022 · 55 comments

Comments

@asger-finding
Copy link

It's possible to create a new iframe (about:blank) and pull functions from its Document, then comparing it to the tampered function. This is, for example, being done in Krunker to detect function tamper. CanvasBlocker has some good methods to address this.

@hrt
Copy link
Owner

hrt commented Jul 4, 2022

Yes, if you are hooking functions you should also hook all iframes.

Fun fact: Krunker started doing this after I introduced the use of it in above repo.

@asger-finding
Copy link
Author

Yes, if you are hooking functions you should also hook all iframes.

I feel it should also be addressed in this repository, as it's certainly another way to detect tamper.

@doctor8296
Copy link

doctor8296 commented Jan 18, 2023

This is hackable by Object.defineProperty + Proxy.
I hacked the krunker after reading this repo)
I simply defined Function, and checked the stack trace to return proxy, only on game file evaluation.
And it's been working for a year now.
After all of that I came up with conclusion that everthing is hackable.
I worked hard for the whole 3 days, and just don't see any solutons.

@hrt
Copy link
Owner

hrt commented Feb 15, 2023

This is hackable by Object.defineProperty + Proxy.

@redscheme I would like to see an example user script that hooks Array.prototype.join in anyway and gets the pass on https://hrt.github.io/TamperDetectJS/ with chrome.

From what I've understood from your explanation, it wouldn't get the pass.

@oxygen-x
Copy link

This is hackable by Object.defineProperty + Proxy. I hacked the krunker after reading this repo) I simply defined Function, and checked the stack trace to return proxy, only on game file evaluation. And it's been working for a year now. After all of that I came up with conclusion that everthing is hackable. I worked hard for the whole 3 days, and just don't see any solutons.

I'm going to have to agree with hrt here, were you using any other browsers? I've tried to recreate your explanation and It does not work.

@doctor8296
Copy link

doctor8296 commented Apr 19, 2023

@hrt @Kepler-11
Hi,
Despite the fact that each check can be passed pretty easy, the fastest way to hack current scheme is this:

const join = Array.prototype.join;
Object.defineProperty(Array.prototype, 'join', {
    get() {
       // check "this" or stack to replace the function only when its needed or replace function with original only on checks
        if ( ... ) return new Proxy(join, { ... })
        return join;
    }
});

@hrt
Copy link
Owner

hrt commented Apr 20, 2023

@redscheme this means that it wouldn't be hooking join.

All krunker or whoever would do is storejoin in a variable, do whatever checks (which you haven't hooked to get passed the checks) and continue using the stored join that passed the checks.

Your get logic only applies to when the page first stores to a variable.

I may adjust the repo to specifically request the user to hook join to return a unique value that can be checked to ensure the user is actually hooking join. This will let the user be sure that they're hooking join rather than not hooking at all.

@doctor8296
Copy link

@hrt yes you right, you can easy avoid this just by calling getter once before checks. But it is pretty difficult to store each class and method
separately somewhere and force the new global scope. Difficult but not impossible.

I still believe that it is quite possible to bypass these checks, and sooner or later I will do it, since I have seriously dealt with this topic.

@doctor8296
Copy link

doctor8296 commented Sep 21, 2023

That was easy.
It took me 15 minutes.

const Reflect = {
    apply: window.Reflect.apply
};

const console = {log: window.console.log};
window.console.log = () => {};

const originalJoin = Array.prototype.join;
const joinProxy = Array.prototype.join = new Proxy(Array.prototype.join, {});

const originalDateToDateString = Date.prototype.toDateString;
const originalDatetoString = Date.prototype.toString;
const dateToStringProxy = Date.prototype.toString = new Proxy(Date.prototype.toString, {
    apply(target, thisArg, args) {
        switch (thisArg) {
            case joinProxy: {
                throw {stack: '' + originalJoin};
                break;
            }
            case toStringProxy: {
                throw {stack: '' + originalToString};
                break;
            }
            case originalDateToDateString: {
                throw {stack: '' + originalDateToDateString};
                break;
            }
        }
        return Reflect.apply(...arguments);
    }
});

const originalToString = Function.prototype.toString;
const toStringProxy = Function.prototype.toString = new Proxy(Function.prototype.toString, {
    apply(target, thisArg, args) {
        switch (thisArg) {
            case joinProxy: {
                arguments[1] = originalJoin;
                break;
            }
            case toStringProxy: {
                arguments[1] = originalToString;
                break;
            }
            case dateToStringProxy: {
                arguments[1] = originalDatetoString;
                break;
            }
        }
        return Reflect.apply(...arguments);
    }
});

Object.create = new Proxy(Object.create, {
    apply(target, thisArg, args) {
        if (args[0] === joinProxy) throw {stack: ''};
        return Reflect.apply(...arguments);
    }
});

I got back to this theme a week ago. I spent a lot of time on iframe / html render / fight for the event loop etc, but nothing.. and after I losing all hope I got back here just to solve this puzzle for fun and maybe to see something interesting.
Seems I figure something out. The stack of error CANNOT be redefined. I thought it could before, but it seems, if you not calling error through other redefinable functions you cannot reach it. I was really surprized by this fact. Anyway, a week of researches gave me very little bit that I cannot actually use in my goal to create client sided anticheat / checker for redefinitions. Sad. But I'll look into the stack thing.

@hrt
Copy link
Owner

hrt commented Sep 21, 2023

@doctor8296 Good one.

You've beaten it. If I were to make it harder, I'd first make the checks on the stack traces a little stronger (Proxy is seen everywhere) 😂

(index):152 check_stack_5_toString
----
TypeError: Method Date.prototype.toDateString called on incompatible receiver function toString() { [native code] }
    at Proxy.toDateString (<anonymous>)
    at Array.check_stack_5_toString (https://hrt.github.io/TamperDetectJS/:149:33)
    at https://hrt.github.io/TamperDetectJS/:487:33

@doctor8296
Copy link

Well, I mean yeah, you can add extra checks, but I think it just will took me more time to hack it. I am currently looking into the stack thing. I think with this I have a chance to implement some iframe + document.write logic.

@doctor8296
Copy link

@hrt WE CAN CHECK EVERYTHING WITH CLEAR toString!

@hrt
Copy link
Owner

hrt commented Oct 10, 2023

@doctor8296

Not sure I understood that.


I likely read your comment wrong but I should also add delete Function.prototype.something where something can be a lot of prototype functions for example toString which would restore the native function or at least let you compare against the native function.

@doctor8296
Copy link

doctor8296 commented Oct 10, 2023

@hrt
Yes that what I meant. Did you know about such behavior? I just figure out this today, playing with primitive prototype redefinition, and figured out that it restores itself after deleting, as well as prototype.toString.
So does that mean that the topic completely closed? From my point of view having clear toString solves every our problem, without huge mass of different checks.

@hrt
Copy link
Owner

hrt commented Oct 10, 2023

@doctor8296 not necessarily:
image

@doctor8296
Copy link

doctor8296 commented Oct 10, 2023

@hrt but delete returns false in this case, and this how we can check :P

@doctor8296
Copy link

@hrt never mind 😢
image

@hrt
Copy link
Owner

hrt commented Oct 10, 2023

Right so v8 is likely replacing toString with some other toString function.

Anyway if someone really wanted to get around it, they could try to mimic the entirety of Function (incomplete example incoming):
image

@doctor8296
Copy link

doctor8296 commented Oct 10, 2023

@hrt
Yes, I foresaw replacing Function with the fake one.

const f = _=>0;
if (!(delete f.constructor)) {
    throw "Function constructor was predefined";
}
const _Function = f.constructor;
if (!(f instanceof _Function)) {
    throw "Function has illegal redefined constructor";
}

this check should avoid such actions

@doctor8296
Copy link

doctor8296 commented Oct 10, 2023

But yeah, the fact that Function.prototype.toString getting replaced with not working one is heart breaking. :(
Anyway I have very hard but 100% solution for chromium browsers, but it still hackable on FF.
Clear toString could solve every our problem ... 😭😭😭

I'll come back if will find some other actually working solutions...

By the way such deleting logic based on this:

class Parent {
    method() {}
}

class Child extends Parent {
    method() {
        super.method();
    }
}

Parent.prototype.method === Child.prototype.method; // false
delete Child.prototype.method
Parent.prototype.method === Child.prototype.method; // true

And the thing with constructor that I showed doesn't work as well... (but checking through instanceof works)

@doctor8296
Copy link

doctor8296 commented May 28, 2024

Btw you can checkout anticheat on cryzen.io that I made.
It seems, that scripts with default inject mode, and with document-start run only after scripts in
Basically you can just freeze all object and functions and that's it.
But if somehow (I know how >:3 ) script will run before that, this will not help you

@nostopgmaming17
Copy link

that doesnt actually work if you try to do

let iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
console.log(iframe.contentWindow.Function.prototype.bind==Function.prototype.bind)

it will always be false
maybe im doing something wrong idk

@nostopgmaming17
Copy link

anyway this is my full bypass to the anticheat you made

bypass.js (in chrome extension since tampermonkey doesnt instantly inject for some reason)

(function() {
    'use strict';

    const spoof = new WeakMap;
    spoof.set = spoof.set;
    spoof.get = spoof.get;
    spoof.has = spoof.has;
    spoof.delete = spoof.delete;

    const reflect = {};
    for (let i of Object.getOwnPropertyNames(Reflect)) {
        reflect[i] = Reflect[i];
    }
    const proxy = Proxy;

    const hook = (o,n,h) => {
        const hooked = new proxy(o[n],h);
        spoof.set(hooked,o[n]);
        o[n] = hooked;
    }

    hook(Function.prototype,"toString",{
        apply(f,th,args){
            try{
                return reflect.apply(f,spoof.get(th)||th,args);
            }catch(err) {
                err.stack = err.stack.replace(/^.+bypass\.js.+\n/gm,"");
                err.stack = err.stack.replace(/Object/m,"Function");
                throw err;
            }
        }
    });
    hook(Function.prototype,"apply",{
        apply(f,th,args){
            try{
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/^.+bypass\.js.+\n/gm,"");
                if (spoof.has(args[0])) {
                    err.stack = err.stack.replace(/Proxy/gm,"Function");
                    throw err;
                } else {
                    throw err;
                }
            }
        }
    });

    hook(Array.prototype,"join",{
        apply(f,th,args) {
            try{
                if (th instanceof Array) {
                    for(let i of th) {
                        if (typeof i == "object" && typeof i.toString != "undefined" && i.toString.toString().includes("try")) {
                            i.toString = ()=>"";
                        }
                    }
                }
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/^.+bypass\.js.+\n/gm,"");
                throw err;
            }
        }
    });
})();

keep in mind that there are other elegant solutions but which allow for easy false positive checks which could flag you
also you would need to change /^.+bypass.js.+\n/gm to match for your file name

also heads up none of that fancy stuff like Uint8Array.prototype.sort is needed
you can simply do

void function() {
 try{
  ""();
  detected();
 } catch(error) {
  if (error.stack.includes("Proxy")) {detected()}
 }
}.call(Array.prototype.join);

NOTE: using Error.prepareStackTrace is not only irrational and stupid but it is a massive insecurity risk which the anticheat devs could use to implement false positives and ultimately banning the client
this is because Error.prepareStackTrace only gets the error message and cant get further info about the error youre handlnig

using a call/apply hook is much better because you can check if its a hooked function

@doctor8296
Copy link

doctor8296 commented Jul 17, 2024

that doesnt actually work if you try to do

let iframe = document.createElement("iframe");
iframe.style.display = "none";
document.body.appendChild(iframe);
console.log(iframe.contentWindow.Function.prototype.bind==Function.prototype.bind)

it will always be false maybe im doing something wrong idk

@nostopgmaming17 Hi!
Well it doesn't work like that, since "Function" itself is instance, and as you know {} === {} will give you false every time.
The way we are trying to compare those functions with iframe is by using "clear" function for example for comparing their signatures that we could achive by:

const clearWindow = iframe.contentWindow;
const clearToString = clearWindow.Function.prototype.toString;
const clearJoinSignature = clearToString.call(clearWindow.Array.prototype.join);
const globalJoinSignature = clearToString.call(Array.prototype.join);

if (clearJoinSignature !== globalJoinSignature) {
    alert("Array.prototype.join was redefined!")
}

@doctor8296
Copy link

void function() {
 try{
  ""();
  detected();
 } catch(error) {
  if (error.stack.includes("Proxy")) {detected()}
 }
}.call(Array.prototype.join);

@nostopgmaming17 can I ask why did you place detected call after ""() is there any case where ""() will not throw an error? Also, I prefer to use 0() since there less symbols.

@nostopgmaming17
Copy link

void function() {
 try{
  ""();
  detected();
 } catch(error) {
  if (error.stack.includes("Proxy")) {detected()}
 }
}.call(Array.prototype.join);

@nostopgmaming17 can I ask why did you place detected call after ""() is there any case where ""() will not throw an error? Also, I prefer to use 0() since there less symbols.

looking back at it I dont know i guess its just incase, also you would probably have it after the function call like that

void function() {
 try{
  ""();
 } catch(error) {
  if (error.stack.includes("Proxy")) {detected()}
 }
}.call(Array.prototype.join);
detected();

this is because Function.prototype.call can be hooked

@doctor8296
Copy link

@nostopgmaming17 yes everything can be hooked.
Except few cases.

@nostopgmaming17
Copy link

nostopgmaming17 commented Jul 17, 2024

@nostopgmaming17 yes everything can be hooked. Except few cases.

new bypass with HTMLIFrameElement contentWindow getter

(function() {
    'use strict';

    const spoof = new WeakMap;
    spoof.set = spoof.set;
    spoof.get = spoof.get;
    spoof.has = spoof.has;
    spoof.delete = spoof.delete;


    const reflect = {};
    for (let i of Object.getOwnPropertyNames(Reflect)) {
        reflect[i] = Reflect[i];
    }
    const proxy = Proxy;

    const hook = (o,n,h) => {
        const hooked = new proxy(o[n],h);
        spoof.set(hooked,o[n]);
        o[n] = hooked;
    }

    hook(Function.prototype,"toString",{
        apply(f,th,args){
            try{
                return reflect.apply(f,spoof.get(th)||th,args);
            }catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                err.stack = err.stack.replace(/Object/m,"Function");
                throw err;
            }
        }
    });
    const descriptor = reflect.getOwnPropertyDescriptor(HTMLIFrameElement.prototype,"contentWindow");
    hook(descriptor,"get",{
        apply(f,th,args) {
            const ret = reflect.apply(f,th,args);
            if (!spoof.has(ret.Function.prototype.toString)) {
                hook(ret.Function.prototype,"toString",{
                    apply(f,th,args){
                        try{
                            return reflect.apply(f,spoof.get(th)||th,args);
                        }catch(err) {
                            err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                            err.stack = err.stack.replace(/Object/m,"Function");
                            throw err;
                        }
                    }
                });
            }
            return ret;
        }
    })
    reflect.defineProperty(HTMLIFrameElement.prototype,"contentWindow",descriptor);

    hook(Function.prototype,"apply",{
        apply(f,th,args){
            try{
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                if (spoof.has(args[0])) {
                    err.stack = err.stack.replace(/Proxy/gm,"Function");
                    throw err;
                } else {
                    throw err;
                }
            }
        }
    });

    hook(Array.prototype,"join",{
        apply(f,th,args) {
            try{
                if (th instanceof Array) {
                    for(let i of th) {
                        if (typeof i == "object" && typeof i.toString != "undefined" && i.toString.toString().includes("try")) {
                            i.toString = ()=>"";
                        }
                    }
                }
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                throw err;
            }
        }
    });
})();

@doctor8296
Copy link

@nostopgmaming17 contentWindow will not get called if I will take it from ’window[n]’

@nostopgmaming17
Copy link

@nostopgmaming17 contentWindow will not get called if I will take it from ’window[n]’

wdym?

@doctor8296
Copy link

@nostopgmaming17
Copy link

@nostopgmaming17 https://html.spec.whatwg.org/#accessing-other-browsing-contexts

if you mean by setting src and having it run the checks there, then i can make a src getter setter too

@doctor8296
Copy link

@nostopgmaming17 no, not the src.
Read the specification I linked.
You can access contentWindow by window[0].

@nostopgmaming17
Copy link

@nostopgmaming17 no, not the src. Read the specification I linked. You can access contentWindow by window[0].

oh that works i guess you could use it

@doctor8296
Copy link

doctor8296 commented Jul 17, 2024

@nostopgmaming17 no, you can just use it, it would be tooo easy to protect the code like that, but unfortunately no.
Iframe can get handled by MutationObserver or, if you are adding it from code, you can patch the function for creating/adding element.

But there are some cases when you can actually safely create the iframe, but it doesn't work on firefox.

@nostopgmaming17
Copy link

@nostopgmaming17 no, you can just use it, it would be tooo easy to protect the code like that, but unfortunately no. Iframe can get handled by MutationObserver or, if you are adding it from code, you can patch the function for creating/adding element.

But there are some cases when you can actually safely create the iframe, but it doesn't work on firefox.

(function() {
    'use strict';

    const spoof = new WeakMap;
    spoof.set = spoof.set;
    spoof.get = spoof.get;
    spoof.has = spoof.has;
    spoof.delete = spoof.delete;


    const reflect = {};
    for (let i of Object.getOwnPropertyNames(Reflect)) {
        reflect[i] = Reflect[i];
    }
    const proxy = Proxy;

    const hook = (o,n,h) => {
        const hooked = new proxy(o[n],h);
        spoof.set(hooked,o[n]);
        o[n] = hooked;
    }

    hook(Function.prototype,"toString",{
        apply(f,th,args){
            try{
                return reflect.apply(f,spoof.get(th)||th,args);
            }catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                err.stack = err.stack.replace(/Object/m,"Function");
                throw err;
            }
        }
    });


    hook(Function.prototype,"apply",{
        apply(f,th,args){
            try{
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                if (spoof.has(args[0])) {
                    err.stack = err.stack.replace(/Proxy/gm,"Function");
                    throw err;
                } else {
                    throw err;
                }
            }
        }
    });
    
    new MutationObserver(()=>{
        for (let i = 0; window[i] != null; i++) {
            if (!spoof.has(window[i].Function.prototype.toString)) {
                hook(window[i].Function.prototype,"toString",{
                    apply(f,th,args){
                        try{
                            return reflect.apply(f,spoof.get(th)||th,args);
                        }catch(err) {
                            err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                            err.stack = err.stack.replace(/Object/m,"Function");
                            throw err;
                        }
                    }
                });
            }
        }
    }).observe(document,{ attributes: true, childList: true, subtree: true });

    hook(Array.prototype,"join",{
        apply(f,th,args) {
            try{
                if (th instanceof Array) {
                    for(let i of th) {
                        if (typeof i == "object" && typeof i.toString != "undefined" && i.toString.toString().includes("try")) {
                            i.toString = ()=>"";
                        }
                    }
                }
                return reflect.apply(f,th,args);
            } catch(err) {
                err.stack = err.stack.replace(/\n.+bypass\.js.+/gm,"");
                throw err;
            }
        }
    });
})();

@doctor8296
Copy link

@nostopgmaming17 yes, just like that!
Good for me that I know methods to bypass MutationObserver >:3

@nostopgmaming17
Copy link

@nostopgmaming17 yes, just like that! Good for me that I know methods to bypass MutationObserver >:3

how?

@doctor8296
Copy link

@nostopgmaming17 yes, just like that! Good for me that I know methods to bypass MutationObserver >:3

how?

Well, you see there are methods that just don't trigger mutation observer and also don't require document.createElement.
Also, I am not sure how MutationObserver works with nested iframe, because you can create iframe inside iframe inside of iframe etc.
When I tested it didn't trigger for me, even tho I used documents from the inside of iframes.

@nostopgmaming17
Copy link

@nostopgmaming17 yes, just like that! Good for me that I know methods to bypass MutationObserver >:3

how?

Well, you see there are methods that just don't trigger mutation observer and also don't require document.createElement. Also, I am not sure how MutationObserver works with nested iframe, because you can create iframe inside iframe inside of iframe etc. When I tested it didn't trigger for me, even tho I used documents from the inside of iframes.

oh innerHTML doesnt work that sucks

@doctor8296
Copy link

@nostopgmaming17 , no, innerHTML doesn't work, I mean I am not sure if that triggers MutationObserver, but it is surely can be handled.

@nostopgmaming17
Copy link

@nostopgmaming17 , no, innerHTML doesn't work, I mean I am not sure if that triggers MutationObserver, but it is surely can be handled.

nvm mutationobserver does work on it

@hashiravi
Copy link

hashiravi commented Jul 25, 2024

also heads up none of that fancy stuff like Uint8Array.prototype.sort is needed you can simply do

void function() {
 try{
  ""();
  detected();
 } catch(error) {
  if (error.stack.includes("Proxy")) {detected()}
 }
}.call(Array.prototype.join);

@nostopgmaming17 Are there similar methods that would work in Safari as it doesn't produce the same granularity of error messages?

@doctor8296
Copy link

@hashiravi hi!
What are you trying to do?

@hashiravi
Copy link

Detect if a function is a Proxy, but in Safari/Webkit

@doctor8296
Copy link

@hashiravi compare the signatures then!

For proxy it will probably be
function () { [native code] }
and for push function, for example it has to be:
function push() { [native code] }

@hashiravi
Copy link

@doctor8296 If we have something like this defined to proxy the navigator object, I dont see any way using Safari to detect it as a Proxy.

const handler = {
        get: function(target, prop) {
            // If the property is a function, return a bound function to maintain the correct context
            if (typeof target[prop] === 'function') {
                return target[prop].bind(target);
            }
            return target[prop];
        }
    };

    // Create a proxy for the navigator object
    const navigatorProxy = new Proxy(navigator, handler);

    // Overwrite the global navigator object with the proxy
    Object.defineProperty(window, 'navigator', {
        value: navigatorProxy,
        configurable: true,
        enumerable: true,
    });

@doctor8296
Copy link

@hashiravi well, yeah, there is no clear way to check objects itselfs, but tbh the is no much reason to.

But in this case bind override function signature as well

@hashiravi
Copy link

@doctor8296 Yes, problem is we have no way of knowing if this object has been tampered with or not.

@nostopgmaming17
Copy link

@doctor8296 If we have something like this defined to proxy the navigator object, I dont see any way using Safari to detect it as a Proxy.

const handler = {
        get: function(target, prop) {
            // If the property is a function, return a bound function to maintain the correct context
            if (typeof target[prop] === 'function') {
                return target[prop].bind(target);
            }
            return target[prop];
        }
    };

    // Create a proxy for the navigator object
    const navigatorProxy = new Proxy(navigator, handler);

    // Overwrite the global navigator object with the proxy
    Object.defineProperty(window, 'navigator', {
        value: navigatorProxy,
        configurable: true,
        enumerable: true,
    });
navigator.constructor===Navigator

this is only one way, there are so much other ways (like getting user agent from the server through packets or api requests)

@nostopgmaming17
Copy link

made a new bypass (less clean like the other one as it uses error handling)

(function() {
    'use strict';

    const spoof = new WeakMap;
    spoof.set = spoof.set;
    spoof.get = spoof.get;
    spoof.has = spoof.has;
    spoof.delete = spoof.delete;


    const reflect = {};
    for (let i of Object.getOwnPropertyNames(Reflect)) {
        reflect[i] = Reflect[i];
    }
    const proxy = Proxy;

    const hook = (o,n,h) => {
        const hooked = new proxy(o[n],h);
        spoof.set(hooked,o[n]);
        o[n] = hooked;
    }

    hook(Function.prototype,"toString",{
        apply(f,th,args){
            return reflect.apply(f,spoof.get(th)||th,args);
        }
    });

    hook(Array.prototype,"join",{
        apply(f,th,args) {
            return reflect.apply(f,th,args);
        }
    });

    const native = name => `function ${name}() { [native code] }`;

    let check = false;
    reflect.defineProperty(Error, "prepareStackTrace", {
        get() {
            if (check) {
                return (_, b) => {
                    check = false;
                    return b;
                }
            }
            check = true;
            let stack = new Error().stack;
            let lastfunc = stack.findLast(v=>v.getFunctionName()!=null)?.getFunctionName();
            if (lastfunc != null) {
                switch(lastfunc) {
                    case "check_stack_1":
                        return ()=>"\n\n\n";
                    case "check_stack_2":
                        return ()=>"\n\n\n\n";
                    case "check_stack_3":
                        return ()=>native("join");
                    case "check_stack_7_toString":
                        return ()=>native("toString");
                    case "check_stack_11_object_create":
                        return ()=>"";
                }
            }
        }
    });
    hook(Reflect,"getOwnPropertyDescriptor",{
        apply(f, th, args) {
            if (args[0] === Error && args[1] === "prepareStackTrace")
                return undefined;
            return reflect.apply(f, th, args);
        }
    });
    hook(Object,"getOwnPropertyDescriptor",{
        apply(f, th, args) {
            if (args[0] === Error && args[1] === "prepareStackTrace")
                return undefined;
            return reflect.apply(f, th, args);
        }
    });
    hook(Object,"getOwnPropertyDescriptors",{
        apply(f, th, args) {
            if (args[0] === Error) {
                const ret = reflect.apply(f, th, args);
                delete ret.prepareStackTrace;
                return ret;
            }
            return reflect.apply(f, th, args);
        }
    });
})();

@doctor8296
Copy link

doctor8296 commented Jul 27, 2024

@hashiravi

I guess you can just create iframe and get clear navigator from there.

Screenshot 2024-07-27 at 11 04 22

And if you will obfuscate your code good enough "hacker" will have no idea that it is checked like that. So you have no need to do some weird checks for iframe was not tampered.

@doctor8296
Copy link

doctor8296 commented Jul 30, 2024

@hashiravi just figured out that you also can do this:

Object.getOwnPropertyDescriptor(Navigator.prototype, "appName").get.call(navigator)

By that you will completely bypass Proxy get

@nostopgmaming17
Copy link

nostopgmaming17 commented Jul 31, 2024

@hashiravi just figured out that you also can do this:

Object.getOwnPropertyDescriptor(Navigator.prototype, "appName").get.call(navigator)

By that you will completely bypass Proxy get

hook Function.prototype.call (or Object.getOwnPropertyDescriptor)

@doctor8296
Copy link

doctor8296 commented Jul 31, 2024

@nostopgmaming17 we talked about inability to check object for proxy.
Now we have only functions left to check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants