Skip to content

Instantly share code, notes, and snippets.

@yastanotheruser
Created May 29, 2021 03:59
Show Gist options
  • Save yastanotheruser/9352ea43574771d2c2631e84a34db562 to your computer and use it in GitHub Desktop.
Save yastanotheruser/9352ea43574771d2c2631e84a34db562 to your computer and use it in GitHub Desktop.
Naive Java object inspection functions for Frida, using reflection
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,
}
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