-
Notifications
You must be signed in to change notification settings - Fork 2
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
Comments
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. |
I feel it should also be addressed in this repository, as it's certainly another way to detect tamper. |
This is hackable by Object.defineProperty + Proxy. |
@redscheme I would like to see an example user script that hooks From what I've understood from your explanation, it wouldn't get the pass. |
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. |
@hrt @Kepler-11 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;
}
}); |
@redscheme this means that it wouldn't be hooking All krunker or whoever would do is store Your I may adjust the repo to specifically request the user to hook |
@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 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. |
That was easy. 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. |
@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 ( (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 |
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. |
@hrt WE CAN CHECK EVERYTHING WITH CLEAR toString! |
Not sure I understood that. I likely read your comment wrong but I should also add |
@hrt |
@doctor8296 not necessarily: |
@hrt but delete returns false in this case, and this how we can check :P |
@hrt never mind 😢 |
@hrt 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 |
But yeah, the fact that Function.prototype.toString getting replaced with not working one is heart breaking. :( 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) |
Btw you can checkout anticheat on cryzen.io that I made. |
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 |
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 heads up none of that fancy stuff like Uint8Array.prototype.sort is needed 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 using a call/apply hook is much better because you can check if its a hooked function |
@nostopgmaming17 Hi! 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!")
} |
@nostopgmaming17 can I ask why did you place |
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 |
@nostopgmaming17 yes everything can be hooked. |
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;
}
}
});
})(); |
@nostopgmaming17 contentWindow will not get called if I will take it from ’window[n]’ |
wdym? |
if you mean by setting src and having it run the checks there, then i can make a src getter setter too |
@nostopgmaming17 no, not the src. |
oh that works i guess you could use it |
@nostopgmaming17 no, you can just use it, it would be tooo easy to protect the code like that, but unfortunately no. 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;
}
}
});
})(); |
@nostopgmaming17 yes, just like that! |
how? |
Well, you see there are methods that just don't trigger mutation observer and also don't require document.createElement. |
oh innerHTML doesnt work that sucks |
@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 |
@nostopgmaming17 Are there similar methods that would work in Safari as it doesn't produce the same granularity of error messages? |
@hashiravi hi! |
Detect if a function is a Proxy, but in Safari/Webkit |
@hashiravi compare the signatures then! For proxy it will probably be |
@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.
|
@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 |
@doctor8296 Yes, problem is we have no way of knowing if this object has been tampered with or not. |
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) |
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);
}
});
})(); |
@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) |
@nostopgmaming17 we talked about inability to check object for proxy. |
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.The text was updated successfully, but these errors were encountered: