-
-
Save apkunpacker/f9be2a610cce4c35b151a9e6ed2d6085 to your computer and use it in GitHub Desktop.
ACTk ObscuredTypes hax with frida; tested on 2.0.2
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import "frida-il2cpp-bridge"; | |
function main() { | |
const AssemblyCSharp = Il2Cpp.domain.assembly("Assembly-CSharp").image; | |
// Note that on versions older than 2.x.y this isn't needed | |
// Since ACTk bundled directly into Assembly-CSharp | |
const ACTk_Runtime = Il2Cpp.domain.assembly("ACTk.Runtime").image; | |
// Target class | |
const PlayerData = AssemblyCSharp.class("PlayerData"); | |
// Here I would show about `ObscuredInt` | |
// But `ObscuredTypes` are really similar | |
// So you have to just checkout dump and edit my code a little | |
const ObscuredInt = ACTk_Runtime.class("CodeStage.AntiCheat.ObscuredTypes.ObscuredInt"); | |
// Cache | |
const map = new Map<Il2Cpp.Class, Il2Cpp.Object[]>(); | |
const scanWithCache = (klass: Il2Cpp.Class): Il2Cpp.Object[] => { | |
const value = map.get(klass); | |
// Value is cached and still valid | |
if (value && value.every(v => !v.isNull())) | |
return value; | |
// No value in cache | |
const objects = Il2Cpp.gc.choose(klass); | |
if (objects.length > 0) { | |
// Object already created | |
map.set(klass, objects); | |
return objects; | |
} | |
// Found nothing, return empty array | |
return []; | |
} | |
const replaceValue = (instance: Il2Cpp.ValueType, value: number) => { | |
// `GetEncrypted(out int key)` | |
// out is reference (System.Int32 &key) but pointer is acceptable too | |
const key = Il2Cpp.alloc(4); // int32 size | |
instance.method("GetEncrypted").invoke(key); | |
// Remember, key is pointer | |
const keyValue = key.readU32(); | |
// We don't need that pointer anymore | |
Il2Cpp.free(key); | |
const encryptedValue = ObscuredInt.method("Encrypt").invoke(value, keyValue); | |
instance.method("SetEncrypted").invoke(encryptedValue, keyValue); | |
} | |
// Every time when game calls this, it will OVERRIDE value | |
// So nothing will affect value unless you revert method | |
ObscuredInt.method("GetDecrypted").implementation = function () { | |
// Replace with yours | |
const myValue = 1337 * (4 * 2); | |
const objects = scanWithCache(PlayerData); | |
for (const instance of objects) { | |
// Target field | |
// Note that it's `ValueType`, not `Object`, so to get properties such as `.class` | |
// You should `.box()` it | |
// But do NOT use boxed value for replacement | |
const currentMoney = instance.field<Il2Cpp.ValueType>("currentMoney").value; | |
const oldValue = currentMoney.method<number>("GetDecrypted").invoke(); | |
// Already replaced | |
if (oldValue == myValue) | |
// If target class have more than one instance - we have to check value on them too | |
continue; | |
// Stop everything | |
// Because ACTk in another thread can overwrite key before we change value | |
// Note that if we get error here, app will be deadlocked | |
// Let's hope everything will be ok | |
Il2Cpp.gc.stopWorld(); | |
replaceValue(currentMoney, myValue); | |
console.log(`Replaced old value (${oldValue}) with new (${myValue})`); | |
// Start back | |
Il2Cpp.gc.startWorld(); | |
} | |
// If we found nothing in `scanWithCache` call or it's not our target - return original value | |
return this.method<number>("GetDecrypted").invoke(); | |
// Note that game might sub or add to our value | |
// If you really care about fancy value like `1337` | |
// Trace this class and find method which performs operation | |
// Or look into pseudocode using Ghidra / IDA and find how it works | |
// Personally, I don't care about this and would set just a big number | |
} | |
} | |
Il2Cpp.perform(main); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment