TamperDetectJS
TamperDetectJS copied to clipboard
Detecting tamper via strict comparison to native function pulled from iframe
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.
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.
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.
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.
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.
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.
@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;
}
});
@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.
@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.
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.
@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
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!
@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.
@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.
@doctor8296 not necessarily:
@hrt but delete returns false in this case, and this how we can check :P
@hrt never mind 😢
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):
@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
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)
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
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
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
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!")
}
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.
void function() { try{ ""(); detected(); } catch(error) { if (error.stack.includes("Proxy")) {detected()} } }.call(Array.prototype.join);@nostopgmaming17 can I ask why did you place
detectedcall after""()is there any case where""()will not throw an error? Also, I prefer to use0()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
@nostopgmaming17 yes everything can be hooked. Except few cases.
@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;
}
}
});
})();
@nostopgmaming17 contentWindow will not get called if I will take it from ’window[n]’
@nostopgmaming17 contentWindow will not get called if I will take it from ’window[n]’
wdym?