Created
May 29, 2021 03:59
-
-
Save yastanotheruser/9352ea43574771d2c2631e84a34db562 to your computer and use it in GitHub Desktop.
Naive Java object inspection functions for Frida, using reflection
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 { JavaClassInfo, JavaClassTree, JavaDisposable } from './types' | |
export function isJavaClass<T extends Java.Members<T> = {}>( | |
v: any | |
): v is Java.Wrapper<T> { | |
if (!v || typeof v !== 'object' || v.$h !== null) { | |
return false | |
} | |
const isInterface = | |
typeof v.class?.isInterface === 'function' && v.class.isInterface() | |
return isInterface || typeof v.$new === 'function' | |
} | |
export function isJavaInstance<T extends Java.Members<T> = {}>( | |
v: any | |
): v is Java.Wrapper<T> { | |
return !!v && typeof v === 'object' && v.$h instanceof NativePointer | |
} | |
export function isJavaMethod<T extends Java.Members<T> = {}>( | |
v: any | |
): v is Java.Method<T> { | |
return ( | |
typeof v === 'function' && | |
typeof v.invoke === 'function' && | |
v.handle instanceof NativePointer | |
) | |
} | |
export function isJavaField<T extends Java.Members<T> = {}>( | |
v: any | |
): v is Java.Field<T> { | |
return !!v && typeof v === 'object' && 'fieldReturnType' in v && 'value' in v | |
} | |
const _java_ReflectModifier = Java.use('java.lang.reflect.Modifier') | |
function _normClass<T extends Java.Members<T> = {}>( | |
_class: Java.Wrapper<T> | string | null | |
): Java.Wrapper<T> | null { | |
if (typeof _class === 'string') { | |
try { | |
_class = Java.use(_class) | |
} catch { | |
return null | |
} | |
} | |
if (!isJavaClass(_class) && !isJavaInstance(_class)) { | |
return null | |
} | |
if (_class.class.getName() !== _class.$className) { | |
const realClass = Java.use(_class.$className) | |
const pclass = _class | |
_class = Java.cast(pclass, realClass) as Java.Wrapper<T> | |
realClass.$dispose() | |
pclass.$dispose() | |
} | |
return _class | |
} | |
function _repeatedNames(names: string[]): string[] { | |
for (let i = 0; i < names.length; ++i) { | |
let j = names.indexOf(names[i], i + 1) | |
if (j === -1) { | |
names.splice(i, 1) | |
--i | |
continue | |
} | |
names.splice(j, 1) | |
while ((j = names.indexOf(names[i], j + 1)) !== -1) { | |
names.splice(j, 1) | |
} | |
} | |
return names | |
} | |
function _getJavaClassInfo<T extends Java.Members<T> = {}>( | |
_class: Java.Wrapper<T> | |
): JavaDisposable<JavaClassInfo[]> { | |
let objs: { $dispose: () => void }[] = [] | |
let dispose: () => void | |
const info: JavaDisposable<JavaClassInfo[]> = Object.assign([], { | |
$dispose: () => dispose(), | |
}) | |
dispose = () => { | |
info.splice(0) | |
objs.forEach(o => o.$dispose()) | |
// @ts-ignore | |
objs = null | |
// @ts-ignore | |
dispose = null | |
} | |
for (let c = _class.class; c !== null; c = c.getSuperclass()) { | |
const className: string = c.getName() | |
const fields = c.getDeclaredFields() | |
const methods = c.getDeclaredMethods() | |
const fieldNames = fields.map((f: any) => f.getName()) | |
const methodNames = methods.map((m: any) => m.getName()) | |
const fieldMethodNames = Array.from(new Set<string>(fieldNames)).concat( | |
Array.from(new Set<string>(methodNames)) | |
) | |
info.push({ | |
className, | |
wrapper: c, | |
fields, | |
methods, | |
fieldNameCollisions: new Set<string>(_repeatedNames(fieldNames)), | |
methodNameCollisions: new Set<string>(_repeatedNames(methodNames)), | |
fieldMethodNameCollisions: new Set<string>( | |
_repeatedNames(fieldMethodNames) | |
), | |
}) | |
objs.push(c, fields, methods) | |
} | |
return info | |
} | |
export function getJavaClassInfo<T extends Java.Members<T> = {}>( | |
_class: Java.Wrapper<T> | string | null | |
): JavaDisposable<JavaClassInfo[]> | null { | |
_class = _normClass(_class) | |
if (_class === null) { | |
return null | |
} | |
return _getJavaClassInfo(_class) | |
} | |
const _javaPrimitiveMapping: { [type: string]: Java.Wrapper } = { | |
'byte': Java.use('java.lang.Byte'), | |
'short': Java.use('java.lang.Short'), | |
'int': Java.use('java.lang.Integer'), | |
'long': Java.use('java.lang.Long'), | |
'float': Java.use('java.lang.Float'), | |
'double': Java.use('java.lang.Double'), | |
'boolean': Java.use('java.lang.Boolean'), | |
'char': Java.use('java.lang.Character'), | |
} | |
function _getReflectFieldValue( | |
f: Java.Wrapper, | |
o: Java.Wrapper<any>, | |
s?: boolean, | |
d?: any[] | null | |
): any { | |
const _fieldValue = f.get(!s ? o : null) | |
const _fieldClass = f.getType() | |
let fieldClass: any = null | |
let fieldValue = _fieldValue | |
if (fieldValue && typeof fieldValue === 'object') { | |
fieldClass = !_fieldClass.isPrimitive() | |
? Java.use(_fieldClass.getName()) | |
: _javaPrimitiveMapping[_fieldClass.getName() as string] | |
fieldValue = Java.cast(_fieldValue, fieldClass) | |
} | |
if (Array.isArray(d)) { | |
d.push(_fieldClass) | |
if (fieldClass !== null) { | |
d.push(fieldClass) | |
} | |
} else { | |
_fieldClass.$dispose() | |
if (fieldClass !== null) { | |
fieldClass.$dispose() | |
} | |
} | |
return fieldValue | |
} | |
function _createReflectMethodInvoker(m: Java.Wrapper): (...args: any[]) => any { | |
return function (this: any, ...args: any[]): any { | |
let rv = m.invoke(this, args) | |
if (rv && typeof rv === 'object') { | |
const realClass = Java.use(rv.$className) | |
rv = Java.cast(rv, realClass) | |
realClass.$dispose() | |
} | |
return rv | |
} | |
} | |
function _inspectJavaObject<T extends Java.Members<T> = {}>( | |
v: Java.Wrapper<T>, | |
depth: number, | |
refs: WeakMap<any, any>, | |
classInfoCache?: { [className: string]: JavaDisposable<JavaClassInfo[]> } | |
): any { | |
const isClass = isJavaClass(v) | |
if (!isClass && !isJavaInstance(v)) { | |
return v | |
} | |
if (refs.has(v)) { | |
return refs.get(v) | |
} | |
if (depth < 0) { | |
return v | |
} | |
let info: JavaDisposable<JavaClassInfo[]> | |
if (classInfoCache) { | |
const className = v.class.getName() | |
if (className in classInfoCache) { | |
info = classInfoCache[className] | |
} else { | |
info = _getJavaClassInfo(v) | |
classInfoCache[className] = info | |
} | |
} else { | |
info = _getJavaClassInfo(v) | |
} | |
let tmpObjs: any[] = [] | |
let objs: any[] = [v, info] | |
let dispose: () => void | |
const o: any = { | |
$inherited: {}, | |
} | |
refs.set(v, o) | |
Object.defineProperties(o, { | |
$class: { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: v.class, | |
}, | |
$className: { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: v.$className, | |
}, | |
$dispose: { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: () => dispose(), | |
}, | |
$wrapper: { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: v, | |
}, | |
}) | |
if (isClass) { | |
for (const k of ['$alloc', '$init', '$new', '$super', '$ownMembers']) { | |
o[k] = v[k] | |
} | |
} | |
dispose = () => { | |
for (const k of Object.keys(o)) { | |
o[k] = null | |
delete o[k] | |
} | |
objs.forEach(o => typeof o.$dispose === 'function' && o.$dispose()) | |
// @ts-ignore | |
info = null | |
// @ts-ignore | |
objs = null | |
// @ts-ignore | |
dispose = null | |
} | |
let fieldCounters: { [key: string]: number } | |
let methodCounters: { [key: string]: number } | |
let fieldMethodCounters: { | |
field: { [key: string]: number } | |
method: { [key: string]: number } | |
} | |
let fieldNameCollisions: Set<string> | |
let methodNameCollisions: Set<string> | |
let fieldMethodNameCollisions: Set<string> | |
const fieldSuffix = (name: string): string => { | |
let s = '' | |
const f = fieldNameCollisions.has(name) | |
const fm = fieldMethodNameCollisions.has(name) | |
if (fm) { | |
s += '$field' | |
} | |
if (f) { | |
let c: number | |
if (fm) { | |
if (name in fieldMethodCounters.field) { | |
c = ++fieldMethodCounters.field[name] | |
} else { | |
fieldMethodCounters.field[name] = 1 | |
c = 1 | |
} | |
} else { | |
if (name in fieldCounters) { | |
c = ++fieldCounters[name] | |
} else { | |
fieldCounters[name] = 1 | |
c = 1 | |
} | |
} | |
s += `$${c}` | |
} | |
return s | |
} | |
const methodSuffix = (name: string): string => { | |
let s = '' | |
const m = methodNameCollisions.has(name) | |
const fm = fieldMethodNameCollisions.has(name) | |
if (fm) { | |
s += '$method' | |
} | |
if (m) { | |
let c: number | |
if (fm) { | |
if (name in fieldMethodCounters.method) { | |
c = ++fieldMethodCounters.method[name] | |
} else { | |
fieldMethodCounters.method[name] = 1 | |
c = 1 | |
} | |
} else { | |
if (name in methodCounters) { | |
c = ++methodCounters[name] | |
} else { | |
methodCounters[name] = 1 | |
c = 1 | |
} | |
} | |
s += `$${c}` | |
} | |
return s | |
} | |
let first = true | |
for (const c of info) { | |
const { className, fields, methods } = c | |
;({ | |
fieldNameCollisions, | |
methodNameCollisions, | |
fieldMethodNameCollisions, | |
} = c) | |
let target: any | |
fieldCounters = {} | |
methodCounters = {} | |
fieldMethodCounters = { | |
field: {}, | |
method: {}, | |
} | |
if (first) { | |
target = o | |
first = false | |
} else { | |
Object.defineProperty(o.$inherited, className, { | |
configurable: true, | |
enumerable: className !== 'java.lang.Object', | |
writable: true, | |
value: {}, | |
}) | |
target = o.$inherited[className] | |
} | |
if (isClass) { | |
for (const f of fields) { | |
f.setAccessible(true) | |
const m = f.getModifiers() | |
const n = f.getName() | |
const s = _java_ReflectModifier.isStatic(m) | |
const targetProp = `${n}${fieldSuffix(n)}` | |
if (s) { | |
const fieldValue = _getReflectFieldValue(f, v, true, tmpObjs) | |
const val = _inspectJavaObject( | |
fieldValue, | |
depth - 1, | |
refs, | |
classInfoCache | |
) | |
target[targetProp] = val | |
if (val) { | |
objs.push(val) | |
} | |
} else { | |
target[targetProp] = (o: any): any => _getReflectFieldValue(f, o) | |
Object.defineProperty(target[targetProp], '$isField', { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: true, | |
}) | |
} | |
} | |
} else { | |
for (const f of fields) { | |
f.setAccessible(true) | |
const m = f.getModifiers() | |
const n = f.getName() | |
const s = _java_ReflectModifier.isStatic(m) | |
const targetProp = `${n}${fieldSuffix(n)}${s ? '$static' : ''}` | |
const fieldValue = _getReflectFieldValue(f, v, s, tmpObjs) | |
const val = _inspectJavaObject( | |
fieldValue, | |
depth - 1, | |
refs, | |
classInfoCache | |
) | |
target[targetProp] = val | |
if (val) { | |
objs.push(val) | |
} | |
} | |
} | |
for (const m of methods) { | |
m.setAccessible(true) | |
const n = m.getName() | |
const _retClass = m.getReturnType() | |
const targetProp = `${n}${methodSuffix(n)}` | |
const invoker = _createReflectMethodInvoker(m) | |
if (!isClass) { | |
target[targetProp] = invoker.bind(v) | |
Object.defineProperty(target, `${targetProp}$unbound`, { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: invoker, | |
}) | |
} else { | |
target[targetProp] = invoker | |
} | |
Object.defineProperty(target[targetProp], '$wrapper', { | |
configurable: true, | |
enumerable: false, | |
writable: true, | |
value: m, | |
}) | |
tmpObjs.push(_retClass) | |
} | |
} | |
tmpObjs.forEach(o => typeof o?.$dispose === 'function' && o.$dispose()) | |
// @ts-ignore | |
tmpObjs = null | |
return o | |
} | |
export function inspectJavaObject<T extends Java.Members<T> = {}>( | |
v: Java.Wrapper<T> | Java.Wrapper<T>[], | |
depth: number = 2 | |
): any { | |
if (depth < 0) { | |
return v | |
} | |
const refs = new WeakMap<any, any>() | |
if (Array.isArray(v)) { | |
return v.map(i => _inspectJavaObject(i, depth - 1, refs)) | |
} | |
return _inspectJavaObject(v, depth, refs, {}) | |
} | |
const _java_android$util$Log = Java.use('android.util.Log') | |
const _java_java$lang$Exception = Java.use('java.lang.Exception') | |
export function getJavaStackTrace(): string { | |
return _java_android$util$Log.getStackTraceString( | |
_java_java$lang$Exception.$new() | |
) | |
} | |
export function inspectJavaClassTree<T extends Java.Members<T> = {}>( | |
_class: Java.Wrapper<T> | string | null | |
): JavaClassTree | null { | |
_class = _normClass(_class) | |
if (_class === null) { | |
return null | |
} | |
const tree: JavaClassTree = [] | |
for (let c = _class.class; c !== null; c = c.getSuperclass()) { | |
const is: string[][] = [] | |
for (const i of c.getInterfaces()) { | |
const ic: string[] = [] | |
for (let c = i; c !== null; c = c.getSuperclass()) { | |
ic.push(c.getName()) | |
} | |
is.push(ic) | |
} | |
tree.push({ | |
className: c.getName(), | |
interfaces: is, | |
}) | |
} | |
return tree | |
} | |
global.JavaUtils = { | |
isJavaInstance, | |
isJavaField, | |
isJavaMethod, | |
isJavaClass, | |
getJavaClassInfo, | |
inspectJavaObject, | |
inspectJavaClassTree, | |
getJavaStackTrace, | |
} |
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
export type JavaDisposable<T = {}> = T & { $dispose: () => void } | |
export interface JavaClassInfo { | |
className: string | |
wrapper: Java.Wrapper | |
fields: Java.Wrapper[] | |
fieldNameCollisions: Set<string> | |
methods: Java.Wrapper[] | |
methodNameCollisions: Set<string> | |
fieldMethodNameCollisions: Set<string> | |
} | |
export interface JavaClassTreeItem { | |
className: string | |
interfaces: string[][] | |
} | |
export type JavaClassTree = JavaClassTreeItem[] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment