From 057925184fbf4cdd7c334da37759d869f3017eb5 Mon Sep 17 00:00:00 2001 From: Yotam Nachum Date: Tue, 24 Jan 2023 00:03:34 +0200 Subject: [PATCH 1/3] Add deobfuscator method --- index.js | 5 +++ lib/deobfuscator.js | 105 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 lib/deobfuscator.js diff --git a/index.js b/index.js index 198bae6..7e5dafb 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ const Env = require('./lib/env'); const Types = require('./lib/types'); const VM = require('./lib/vm'); const { checkJniResult } = require('./lib/result'); +const Deobfuscator = require('./lib/deobfuscator'); const jsizeSize = 4; const pointerSize = Process.pointerSize; @@ -566,6 +567,10 @@ class Runtime { return result; } + + deobfuscator(mapping) { + return new Deobfuscator(mapping); + } } function initFactoryFromApplication (factory, app) { diff --git a/lib/deobfuscator.js b/lib/deobfuscator.js new file mode 100644 index 0000000..1c6fea2 --- /dev/null +++ b/lib/deobfuscator.js @@ -0,0 +1,105 @@ +const ClassFactory = require('./lib/class-factory'); + +class Deobfuscator { + #mapping; + #reverseMapping; + + constructor (mapping) { + if (mapping instanceof Map) { + this.#mapping = mapping; + } else { + const o = Object.entries(mapping).map(e => [e[0], { + realSpecifier: e[1].realSpecifier, + properties: new Map(Object.entries(e[1].properties)) + }]); + this.#mapping = new Map(o); + } + + this.#reverseMapping = new Map(); + for (const [key, value] of this.#mapping.entries()) { + this.#reverseMapping.set(value.realSpecifier, key); + } + } + + #getRealSpecifier (deobfuscatedSpecifier) { + const specifierMapping = this.#mapping.get(deobfuscatedSpecifier); + return specifierMapping?.realSpecifier; + } + + use (deobfuscatedSpecifier) { + const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) || deobfuscatedSpecifier; + const realUse = ClassFactory.use(realSpecifier); + return this.wrap(realUse); + } + + choose (deobfuscatedSpecifier, callbacks) { + const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) || deobfuscatedSpecifier; + ClassFactory.choose(realSpecifier, { + onMatch: instance => callbacks.onMatch(this.wrap(instance)), + onComplete: () => callbacks.onComplete() + }); + } + + cast (obj, klass, owned) { + const casted = ClassFactory.cast(obj, klass, owned); + return this.wrap(casted); + } + + wrap (wrapper) { + const realSpecifier = wrapper.$n; + const deobfuscatedSpecifier = this.#reverseMapping.get(realSpecifier) || realSpecifier; + const propertiesMapping = this.#mapping.get(deobfuscatedSpecifier)?.properties || new Map(); + + return new Proxy(wrapper, { + get: (target, prop, receiver) => { + const realProp = propertiesMapping.get(prop) || prop; + const result = Reflect.get(target, realProp, receiver); + + if (result) { + if (result.value && result.value.$className) { + return this.#wrapField(result); + } + + if (result instanceof Function) { + return this.#wrapMethod(target, result); + } + } + + // This code path should never be reached, however, returning it makes sure to have some sort of forward compatability. + return result; + } + }); + } + + #wrapField (field) { + return new Proxy(field, { + get: (target, prop, receiver) => { + if (prop === 'value') { + return this.wrap(target.value); + } + return Reflect.get(target, prop, receiver); + } + }); + } + + #wrapMethod (wrapper, method) { + return new Proxy(method, { + apply: (_, thisArg, argumentsList) => { + const result = method.apply(thisArg, argumentsList); + if (result && result.$className) { + return this.wrap(result); + } + return result; + }, + set: (target, prop, newValue, receiver) => { + if (prop === 'implementation') { + return Reflect.set(target, prop, newValue.bind(wrapper), receiver); + } + + return Reflect.set(target, prop, newValue, receiver); + } + }); + } +} + +module.exports = Deobfuscator; From 0bcff5f7335ec77e524c81ece62e4f1c5f2e745b Mon Sep 17 00:00:00 2001 From: Yotam Nachum Date: Fri, 14 Apr 2023 16:10:04 +0300 Subject: [PATCH 2/3] Fix formatting --- lib/class-factory.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/class-factory.js b/lib/class-factory.js index de530fe..b0a7251 100644 --- a/lib/class-factory.js +++ b/lib/class-factory.js @@ -2166,7 +2166,7 @@ Object.defineProperties(Field.prototype, { \tvalue: ${this.value}, }`; return multilineString.split('\n').map(l => l.length > 200 ? l.slice(0, l.indexOf(' ') + 1) + '...,' : l).join('\n'); - }, + } } }); From 34e3a038faedec1731f0f793884a753f910ea86f Mon Sep 17 00:00:00 2001 From: Yotam Nachum Date: Fri, 14 Apr 2023 16:50:08 +0300 Subject: [PATCH 3/3] Split properties to fields and methods --- lib/deobfuscator.js | 66 ++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/lib/deobfuscator.js b/lib/deobfuscator.js index 1c6fea2..e723981 100644 --- a/lib/deobfuscator.js +++ b/lib/deobfuscator.js @@ -1,4 +1,7 @@ -const ClassFactory = require('./lib/class-factory'); +// const Java = require('./lib/class-factory'); + +class UnexpectedPropertyError extends Error { +} class Deobfuscator { #mapping; @@ -10,7 +13,8 @@ class Deobfuscator { } else { const o = Object.entries(mapping).map(e => [e[0], { realSpecifier: e[1].realSpecifier, - properties: new Map(Object.entries(e[1].properties)) + fields: new Map(Object.entries(e[1].fields)), + methods: new Map(Object.entries(e[1].methods)) }]); this.#mapping = new Map(o); } @@ -27,50 +31,76 @@ class Deobfuscator { } use (deobfuscatedSpecifier) { - const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) || deobfuscatedSpecifier; - const realUse = ClassFactory.use(realSpecifier); + const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) ?? deobfuscatedSpecifier; + const realUse = Java.use(realSpecifier); return this.wrap(realUse); } choose (deobfuscatedSpecifier, callbacks) { - const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) || deobfuscatedSpecifier; - ClassFactory.choose(realSpecifier, { + const realSpecifier = this.#getRealSpecifier(deobfuscatedSpecifier) ?? deobfuscatedSpecifier; + Java.choose(realSpecifier, { onMatch: instance => callbacks.onMatch(this.wrap(instance)), onComplete: () => callbacks.onComplete() }); } cast (obj, klass, owned) { - const casted = ClassFactory.cast(obj, klass, owned); + const casted = Java.cast(obj, klass, owned); return this.wrap(casted); } wrap (wrapper) { const realSpecifier = wrapper.$n; - const deobfuscatedSpecifier = this.#reverseMapping.get(realSpecifier) || realSpecifier; - const propertiesMapping = this.#mapping.get(deobfuscatedSpecifier)?.properties || new Map(); + const deobfuscatedSpecifier = this.#reverseMapping.get(realSpecifier) ?? realSpecifier; + + const methodsMapping = this.#mapping.get(deobfuscatedSpecifier)?.methods ?? new Map(); + const fieldsMapping = this.#mapping.get(deobfuscatedSpecifier)?.fields ?? new Map(); return new Proxy(wrapper, { get: (target, prop, receiver) => { - const realProp = propertiesMapping.get(prop) || prop; - const result = Reflect.get(target, realProp, receiver); + if ((methodProp = methodsMapping.get(prop)) !== undefined) { + const result = Reflect.get(target, methodProp, receiver); + if (!(result instanceof Function)) { + throw new UnexpectedPropertyError(`Mapped property was expected to be a method, got ${result} of type ${typeof result} instead`); + } + return this.#wrapMethod(target, result) + } - if (result) { - if (result.value && result.value.$className) { - return this.#wrapField(result); + if ((fieldProp = fieldsMapping.get(prop)) !== undefined) { + let result = Reflect.get(target, realProp, receiver); + while (result instanceof Function) { + fieldProp = "_" + fieldProp; + result = Reflect.get(target, realProp, receiver); } - if (result instanceof Function) { - return this.#wrapMethod(target, result); + if (!(obj.value && obj.value.$className)) { + throw new UnexpectedPropertyError(`Mapped property was expected to be a field, got ${result} of type ${typeof result} instead`); } + + return this.#wrapField(result) } - // This code path should never be reached, however, returning it makes sure to have some sort of forward compatability. - return result; + const result = Reflect.get(target, prop, receiver); + return this.#wrapUnknown(target, result); } }); } + #wrapUnknown(target, obj) { + if (obj) { + if (obj.value && obj.value.$className) { + return this.#wrapField(obj); + } + + if (obj instanceof Function) { + return this.#wrapMethod(target, obj); + } + } + + // This code path should never be reached, however, returning it makes sure to have some sort of forward compatability. + return obj; + } + #wrapField (field) { return new Proxy(field, { get: (target, prop, receiver) => {