Created
November 20, 2024 12:35
-
-
Save hackcatml/e4ad5a73f08758332da9d347b11aac0e to your computer and use it in GitHub Desktop.
Modified version of matbrik's patch for the Frida ART issue (https://github.com/frida/frida-java-bridge/pull/337)
This file contains hidden or 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
const makeCodeAllocator = require('./alloc'); | |
const { | |
jvmtiVersion, | |
jvmtiCapabilities, | |
EnvJvmti | |
} = require('./jvmti'); | |
const { parseInstructionsAt } = require('./machine-code'); | |
const memoize = require('./memoize'); | |
const { checkJniResult, JNI_OK } = require('./result'); | |
const VM = require('./vm'); | |
const jsizeSize = 4; | |
const pointerSize = Process.pointerSize; | |
const { | |
readU32, | |
readPointer, | |
writeU32, | |
writePointer | |
} = NativePointer.prototype; | |
const kAccPublic = 0x0001; | |
const kAccStatic = 0x0008; | |
const kAccFinal = 0x0010; | |
const kAccNative = 0x0100; | |
const kAccFastNative = 0x00080000; | |
const kAccCriticalNative = 0x00200000; | |
const kAccFastInterpreterToInterpreterInvoke = 0x40000000; | |
const kAccSkipAccessChecks = 0x00080000; | |
const kAccSingleImplementation = 0x08000000; | |
const kAccNterpEntryPointFastPathFlag = 0x00100000; | |
const kAccNterpInvokeFastPathFlag = 0x00200000; | |
const kAccPublicApi = 0x10000000; | |
const kAccXposedHookedMethod = 0x10000000; | |
const kPointer = 0x0; | |
const kFullDeoptimization = 3; | |
const kSelectiveDeoptimization = 5; | |
const THUMB_BIT_REMOVAL_MASK = ptr(1).not(); | |
const X86_JMP_MAX_DISTANCE = 0x7fffbfff; | |
const ARM64_ADRP_MAX_DISTANCE = 0xfffff000; | |
const ENV_VTABLE_OFFSET_EXCEPTION_CLEAR = 17 * pointerSize; | |
const ENV_VTABLE_OFFSET_FATAL_ERROR = 18 * pointerSize; | |
const DVM_JNI_ENV_OFFSET_SELF = 12; | |
const DVM_CLASS_OBJECT_OFFSET_VTABLE_COUNT = 112; | |
const DVM_CLASS_OBJECT_OFFSET_VTABLE = 116; | |
const DVM_OBJECT_OFFSET_CLAZZ = 0; | |
const DVM_METHOD_SIZE = 56; | |
const DVM_METHOD_OFFSET_ACCESS_FLAGS = 4; | |
const DVM_METHOD_OFFSET_METHOD_INDEX = 8; | |
const DVM_METHOD_OFFSET_REGISTERS_SIZE = 10; | |
const DVM_METHOD_OFFSET_OUTS_SIZE = 12; | |
const DVM_METHOD_OFFSET_INS_SIZE = 14; | |
const DVM_METHOD_OFFSET_SHORTY = 28; | |
const DVM_METHOD_OFFSET_JNI_ARG_INFO = 36; | |
const DALVIK_JNI_RETURN_VOID = 0; | |
const DALVIK_JNI_RETURN_FLOAT = 1; | |
const DALVIK_JNI_RETURN_DOUBLE = 2; | |
const DALVIK_JNI_RETURN_S8 = 3; | |
const DALVIK_JNI_RETURN_S4 = 4; | |
const DALVIK_JNI_RETURN_S2 = 5; | |
const DALVIK_JNI_RETURN_U2 = 6; | |
const DALVIK_JNI_RETURN_S1 = 7; | |
const DALVIK_JNI_NO_ARG_INFO = 0x80000000; | |
const DALVIK_JNI_RETURN_SHIFT = 28; | |
const STD_STRING_SIZE = 3 * pointerSize; | |
const STD_VECTOR_SIZE = 3 * pointerSize; | |
const AF_UNIX = 1; | |
const SOCK_STREAM = 1; | |
const getArtRuntimeSpec = memoize(_getArtRuntimeSpec); | |
const getArtInstrumentationSpec = memoize(_getArtInstrumentationSpec); | |
const getArtMethodSpec = memoize(_getArtMethodSpec); | |
const getArtThreadSpec = memoize(_getArtThreadSpec); | |
const getArtManagedStackSpec = memoize(_getArtManagedStackSpec); | |
const getArtThreadStateTransitionImpl = memoize(_getArtThreadStateTransitionImpl); | |
const getAndroidVersion = memoize(_getAndroidVersion); | |
const getAndroidCodename = memoize(_getAndroidCodename); | |
const getAndroidApiLevel = memoize(_getAndroidApiLevel); | |
const getArtQuickFrameInfoGetterThunk = memoize(_getArtQuickFrameInfoGetterThunk); | |
const makeCxxMethodWrapperReturningPointerByValue = | |
(Process.arch === 'ia32') | |
? makeCxxMethodWrapperReturningPointerByValueInFirstArg | |
: makeCxxMethodWrapperReturningPointerByValueGeneric; | |
const nativeFunctionOptions = { | |
exceptions: 'propagate' | |
}; | |
const artThreadStateTransitions = {}; | |
let cachedApi = null; | |
let cachedArtClassLinkerSpec = null; | |
let MethodMangler = null; | |
let artController = null; | |
const inlineHooks = []; | |
const patchedClasses = new Map(); | |
const artQuickInterceptors = []; | |
let thunkPage = null; | |
let thunkOffset = 0; | |
let taughtArtAboutReplacementMethods = false; | |
let taughtArtAboutMethodInstrumentation = false; | |
let backtraceModule = null; | |
const jdwpSessions = []; | |
let socketpair = null; | |
let trampolineAllocator = null; | |
function getApi () { | |
if (cachedApi === null) { | |
cachedApi = _getApi(); | |
} | |
return cachedApi; | |
} | |
function _getApi () { | |
const vmModules = Process.enumerateModules() | |
.filter(m => /^lib(art|dvm).so$/.test(m.name)) | |
.filter(m => !/\/system\/fake-libs/.test(m.path)); | |
if (vmModules.length === 0) { | |
return null; | |
} | |
const vmModule = vmModules[0]; | |
const flavor = (vmModule.name.indexOf('art') !== -1) ? 'art' : 'dalvik'; | |
const isArt = flavor === 'art'; | |
const temporaryApi = { | |
module: vmModule, | |
flavor, | |
addLocalReference: null | |
}; | |
const pending = isArt | |
? [{ | |
module: vmModule.path, | |
functions: { | |
JNI_GetCreatedJavaVMs: ['JNI_GetCreatedJavaVMs', 'int', ['pointer', 'int', 'pointer']], | |
// Android < 7 | |
artInterpreterToCompiledCodeBridge: function (address) { | |
this.artInterpreterToCompiledCodeBridge = address; | |
}, | |
// Android >= 8 | |
_ZN3art9JavaVMExt12AddGlobalRefEPNS_6ThreadENS_6ObjPtrINS_6mirror6ObjectEEE: ['art::JavaVMExt::AddGlobalRef', 'pointer', ['pointer', 'pointer', 'pointer']], | |
// Android >= 6 | |
_ZN3art9JavaVMExt12AddGlobalRefEPNS_6ThreadEPNS_6mirror6ObjectE: ['art::JavaVMExt::AddGlobalRef', 'pointer', ['pointer', 'pointer', 'pointer']], | |
// Android < 6: makeAddGlobalRefFallbackForAndroid5() needs these: | |
_ZN3art17ReaderWriterMutex13ExclusiveLockEPNS_6ThreadE: ['art::ReaderWriterMutex::ExclusiveLock', 'void', ['pointer', 'pointer']], | |
_ZN3art17ReaderWriterMutex15ExclusiveUnlockEPNS_6ThreadE: ['art::ReaderWriterMutex::ExclusiveUnlock', 'void', ['pointer', 'pointer']], | |
// Android <= 7 | |
_ZN3art22IndirectReferenceTable3AddEjPNS_6mirror6ObjectE: function (address) { | |
this['art::IndirectReferenceTable::Add'] = new NativeFunction(address, 'pointer', ['pointer', 'uint', 'pointer'], nativeFunctionOptions); | |
}, | |
// Android > 7 | |
_ZN3art22IndirectReferenceTable3AddENS_15IRTSegmentStateENS_6ObjPtrINS_6mirror6ObjectEEE: function (address) { | |
this['art::IndirectReferenceTable::Add'] = new NativeFunction(address, 'pointer', ['pointer', 'uint', 'pointer'], nativeFunctionOptions); | |
}, | |
// Android >= 7 | |
_ZN3art9JavaVMExt12DecodeGlobalEPv: function (address) { | |
let decodeGlobal; | |
if (getAndroidApiLevel() >= 26) { | |
// Returns ObjPtr<mirror::Object> | |
decodeGlobal = makeCxxMethodWrapperReturningPointerByValue(address, ['pointer', 'pointer']); | |
} else { | |
// Returns mirror::Object * | |
decodeGlobal = new NativeFunction(address, 'pointer', ['pointer', 'pointer'], nativeFunctionOptions); | |
} | |
this['art::JavaVMExt::DecodeGlobal'] = function (vm, thread, ref) { | |
return decodeGlobal(vm, ref); | |
}; | |
}, | |
// Android >= 6 | |
_ZN3art9JavaVMExt12DecodeGlobalEPNS_6ThreadEPv: ['art::JavaVMExt::DecodeGlobal', 'pointer', ['pointer', 'pointer', 'pointer']], | |
// makeDecodeGlobalFallback() uses: | |
// Android >= 15 | |
_ZNK3art6Thread19DecodeGlobalJObjectEP8_jobject: ['art::Thread::DecodeJObject', 'pointer', ['pointer', 'pointer']], | |
// Android < 6 | |
_ZNK3art6Thread13DecodeJObjectEP8_jobject: ['art::Thread::DecodeJObject', 'pointer', ['pointer', 'pointer']], | |
// Android >= 6 | |
_ZN3art10ThreadList10SuspendAllEPKcb: ['art::ThreadList::SuspendAll', 'void', ['pointer', 'pointer', 'bool']], | |
// or fallback: | |
_ZN3art10ThreadList10SuspendAllEv: function (address) { | |
const suspendAll = new NativeFunction(address, 'void', ['pointer'], nativeFunctionOptions); | |
this['art::ThreadList::SuspendAll'] = function (threadList, cause, longSuspend) { | |
return suspendAll(threadList); | |
}; | |
}, | |
_ZN3art10ThreadList9ResumeAllEv: ['art::ThreadList::ResumeAll', 'void', ['pointer']], | |
// Android >= 7 | |
_ZN3art11ClassLinker12VisitClassesEPNS_12ClassVisitorE: ['art::ClassLinker::VisitClasses', 'void', ['pointer', 'pointer']], | |
// Android < 7 | |
_ZN3art11ClassLinker12VisitClassesEPFbPNS_6mirror5ClassEPvES4_: function (address) { | |
const visitClasses = new NativeFunction(address, 'void', ['pointer', 'pointer', 'pointer'], nativeFunctionOptions); | |
this['art::ClassLinker::VisitClasses'] = function (classLinker, visitor) { | |
visitClasses(classLinker, visitor, NULL); | |
}; | |
}, | |
_ZNK3art11ClassLinker17VisitClassLoadersEPNS_18ClassLoaderVisitorE: ['art::ClassLinker::VisitClassLoaders', 'void', ['pointer', 'pointer']], | |
_ZN3art2gc4Heap12VisitObjectsEPFvPNS_6mirror6ObjectEPvES5_: ['art::gc::Heap::VisitObjects', 'void', ['pointer', 'pointer', 'pointer']], | |
_ZN3art2gc4Heap12GetInstancesERNS_24VariableSizedHandleScopeENS_6HandleINS_6mirror5ClassEEEiRNSt3__16vectorINS4_INS5_6ObjectEEENS8_9allocatorISB_EEEE: ['art::gc::Heap::GetInstances', 'void', ['pointer', 'pointer', 'pointer', 'int', 'pointer']], | |
// Android >= 9 | |
_ZN3art2gc4Heap12GetInstancesERNS_24VariableSizedHandleScopeENS_6HandleINS_6mirror5ClassEEEbiRNSt3__16vectorINS4_INS5_6ObjectEEENS8_9allocatorISB_EEEE: function (address) { | |
const getInstances = new NativeFunction(address, 'void', ['pointer', 'pointer', 'pointer', 'bool', 'int', 'pointer'], nativeFunctionOptions); | |
this['art::gc::Heap::GetInstances'] = function (instance, scope, hClass, maxCount, instances) { | |
const useIsAssignableFrom = 0; | |
getInstances(instance, scope, hClass, useIsAssignableFrom, maxCount, instances); | |
}; | |
}, | |
_ZN3art12StackVisitorC2EPNS_6ThreadEPNS_7ContextENS0_13StackWalkKindEjb: ['art::StackVisitor::StackVisitor', 'void', ['pointer', 'pointer', 'pointer', 'uint', 'uint', 'bool']], | |
_ZN3art12StackVisitorC2EPNS_6ThreadEPNS_7ContextENS0_13StackWalkKindEmb: ['art::StackVisitor::StackVisitor', 'void', ['pointer', 'pointer', 'pointer', 'uint', 'size_t', 'bool']], | |
_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb: ['art::StackVisitor::WalkStack', 'void', ['pointer', 'bool']], | |
_ZNK3art12StackVisitor9GetMethodEv: ['art::StackVisitor::GetMethod', 'pointer', ['pointer']], | |
_ZNK3art12StackVisitor16DescribeLocationEv: function (address) { | |
this['art::StackVisitor::DescribeLocation'] = makeCxxMethodWrapperReturningStdStringByValue(address, ['pointer']); | |
}, | |
_ZNK3art12StackVisitor24GetCurrentQuickFrameInfoEv: function (address) { | |
this['art::StackVisitor::GetCurrentQuickFrameInfo'] = makeArtQuickFrameInfoGetter(address); | |
}, | |
_ZN3art6Thread18GetLongJumpContextEv: ['art::Thread::GetLongJumpContext', 'pointer', ['pointer']], | |
_ZN3art6mirror5Class13GetDescriptorEPNSt3__112basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEE: function (address) { | |
this['art::mirror::Class::GetDescriptor'] = address; | |
}, | |
_ZN3art6mirror5Class11GetLocationEv: function (address) { | |
this['art::mirror::Class::GetLocation'] = makeCxxMethodWrapperReturningStdStringByValue(address, ['pointer']); | |
}, | |
_ZN3art9ArtMethod12PrettyMethodEb: function (address) { | |
this['art::ArtMethod::PrettyMethod'] = makeCxxMethodWrapperReturningStdStringByValue(address, ['pointer', 'bool']); | |
}, | |
_ZN3art12PrettyMethodEPNS_9ArtMethodEb: function (address) { | |
this['art::ArtMethod::PrettyMethodNullSafe'] = makeCxxMethodWrapperReturningStdStringByValue(address, ['pointer', 'bool']); | |
}, | |
// Android < 6 for cloneArtMethod() | |
_ZN3art6Thread14CurrentFromGdbEv: ['art::Thread::CurrentFromGdb', 'pointer', []], | |
_ZN3art6mirror6Object5CloneEPNS_6ThreadE: function (address) { | |
this['art::mirror::Object::Clone'] = new NativeFunction(address, 'pointer', ['pointer', 'pointer'], nativeFunctionOptions); | |
}, | |
_ZN3art6mirror6Object5CloneEPNS_6ThreadEm: function (address) { | |
const clone = new NativeFunction(address, 'pointer', ['pointer', 'pointer', 'pointer'], nativeFunctionOptions); | |
this['art::mirror::Object::Clone'] = function (thisPtr, threadPtr) { | |
const numTargetBytes = NULL; | |
return clone(thisPtr, threadPtr, numTargetBytes); | |
}; | |
}, | |
_ZN3art6mirror6Object5CloneEPNS_6ThreadEj: function (address) { | |
const clone = new NativeFunction(address, 'pointer', ['pointer', 'pointer', 'uint'], nativeFunctionOptions); | |
this['art::mirror::Object::Clone'] = function (thisPtr, threadPtr) { | |
const numTargetBytes = 0; | |
return clone(thisPtr, threadPtr, numTargetBytes); | |
}; | |
}, | |
_ZN3art3Dbg14SetJdwpAllowedEb: ['art::Dbg::SetJdwpAllowed', 'void', ['bool']], | |
_ZN3art3Dbg13ConfigureJdwpERKNS_4JDWP11JdwpOptionsE: ['art::Dbg::ConfigureJdwp', 'void', ['pointer']], | |
_ZN3art31InternalDebuggerControlCallback13StartDebuggerEv: ['art::InternalDebuggerControlCallback::StartDebugger', 'void', ['pointer']], | |
_ZN3art3Dbg9StartJdwpEv: ['art::Dbg::StartJdwp', 'void', []], | |
_ZN3art3Dbg8GoActiveEv: ['art::Dbg::GoActive', 'void', []], | |
_ZN3art3Dbg21RequestDeoptimizationERKNS_21DeoptimizationRequestE: ['art::Dbg::RequestDeoptimization', 'void', ['pointer']], | |
_ZN3art3Dbg20ManageDeoptimizationEv: ['art::Dbg::ManageDeoptimization', 'void', []], | |
_ZN3art15instrumentation15Instrumentation20EnableDeoptimizationEv: ['art::Instrumentation::EnableDeoptimization', 'void', ['pointer']], | |
// Android >= 6 | |
_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEPKc: ['art::Instrumentation::DeoptimizeEverything', 'void', ['pointer', 'pointer']], | |
// Android < 6 | |
_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEv: function (address) { | |
const deoptimize = new NativeFunction(address, 'void', ['pointer'], nativeFunctionOptions); | |
this['art::Instrumentation::DeoptimizeEverything'] = function (instrumentation, key) { | |
deoptimize(instrumentation); | |
}; | |
}, | |
_ZN3art7Runtime19DeoptimizeBootImageEv: ['art::Runtime::DeoptimizeBootImage', 'void', ['pointer']], | |
_ZN3art15instrumentation15Instrumentation10DeoptimizeEPNS_9ArtMethodE: ['art::Instrumentation::Deoptimize', 'void', ['pointer', 'pointer']], | |
// Android >= 11 | |
_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID: ['art::jni::JniIdManager::DecodeMethodId', 'pointer', ['pointer', 'pointer']], | |
_ZN3art11interpreter18GetNterpEntryPointEv: ['art::interpreter::GetNterpEntryPoint', 'pointer', []], | |
_ZN3art7Monitor17TranslateLocationEPNS_9ArtMethodEjPPKcPi: ['art::Monitor::TranslateLocation', 'void', ['pointer', 'uint32', 'pointer', 'pointer']] | |
}, | |
variables: { | |
_ZN3art3Dbg9gRegistryE: function (address) { | |
this.isJdwpStarted = () => !address.readPointer().isNull(); | |
}, | |
_ZN3art3Dbg15gDebuggerActiveE: function (address) { | |
this.isDebuggerActive = () => !!address.readU8(); | |
} | |
}, | |
optionals: [ | |
'artInterpreterToCompiledCodeBridge', | |
'_ZN3art9JavaVMExt12AddGlobalRefEPNS_6ThreadENS_6ObjPtrINS_6mirror6ObjectEEE', | |
'_ZN3art9JavaVMExt12AddGlobalRefEPNS_6ThreadEPNS_6mirror6ObjectE', | |
'_ZN3art9JavaVMExt12DecodeGlobalEPv', | |
'_ZN3art9JavaVMExt12DecodeGlobalEPNS_6ThreadEPv', | |
'_ZNK3art6Thread19DecodeGlobalJObjectEP8_jobject', | |
'_ZNK3art6Thread13DecodeJObjectEP8_jobject', | |
'_ZN3art10ThreadList10SuspendAllEPKcb', | |
'_ZN3art10ThreadList10SuspendAllEv', | |
'_ZN3art11ClassLinker12VisitClassesEPNS_12ClassVisitorE', | |
'_ZN3art11ClassLinker12VisitClassesEPFbPNS_6mirror5ClassEPvES4_', | |
'_ZNK3art11ClassLinker17VisitClassLoadersEPNS_18ClassLoaderVisitorE', | |
'_ZN3art6mirror6Object5CloneEPNS_6ThreadE', | |
'_ZN3art6mirror6Object5CloneEPNS_6ThreadEm', | |
'_ZN3art6mirror6Object5CloneEPNS_6ThreadEj', | |
'_ZN3art22IndirectReferenceTable3AddEjPNS_6mirror6ObjectE', | |
'_ZN3art22IndirectReferenceTable3AddENS_15IRTSegmentStateENS_6ObjPtrINS_6mirror6ObjectEEE', | |
'_ZN3art2gc4Heap12VisitObjectsEPFvPNS_6mirror6ObjectEPvES5_', | |
'_ZN3art2gc4Heap12GetInstancesERNS_24VariableSizedHandleScopeENS_6HandleINS_6mirror5ClassEEEiRNSt3__16vectorINS4_INS5_6ObjectEEENS8_9allocatorISB_EEEE', | |
'_ZN3art2gc4Heap12GetInstancesERNS_24VariableSizedHandleScopeENS_6HandleINS_6mirror5ClassEEEbiRNSt3__16vectorINS4_INS5_6ObjectEEENS8_9allocatorISB_EEEE', | |
'_ZN3art12StackVisitorC2EPNS_6ThreadEPNS_7ContextENS0_13StackWalkKindEjb', | |
'_ZN3art12StackVisitorC2EPNS_6ThreadEPNS_7ContextENS0_13StackWalkKindEmb', | |
'_ZN3art12StackVisitor9WalkStackILNS0_16CountTransitionsE0EEEvb', | |
'_ZNK3art12StackVisitor9GetMethodEv', | |
'_ZNK3art12StackVisitor16DescribeLocationEv', | |
'_ZNK3art12StackVisitor24GetCurrentQuickFrameInfoEv', | |
'_ZN3art6Thread18GetLongJumpContextEv', | |
'_ZN3art6mirror5Class13GetDescriptorEPNSt3__112basic_stringIcNS2_11char_traitsIcEENS2_9allocatorIcEEEE', | |
'_ZN3art6mirror5Class11GetLocationEv', | |
'_ZN3art9ArtMethod12PrettyMethodEb', | |
'_ZN3art12PrettyMethodEPNS_9ArtMethodEb', | |
'_ZN3art3Dbg13ConfigureJdwpERKNS_4JDWP11JdwpOptionsE', | |
'_ZN3art31InternalDebuggerControlCallback13StartDebuggerEv', | |
'_ZN3art3Dbg15gDebuggerActiveE', | |
'_ZN3art15instrumentation15Instrumentation20EnableDeoptimizationEv', | |
'_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEPKc', | |
'_ZN3art15instrumentation15Instrumentation20DeoptimizeEverythingEv', | |
'_ZN3art7Runtime19DeoptimizeBootImageEv', | |
'_ZN3art15instrumentation15Instrumentation10DeoptimizeEPNS_9ArtMethodE', | |
'_ZN3art3Dbg9StartJdwpEv', | |
'_ZN3art3Dbg8GoActiveEv', | |
'_ZN3art3Dbg21RequestDeoptimizationERKNS_21DeoptimizationRequestE', | |
'_ZN3art3Dbg20ManageDeoptimizationEv', | |
'_ZN3art3Dbg9gRegistryE', | |
'_ZN3art3jni12JniIdManager14DecodeMethodIdEP10_jmethodID', | |
'_ZN3art11interpreter18GetNterpEntryPointEv', | |
'_ZN3art7Monitor17TranslateLocationEPNS_9ArtMethodEjPPKcPi' | |
] | |
}] | |
: [{ | |
module: vmModule.path, | |
functions: { | |
_Z20dvmDecodeIndirectRefP6ThreadP8_jobject: ['dvmDecodeIndirectRef', 'pointer', ['pointer', 'pointer']], | |
_Z15dvmUseJNIBridgeP6MethodPv: ['dvmUseJNIBridge', 'void', ['pointer', 'pointer']], | |
_Z20dvmHeapSourceGetBasev: ['dvmHeapSourceGetBase', 'pointer', []], | |
_Z21dvmHeapSourceGetLimitv: ['dvmHeapSourceGetLimit', 'pointer', []], | |
_Z16dvmIsValidObjectPK6Object: ['dvmIsValidObject', 'uint8', ['pointer']], | |
JNI_GetCreatedJavaVMs: ['JNI_GetCreatedJavaVMs', 'int', ['pointer', 'int', 'pointer']] | |
}, | |
variables: { | |
gDvmJni: function (address) { | |
this.gDvmJni = address; | |
}, | |
gDvm: function (address) { | |
this.gDvm = address; | |
} | |
} | |
}]; | |
const missing = []; | |
pending.forEach(function (api) { | |
const functions = api.functions || {}; | |
const variables = api.variables || {}; | |
const optionals = new Set(api.optionals || []); | |
const exportByName = Module | |
.enumerateExports(api.module) | |
.reduce(function (result, exp) { | |
result[exp.name] = exp; | |
return result; | |
}, {}); | |
Object.keys(functions) | |
.forEach(function (name) { | |
let exp = exportByName[name]; | |
if (exp === undefined && name === '_ZN3art7Monitor17TranslateLocationEPNS_9ArtMethodEjPPKcPi') { | |
let proc_self_cmdline_string_found_addr; | |
let proc_self_cmdline_string = '00 2f 70 72 6f 63 2f 73 65 6c 66 2f 63 6d 64 6c 69 6e 65'; | |
const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0]; | |
for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, proc_self_cmdline_string)) { | |
if (match) { | |
proc_self_cmdline_string_found_addr = match.address.add(0x1).toString(); | |
break; | |
} | |
} | |
// adrp, add sample | |
// 00 CB FF F0 00 00 07 91 | |
// E0 EF FF D0 00 80 38 91 | |
// 40 D3 FF D0 00 24 11 91 | |
// 00 DC FF 90 00 F0 10 91 | |
// 00 DC FF 90 00 F0 10 91 | |
let adrp, add; | |
let adrp_add_pattern = '?0 ?? FF ?0 00 ?? ?? 91'; | |
let adrp_add_pattern_found_addr; | |
let translate_location_func_addr; | |
const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0]; | |
for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) { | |
let disasm = Instruction.parse(match.address); | |
if (disasm.mnemonic === "adrp") { | |
adrp = disasm.operands.find(op => op.type === 'imm')?.value; | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic !== "add") { | |
disasm = Instruction.parse(disasm.next); | |
} | |
add = disasm.operands.find(op => op.type === 'imm')?.value; | |
if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === proc_self_cmdline_string_found_addr.toString()) { | |
if (adrp_add_pattern_found_addr === undefined) { | |
adrp_add_pattern_found_addr = match.address; | |
} | |
for (let off = 0; off < 40; off += 4) { | |
disasm = Instruction.parse(adrp_add_pattern_found_addr.sub(off)); | |
if (disasm.mnemonic === "b" || disasm.mnemonic === "bl") { | |
let branch_addr = ptr(disasm.operands.find(op => op.type === 'imm')?.value); | |
disasm = Instruction.parse(branch_addr); | |
if (disasm.mnemonic === 'stp') { | |
translate_location_func_addr = disasm.address; | |
exp = { | |
address: translate_location_func_addr, | |
name: "_ZN3art7Monitor17TranslateLocationEPNS_9ArtMethodEjPPKcPi", | |
type: "function" | |
} | |
break; | |
} else { | |
continue; | |
} | |
} | |
} | |
break; | |
} | |
} | |
} | |
} | |
if (exp !== undefined && exp.type === 'function') { | |
const signature = functions[name]; | |
if (typeof signature === 'function') { | |
signature.call(temporaryApi, exp.address); | |
} else { | |
temporaryApi[signature[0]] = new NativeFunction(exp.address, signature[1], signature[2], nativeFunctionOptions); | |
} | |
} else { | |
if (!optionals.has(name)) { | |
missing.push(name); | |
} | |
} | |
}); | |
Object.keys(variables) | |
.forEach(function (name) { | |
const exp = exportByName[name]; | |
if (exp !== undefined && exp.type === 'variable') { | |
const handler = variables[name]; | |
handler.call(temporaryApi, exp.address); | |
} else { | |
if (!optionals.has(name)) { | |
missing.push(name); | |
} | |
} | |
}); | |
}); | |
if (missing.length > 0) { | |
throw new Error('Java API only partially available; please file a bug. Missing: ' + missing.join(', ')); | |
} | |
const vms = Memory.alloc(pointerSize); | |
const vmCount = Memory.alloc(jsizeSize); | |
checkJniResult('JNI_GetCreatedJavaVMs', temporaryApi.JNI_GetCreatedJavaVMs(vms, 1, vmCount)); | |
if (vmCount.readInt() === 0) { | |
return null; | |
} | |
temporaryApi.vm = vms.readPointer(); | |
if (isArt) { | |
const apiLevel = getAndroidApiLevel(); | |
let kAccCompileDontBother; | |
if (apiLevel >= 27) { | |
kAccCompileDontBother = 0x02000000; | |
} else if (apiLevel >= 24) { | |
kAccCompileDontBother = 0x01000000; | |
} else { | |
kAccCompileDontBother = 0; | |
} | |
temporaryApi.kAccCompileDontBother = kAccCompileDontBother; | |
const artRuntime = temporaryApi.vm.add(pointerSize).readPointer(); | |
temporaryApi.artRuntime = artRuntime; | |
const runtimeSpec = getArtRuntimeSpec(temporaryApi); | |
const runtimeOffset = runtimeSpec.offset; | |
const instrumentationOffset = runtimeOffset.instrumentation; | |
temporaryApi.artInstrumentation = (instrumentationOffset !== null) ? artRuntime.add(instrumentationOffset) : null; | |
temporaryApi.artHeap = artRuntime.add(runtimeOffset.heap).readPointer(); | |
temporaryApi.artThreadList = artRuntime.add(runtimeOffset.threadList).readPointer(); | |
/* | |
* We must use the *correct* copy (or address) of art_quick_generic_jni_trampoline | |
* in order for the stack trace to recognize the JNI stub quick frame. | |
* | |
* For ARTs for Android 6.x we can just use the JNI trampoline built into ART. | |
*/ | |
const classLinker = artRuntime.add(runtimeOffset.classLinker).readPointer(); | |
const classLinkerOffsets = getArtClassLinkerSpec(artRuntime, runtimeSpec).offset; | |
const quickResolutionTrampoline = classLinker.add(classLinkerOffsets.quickResolutionTrampoline).readPointer(); | |
const quickImtConflictTrampoline = classLinker.add(classLinkerOffsets.quickImtConflictTrampoline).readPointer(); | |
const quickGenericJniTrampoline = classLinker.add(classLinkerOffsets.quickGenericJniTrampoline).readPointer(); | |
const quickToInterpreterBridgeTrampoline = classLinker.add(classLinkerOffsets.quickToInterpreterBridgeTrampoline).readPointer(); | |
temporaryApi.artClassLinker = { | |
address: classLinker, | |
quickResolutionTrampoline, | |
quickImtConflictTrampoline, | |
quickGenericJniTrampoline, | |
quickToInterpreterBridgeTrampoline | |
}; | |
const vm = new VM(temporaryApi); | |
temporaryApi.artQuickGenericJniTrampoline = getArtQuickEntrypointFromTrampoline(quickGenericJniTrampoline, vm); | |
temporaryApi.artQuickToInterpreterBridge = getArtQuickEntrypointFromTrampoline(quickToInterpreterBridgeTrampoline, vm); | |
temporaryApi.artQuickResolutionTrampoline = getArtQuickEntrypointFromTrampoline(quickResolutionTrampoline, vm); | |
if (temporaryApi['art::JavaVMExt::AddGlobalRef'] === undefined) { | |
temporaryApi['art::JavaVMExt::AddGlobalRef'] = makeAddGlobalRefFallbackForAndroid5(temporaryApi); | |
} | |
if (temporaryApi['art::JavaVMExt::DecodeGlobal'] === undefined) { | |
temporaryApi['art::JavaVMExt::DecodeGlobal'] = makeDecodeGlobalFallback(temporaryApi); | |
} | |
if (temporaryApi['art::ArtMethod::PrettyMethod'] === undefined) { | |
temporaryApi['art::ArtMethod::PrettyMethod'] = temporaryApi['art::ArtMethod::PrettyMethodNullSafe']; | |
} | |
if (temporaryApi['art::interpreter::GetNterpEntryPoint'] !== undefined) { | |
temporaryApi.artNterpEntryPoint = temporaryApi['art::interpreter::GetNterpEntryPoint'](); | |
} else { | |
if (Process.arch === 'arm64' && getAndroidApiLevel() >= 30) { | |
const artMethodCopyfrom = Module.findExportByName('libart.so', '_ZN3art9ArtMethod8CopyFromEPS0_NS_11PointerSizeE'); | |
for (let off = 0; off < 0x300; off += 4) { | |
const disasm = Instruction.parse(artMethodCopyfrom.add(off)); | |
const nextInstr = Instruction.parse(artMethodCopyfrom.add(off).add(0x4)); | |
if (disasm.mnemonic === 'adrp' && nextInstr.mnemonic === 'add') { | |
const base = ptr(disasm.operands[1].value); | |
const offset = nextInstr.operands[2].value; | |
const result = base.add(offset); | |
const dest = Instruction.parse(result); | |
if (dest.mnemonic === 'sub') { | |
temporaryApi.artNterpEntryPoint = result; | |
} | |
} | |
} | |
} | |
} | |
artController = makeArtController(vm); | |
fixupArtQuickDeliverExceptionBug(temporaryApi); | |
let cachedJvmti = null; | |
Object.defineProperty(temporaryApi, 'jvmti', { | |
get () { | |
if (cachedJvmti === null) { | |
cachedJvmti = [tryGetEnvJvmti(vm, this.artRuntime)]; | |
} | |
return cachedJvmti[0]; | |
} | |
}); | |
} | |
const cxxImports = Module.enumerateImports(vmModule.path) | |
.filter(imp => imp.name.indexOf('_Z') === 0) | |
.reduce((result, imp) => { | |
result[imp.name] = imp.address; | |
return result; | |
}, {}); | |
temporaryApi.$new = new NativeFunction(cxxImports._Znwm || cxxImports._Znwj, 'pointer', ['ulong'], nativeFunctionOptions); | |
temporaryApi.$delete = new NativeFunction(cxxImports._ZdlPv, 'void', ['pointer'], nativeFunctionOptions); | |
MethodMangler = isArt ? ArtMethodMangler : DalvikMethodMangler; | |
return temporaryApi; | |
} | |
function tryGetEnvJvmti (vm, runtime) { | |
let env = null; | |
vm.perform(() => { | |
let ensurePluginLoaded; | |
if (Module.findExportByName('libart.so', '_ZN3art7Runtime18EnsurePluginLoadedEPKcPNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE') === null) { | |
if (Process.arch === 'arm64') { | |
let libopenjdkjvmti_so_string_found_addr; | |
let libopenjdkjvmti_so_string = '6c 69 62 6f 70 65 6e 6a 64 6b 6a 76 6d 74 69 2e 73 6f'; | |
const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0]; | |
for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, libopenjdkjvmti_so_string)) { | |
if (match) { | |
libopenjdkjvmti_so_string_found_addr = match.address.toString(); | |
break; | |
} | |
} | |
// adrp, add sample | |
// 61 D4 FF B0 21 80 38 91 | |
// 41 B9 FF D0 21 78 0E 91 | |
// 41 CD FF B0 21 F0 09 91 | |
// 41 B9 FF B0 21 78 0E 91 | |
let adrp, add; | |
let adrp_add_pattern = '?1 ?? FF ?0 21 ?? ?? 91'; | |
let adrp_add_pattern_found_addr; | |
let ensurePluginLoaded_func_addr; | |
const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0]; | |
for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) { | |
let disasm = Instruction.parse(match.address); | |
if (disasm.mnemonic === "adrp") { | |
adrp = disasm.operands.find(op => op.type === 'imm')?.value; | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic !== "add") { | |
disasm = Instruction.parse(disasm.next); | |
} | |
add = disasm.operands.find(op => op.type === 'imm')?.value; | |
if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === libopenjdkjvmti_so_string_found_addr.toString()) { | |
if (adrp_add_pattern_found_addr === undefined) { | |
adrp_add_pattern_found_addr = match.address; | |
} | |
for (let off = 0;; off += 4) { | |
disasm = Instruction.parse(adrp_add_pattern_found_addr.add(off)); | |
if (disasm.mnemonic === "b" || disasm.mnemonic === "bl") { | |
ensurePluginLoaded_func_addr = ptr(disasm.operands.find(op => op.type === 'imm')?.value); | |
break; | |
} | |
} | |
break; | |
} | |
} | |
} | |
if (ensurePluginLoaded_func_addr !== undefined) { | |
ensurePluginLoaded = new NativeFunction(ensurePluginLoaded_func_addr, | |
'bool', | |
['pointer', 'pointer', 'pointer']); | |
} else { | |
return; | |
} | |
} | |
} else { | |
ensurePluginLoaded = new NativeFunction(Module.getExportByName('libart.so', '_ZN3art7Runtime18EnsurePluginLoadedEPKcPNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEE'), | |
'bool', | |
['pointer', 'pointer', 'pointer']); | |
} | |
const errorPtr = Memory.alloc(pointerSize); | |
const success = ensurePluginLoaded(runtime, Memory.allocUtf8String('libopenjdkjvmti.so'), errorPtr); | |
if (!success) { | |
// FIXME: Avoid leaking error | |
return; | |
} | |
const kArtTiVersion = jvmtiVersion.v1_2 | 0x40000000; | |
const handle = vm.tryGetEnvHandle(kArtTiVersion); | |
if (handle === null) { | |
return; | |
} | |
env = new EnvJvmti(handle, vm); | |
const capaBuf = Memory.alloc(8); | |
capaBuf.writeU64(jvmtiCapabilities.canTagObjects); | |
const result = env.addCapabilities(capaBuf); | |
if (result !== JNI_OK) { | |
env = null; | |
} | |
}); | |
return env; | |
} | |
function ensureClassInitialized (env, classRef) { | |
const api = getApi(); | |
if (api.flavor !== 'art') { | |
return; | |
} | |
env.getFieldId(classRef, 'x', 'Z'); | |
env.exceptionClear(); | |
} | |
function getArtVMSpec (api) { | |
return { | |
offset: (pointerSize === 4) | |
? { | |
globalsLock: 32, | |
globals: 72 | |
} | |
: { | |
globalsLock: 64, | |
globals: 112 | |
} | |
}; | |
} | |
function _getArtRuntimeSpec (api) { | |
/* | |
* class Runtime { | |
* ... | |
* gc::Heap* heap_; <-- we need to find this | |
* std::unique_ptr<ArenaPool> jit_arena_pool_; <----- API level >= 24 | |
* std::unique_ptr<ArenaPool> arena_pool_; __ | |
* std::unique_ptr<ArenaPool> low_4gb_arena_pool_/linear_alloc_arena_pool_; <--|__ API level >= 23 | |
* std::unique_ptr<LinearAlloc> linear_alloc_; \_ | |
* std::atomic<LinearAlloc*> startup_linear_alloc_;<----- API level >= 34 | |
* size_t max_spins_before_thin_lock_inflation_; | |
* MonitorList* monitor_list_; | |
* MonitorPool* monitor_pool_; | |
* ThreadList* thread_list_; <--- and these | |
* InternTable* intern_table_; <--/ | |
* ClassLinker* class_linker_; <-/ | |
* SignalCatcher* signal_catcher_; | |
* SmallIrtAllocator* small_irt_allocator_; <------------ API level >= 33 or Android Tiramisu Developer Preview | |
* std::unique_ptr<jni::JniIdManager> jni_id_manager_; <- API level >= 30 or Android R Developer Preview | |
* bool use_tombstoned_traces_; <-------------------- API level 27/28 | |
* std::string stack_trace_file_; <-------------------- API level <= 28 | |
* JavaVMExt* java_vm_; <-- so we find this then calculate our way backwards | |
* ... | |
* } | |
*/ | |
const vm = api.vm; | |
const runtime = api.artRuntime; | |
const startOffset = (pointerSize === 4) ? 200 : 384; | |
const endOffset = startOffset + (100 * pointerSize); | |
const apiLevel = getAndroidApiLevel(); | |
const codename = getAndroidCodename(); | |
const isApiLevel34OrApexEquivalent = Module.findExportByName('libart.so', '_ZN3art7AppInfo29GetPrimaryApkReferenceProfileEv') !== null; | |
let spec = null; | |
for (let offset = startOffset; offset !== endOffset; offset += pointerSize) { | |
const value = runtime.add(offset).readPointer(); | |
if (value.equals(vm)) { | |
let classLinkerOffsets; | |
let jniIdManagerOffset = null; | |
if (apiLevel >= 33 || codename === 'Tiramisu') { | |
classLinkerOffsets = [offset - (4 * pointerSize)]; | |
jniIdManagerOffset = offset - pointerSize; | |
} else if (apiLevel >= 30 || codename === 'R') { | |
classLinkerOffsets = [offset - (3 * pointerSize), offset - (4 * pointerSize)]; | |
jniIdManagerOffset = offset - pointerSize; | |
} else if (apiLevel >= 29) { | |
classLinkerOffsets = [offset - (2 * pointerSize)]; | |
} else if (apiLevel >= 27) { | |
classLinkerOffsets = [offset - STD_STRING_SIZE - (3 * pointerSize)]; | |
} else { | |
classLinkerOffsets = [offset - STD_STRING_SIZE - (2 * pointerSize)]; | |
} | |
for (const classLinkerOffset of classLinkerOffsets) { | |
const internTableOffset = classLinkerOffset - pointerSize; | |
const threadListOffset = internTableOffset - pointerSize; | |
let heapOffset; | |
if (isApiLevel34OrApexEquivalent) { | |
heapOffset = threadListOffset - (9 * pointerSize); | |
} else if (apiLevel >= 24) { | |
heapOffset = threadListOffset - (8 * pointerSize); | |
} else if (apiLevel >= 23) { | |
heapOffset = threadListOffset - (7 * pointerSize); | |
} else { | |
heapOffset = threadListOffset - (4 * pointerSize); | |
} | |
const candidate = { | |
offset: { | |
heap: heapOffset, | |
threadList: threadListOffset, | |
internTable: internTableOffset, | |
classLinker: classLinkerOffset, | |
jniIdManager: jniIdManagerOffset | |
} | |
}; | |
if (tryGetArtClassLinkerSpec(runtime, candidate) !== null) { | |
spec = candidate; | |
break; | |
} | |
} | |
break; | |
} | |
} | |
if (spec === null) { | |
throw new Error('Unable to determine Runtime field offsets'); | |
} | |
spec.offset.instrumentation = tryDetectInstrumentationOffset(api); | |
spec.offset.jniIdsIndirection = tryDetectJniIdsIndirectionOffset(); | |
return spec; | |
} | |
const instrumentationOffsetParsers = { | |
ia32: parsex86InstrumentationOffset, | |
x64: parsex86InstrumentationOffset, | |
arm: parseArmInstrumentationOffset, | |
arm64: parseArm64InstrumentationOffset | |
}; | |
function tryDetectInstrumentationOffset (api) { | |
const impl = api['art::Runtime::DeoptimizeBootImage']; | |
if (impl === undefined) { | |
return null; | |
} | |
return parseInstructionsAt(impl, instrumentationOffsetParsers[Process.arch], { limit: 30 }); | |
} | |
function parsex86InstrumentationOffset (insn) { | |
if (insn.mnemonic !== 'lea') { | |
return null; | |
} | |
const offset = insn.operands[1].value.disp; | |
if (offset < 0x100 || offset > 0x400) { | |
return null; | |
} | |
return offset; | |
} | |
function parseArmInstrumentationOffset (insn) { | |
if (insn.mnemonic !== 'add.w') { | |
return null; | |
} | |
const ops = insn.operands; | |
if (ops.length !== 3) { | |
return null; | |
} | |
const op2 = ops[2]; | |
if (op2.type !== 'imm') { | |
return null; | |
} | |
return op2.value; | |
} | |
function parseArm64InstrumentationOffset (insn) { | |
if (insn.mnemonic !== 'add') { | |
return null; | |
} | |
const ops = insn.operands; | |
if (ops.length !== 3) { | |
return null; | |
} | |
if (ops[0].value === 'sp' || ops[1].value === 'sp') { | |
return null; | |
} | |
const op2 = ops[2]; | |
if (op2.type !== 'imm') { | |
return null; | |
} | |
const offset = op2.value.valueOf(); | |
if (offset < 0x100 || offset > 0x400) { | |
return null; | |
} | |
return offset; | |
} | |
const jniIdsIndirectionOffsetParsers = { | |
ia32: parsex86JniIdsIndirectionOffset, | |
x64: parsex86JniIdsIndirectionOffset, | |
arm: parseArmJniIdsIndirectionOffset, | |
arm64: parseArm64JniIdsIndirectionOffset | |
}; | |
function tryDetectJniIdsIndirectionOffset () { | |
const impl = Module.findExportByName('libart.so', '_ZN3art7Runtime12SetJniIdTypeENS_9JniIdTypeE'); | |
if (impl === null) { | |
return null; | |
} | |
const offset = parseInstructionsAt(impl, jniIdsIndirectionOffsetParsers[Process.arch], { limit: 20 }); | |
if (offset === null) { | |
throw new Error('Unable to determine Runtime.jni_ids_indirection_ offset'); | |
} | |
return offset; | |
} | |
function parsex86JniIdsIndirectionOffset (insn) { | |
if (insn.mnemonic === 'cmp') { | |
return insn.operands[0].value.disp; | |
} | |
return null; | |
} | |
function parseArmJniIdsIndirectionOffset (insn) { | |
if (insn.mnemonic === 'ldr.w') { | |
return insn.operands[1].value.disp; | |
} | |
return null; | |
} | |
function parseArm64JniIdsIndirectionOffset (insn, prevInsn) { | |
if (prevInsn === null) { | |
return null; | |
} | |
const { mnemonic } = insn; | |
const { mnemonic: prevMnemonic } = prevInsn; | |
if ((mnemonic === 'cmp' && prevMnemonic === 'ldr') || (mnemonic === 'bl' && prevMnemonic === 'str')) { | |
return prevInsn.operands[1].value.disp; | |
} | |
return null; | |
} | |
function _getArtInstrumentationSpec () { | |
const deoptimizationEnabledOffsets = { | |
'4-21': 136, | |
'4-22': 136, | |
'4-23': 172, | |
'4-24': 196, | |
'4-25': 196, | |
'4-26': 196, | |
'4-27': 196, | |
'4-28': 212, | |
'4-29': 172, | |
'4-30': 180, | |
'8-21': 224, | |
'8-22': 224, | |
'8-23': 296, | |
'8-24': 344, | |
'8-25': 344, | |
'8-26': 352, | |
'8-27': 352, | |
'8-28': 392, | |
'8-29': 328, | |
'8-30': 336 | |
}; | |
const deoptEnabledOffset = deoptimizationEnabledOffsets[`${pointerSize}-${getAndroidApiLevel()}`]; | |
if (deoptEnabledOffset === undefined) { | |
throw new Error('Unable to determine Instrumentation field offsets'); | |
} | |
return { | |
offset: { | |
forcedInterpretOnly: 4, | |
deoptimizationEnabled: deoptEnabledOffset | |
} | |
}; | |
} | |
function getArtClassLinkerSpec (runtime, runtimeSpec) { | |
const spec = tryGetArtClassLinkerSpec(runtime, runtimeSpec); | |
if (spec === null) { | |
throw new Error('Unable to determine ClassLinker field offsets'); | |
} | |
return spec; | |
} | |
function tryGetArtClassLinkerSpec (runtime, runtimeSpec) { | |
if (cachedArtClassLinkerSpec !== null) { | |
return cachedArtClassLinkerSpec; | |
} | |
/* | |
* On Android 5.x: | |
* | |
* class ClassLinker { | |
* ... | |
* InternTable* intern_table_; <-- We find this then calculate our way forwards | |
* const void* portable_resolution_trampoline_; | |
* const void* quick_resolution_trampoline_; | |
* const void* portable_imt_conflict_trampoline_; | |
* const void* quick_imt_conflict_trampoline_; | |
* const void* quick_generic_jni_trampoline_; <-- ...to this | |
* const void* quick_to_interpreter_bridge_trampoline_; | |
* ... | |
* } | |
* | |
* On Android 6.x and above: | |
* | |
* class ClassLinker { | |
* ... | |
* InternTable* intern_table_; <-- We find this then calculate our way forwards | |
* const void* quick_resolution_trampoline_; | |
* const void* quick_imt_conflict_trampoline_; | |
* const void* quick_generic_jni_trampoline_; <-- ...to this | |
* const void* quick_to_interpreter_bridge_trampoline_; | |
* ... | |
* } | |
*/ | |
const { classLinker: classLinkerOffset, internTable: internTableOffset } = runtimeSpec.offset; | |
const classLinker = runtime.add(classLinkerOffset).readPointer(); | |
const internTable = runtime.add(internTableOffset).readPointer(); | |
const startOffset = (pointerSize === 4) ? 100 : 200; | |
const endOffset = startOffset + (100 * pointerSize); | |
const apiLevel = getAndroidApiLevel(); | |
let spec = null; | |
for (let offset = startOffset; offset !== endOffset; offset += pointerSize) { | |
const value = classLinker.add(offset).readPointer(); | |
if (value.equals(internTable)) { | |
let delta; | |
if (apiLevel >= 30 || getAndroidCodename() === 'R') { | |
delta = 6; | |
} else if (apiLevel >= 29) { | |
delta = 4; | |
} else if (apiLevel >= 23) { | |
delta = 3; | |
} else { | |
delta = 5; | |
} | |
const quickGenericJniTrampolineOffset = offset + (delta * pointerSize); | |
let quickResolutionTrampolineOffset; | |
if (apiLevel >= 23) { | |
quickResolutionTrampolineOffset = quickGenericJniTrampolineOffset - (2 * pointerSize); | |
} else { | |
quickResolutionTrampolineOffset = quickGenericJniTrampolineOffset - (3 * pointerSize); | |
} | |
spec = { | |
offset: { | |
quickResolutionTrampoline: quickResolutionTrampolineOffset, | |
quickImtConflictTrampoline: quickGenericJniTrampolineOffset - pointerSize, | |
quickGenericJniTrampoline: quickGenericJniTrampolineOffset, | |
quickToInterpreterBridgeTrampoline: quickGenericJniTrampolineOffset + pointerSize | |
} | |
}; | |
break; | |
} | |
} | |
if (spec !== null) { | |
cachedArtClassLinkerSpec = spec; | |
} | |
return spec; | |
} | |
function getArtClassSpec (vm) { | |
let apiLevel; | |
try { | |
apiLevel = getAndroidApiLevel(); | |
} catch (e) { | |
return null; | |
} | |
if (apiLevel < 24) { | |
return null; | |
} | |
let base, cmo; | |
if (apiLevel >= 26) { | |
base = 40; | |
cmo = 116; | |
} else { | |
base = 56; | |
cmo = 124; | |
} | |
return { | |
offset: { | |
ifields: base, | |
methods: base + 8, | |
sfields: base + 16, | |
copiedMethodsOffset: cmo | |
} | |
}; | |
} | |
function _getArtMethodSpec (vm) { | |
const api = getApi(); | |
let spec; | |
vm.perform(env => { | |
const process = env.findClass('android/os/Process'); | |
const getElapsedCpuTime = unwrapMethodId(env.getStaticMethodId(process, 'getElapsedCpuTime', '()J')); | |
env.deleteLocalRef(process); | |
const runtimeModule = Process.getModuleByName('libandroid_runtime.so'); | |
const runtimeStart = runtimeModule.base; | |
const runtimeEnd = runtimeStart.add(runtimeModule.size); | |
const apiLevel = getAndroidApiLevel(); | |
const entrypointFieldSize = (apiLevel <= 21) ? 8 : pointerSize; | |
const expectedAccessFlags = kAccPublic | kAccStatic | kAccFinal | kAccNative; | |
const relevantAccessFlagsMask = ~(kAccFastInterpreterToInterpreterInvoke | kAccPublicApi | kAccNterpInvokeFastPathFlag) >>> 0; | |
let jniCodeOffset = null; | |
let accessFlagsOffset = null; | |
let remaining = 2; | |
for (let offset = 0; offset !== 64 && remaining !== 0; offset += 4) { | |
const field = getElapsedCpuTime.add(offset); | |
if (jniCodeOffset === null) { | |
const address = field.readPointer(); | |
if (address.compare(runtimeStart) >= 0 && address.compare(runtimeEnd) < 0) { | |
jniCodeOffset = offset; | |
remaining--; | |
} | |
} | |
if (accessFlagsOffset === null) { | |
const flags = field.readU32(); | |
if ((flags & relevantAccessFlagsMask) === expectedAccessFlags) { | |
accessFlagsOffset = offset; | |
remaining--; | |
} | |
} | |
} | |
if (remaining !== 0) { | |
throw new Error('Unable to determine ArtMethod field offsets'); | |
} | |
const quickCodeOffset = jniCodeOffset + entrypointFieldSize; | |
const size = (apiLevel <= 21) ? (quickCodeOffset + 32) : (quickCodeOffset + pointerSize); | |
spec = { | |
size, | |
offset: { | |
jniCode: jniCodeOffset, | |
quickCode: quickCodeOffset, | |
accessFlags: accessFlagsOffset | |
} | |
}; | |
if ('artInterpreterToCompiledCodeBridge' in api) { | |
spec.offset.interpreterCode = jniCodeOffset - entrypointFieldSize; | |
} | |
}); | |
return spec; | |
} | |
function getArtFieldSpec (vm) { | |
const apiLevel = getAndroidApiLevel(); | |
if (apiLevel >= 23) { | |
return { | |
size: 16, | |
offset: { | |
accessFlags: 4 | |
} | |
}; | |
} | |
if (apiLevel >= 21) { | |
return { | |
size: 24, | |
offset: { | |
accessFlags: 12 | |
} | |
}; | |
} | |
return null; | |
} | |
function _getArtThreadSpec (vm) { | |
/* | |
* bool32_t is_exception_reported_to_instrumentation_; <-- We need this on API level <= 22 | |
* ... | |
* mirror::Throwable* exception; <-- ...and this on all versions | |
* uint8_t* stack_end; | |
* ManagedStack managed_stack; | |
* uintptr_t* suspend_trigger; | |
* JNIEnvExt* jni_env; <-- We find this then calculate our way backwards/forwards | |
* JNIEnvExt* tmp_jni_env; <-- API level >= 23 | |
* Thread* self; | |
* mirror::Object* opeer; | |
* jobject jpeer; | |
* uint8_t* stack_begin; | |
* size_t stack_size; | |
* ThrowLocation throw_location; <-- ...and this on API level <= 22 | |
* union DepsOrStackTraceSample { | |
* DepsOrStackTraceSample() { | |
* verifier_deps = nullptr; | |
* stack_trace_sample = nullptr; | |
* } | |
* std::vector<ArtMethod*>* stack_trace_sample; | |
* verifier::VerifierDeps* verifier_deps; | |
* } deps_or_stack_trace_sample; | |
* Thread* wait_next; | |
* mirror::Object* monitor_enter_object; | |
* BaseHandleScope* top_handle_scope; <-- ...and to this on all versions | |
*/ | |
const apiLevel = getAndroidApiLevel(); | |
let spec; | |
vm.perform(env => { | |
const threadHandle = getArtThreadFromEnv(env); | |
const envHandle = env.handle; | |
let isExceptionReportedOffset = null; | |
let exceptionOffset = null; | |
let throwLocationOffset = null; | |
let topHandleScopeOffset = null; | |
let managedStackOffset = null; | |
let selfOffset = null; | |
for (let offset = 144; offset !== 256; offset += pointerSize) { | |
const field = threadHandle.add(offset); | |
const value = field.readPointer(); | |
if (value.equals(envHandle)) { | |
exceptionOffset = offset - (6 * pointerSize); | |
managedStackOffset = offset - (4 * pointerSize); | |
selfOffset = offset + (2 * pointerSize); | |
if (apiLevel <= 22) { | |
exceptionOffset -= pointerSize; | |
isExceptionReportedOffset = exceptionOffset - pointerSize - (9 * 8) - (3 * 4); | |
throwLocationOffset = offset + (6 * pointerSize); | |
managedStackOffset -= pointerSize; | |
selfOffset -= pointerSize; | |
} | |
topHandleScopeOffset = offset + (9 * pointerSize); | |
if (apiLevel <= 22) { | |
topHandleScopeOffset += (2 * pointerSize) + 4; | |
if (pointerSize === 8) { | |
topHandleScopeOffset += 4; | |
} | |
} | |
if (apiLevel >= 23) { | |
topHandleScopeOffset += pointerSize; | |
} | |
break; | |
} | |
} | |
if (topHandleScopeOffset === null) { | |
throw new Error('Unable to determine ArtThread field offsets'); | |
} | |
spec = { | |
offset: { | |
isExceptionReportedToInstrumentation: isExceptionReportedOffset, | |
exception: exceptionOffset, | |
throwLocation: throwLocationOffset, | |
topHandleScope: topHandleScopeOffset, | |
managedStack: managedStackOffset, | |
self: selfOffset | |
} | |
}; | |
}); | |
return spec; | |
} | |
function _getArtManagedStackSpec () { | |
const apiLevel = getAndroidApiLevel(); | |
if (apiLevel >= 23) { | |
return { | |
offset: { | |
topQuickFrame: 0, | |
link: pointerSize | |
} | |
}; | |
} else { | |
return { | |
offset: { | |
topQuickFrame: 2 * pointerSize, | |
link: 0 | |
} | |
}; | |
} | |
} | |
const artQuickTrampolineParsers = { | |
ia32: parseArtQuickTrampolineX86, | |
x64: parseArtQuickTrampolineX86, | |
arm: parseArtQuickTrampolineArm, | |
arm64: parseArtQuickTrampolineArm64 | |
}; | |
function getArtQuickEntrypointFromTrampoline (trampoline, vm) { | |
let address; | |
vm.perform(env => { | |
const thread = getArtThreadFromEnv(env); | |
const tryParse = artQuickTrampolineParsers[Process.arch]; | |
const insn = Instruction.parse(trampoline); | |
const offset = tryParse(insn); | |
if (offset !== null) { | |
address = thread.add(offset).readPointer(); | |
} else { | |
address = trampoline; | |
} | |
}); | |
return address; | |
} | |
function parseArtQuickTrampolineX86 (insn) { | |
if (insn.mnemonic === 'jmp') { | |
return insn.operands[0].value.disp; | |
} | |
return null; | |
} | |
function parseArtQuickTrampolineArm (insn) { | |
if (insn.mnemonic === 'ldr.w') { | |
return insn.operands[1].value.disp; | |
} | |
return null; | |
} | |
function parseArtQuickTrampolineArm64 (insn) { | |
if (insn.mnemonic === 'ldr') { | |
return insn.operands[1].value.disp; | |
} | |
return null; | |
} | |
function getArtThreadFromEnv (env) { | |
return env.handle.add(pointerSize).readPointer(); | |
} | |
function _getAndroidVersion () { | |
return getAndroidSystemProperty('ro.build.version.release'); | |
} | |
function _getAndroidCodename () { | |
return getAndroidSystemProperty('ro.build.version.codename'); | |
} | |
function _getAndroidApiLevel () { | |
return parseInt(getAndroidSystemProperty('ro.build.version.sdk'), 10); | |
} | |
let systemPropertyGet = null; | |
const PROP_VALUE_MAX = 92; | |
function getAndroidSystemProperty (name) { | |
if (systemPropertyGet === null) { | |
systemPropertyGet = new NativeFunction(Module.getExportByName('libc.so', '__system_property_get'), 'int', ['pointer', 'pointer'], nativeFunctionOptions); | |
} | |
const buf = Memory.alloc(PROP_VALUE_MAX); | |
systemPropertyGet(Memory.allocUtf8String(name), buf); | |
return buf.readUtf8String(); | |
} | |
function withRunnableArtThread (vm, env, fn) { | |
const perform = getArtThreadStateTransitionImpl(vm, env); | |
const id = getArtThreadFromEnv(env).toString(); | |
artThreadStateTransitions[id] = fn; | |
perform(env.handle); | |
if (artThreadStateTransitions[id] !== undefined) { | |
delete artThreadStateTransitions[id]; | |
throw new Error('Unable to perform state transition; please file a bug'); | |
} | |
} | |
function _getArtThreadStateTransitionImpl (vm, env) { | |
const callback = new NativeCallback(onThreadStateTransitionComplete, 'void', ['pointer']); | |
return makeArtThreadStateTransitionImpl(vm, env, callback); | |
} | |
function onThreadStateTransitionComplete (thread) { | |
const id = thread.toString(); | |
const fn = artThreadStateTransitions[id]; | |
delete artThreadStateTransitions[id]; | |
fn(thread); | |
} | |
function withAllArtThreadsSuspended (fn) { | |
const api = getApi(); | |
const threadList = api.artThreadList; | |
const longSuspend = false; | |
api['art::ThreadList::SuspendAll'](threadList, Memory.allocUtf8String('frida'), longSuspend ? 1 : 0); | |
try { | |
fn(); | |
} finally { | |
api['art::ThreadList::ResumeAll'](threadList); | |
} | |
} | |
class ArtClassVisitor { | |
constructor (visit) { | |
const visitor = Memory.alloc(4 * pointerSize); | |
const vtable = visitor.add(pointerSize); | |
visitor.writePointer(vtable); | |
const onVisit = new NativeCallback((self, klass) => { | |
return visit(klass) === true ? 1 : 0; | |
}, 'bool', ['pointer', 'pointer']); | |
vtable.add(2 * pointerSize).writePointer(onVisit); | |
this.handle = visitor; | |
this._onVisit = onVisit; | |
} | |
} | |
function makeArtClassVisitor (visit) { | |
const api = getApi(); | |
if (api['art::ClassLinker::VisitClasses'] instanceof NativeFunction) { | |
return new ArtClassVisitor(visit); | |
} | |
return new NativeCallback(klass => { | |
return visit(klass) === true ? 1 : 0; | |
}, 'bool', ['pointer', 'pointer']); | |
} | |
class ArtClassLoaderVisitor { | |
constructor (visit) { | |
const visitor = Memory.alloc(4 * pointerSize); | |
const vtable = visitor.add(pointerSize); | |
visitor.writePointer(vtable); | |
const onVisit = new NativeCallback((self, klass) => { | |
visit(klass); | |
}, 'void', ['pointer', 'pointer']); | |
vtable.add(2 * pointerSize).writePointer(onVisit); | |
this.handle = visitor; | |
this._onVisit = onVisit; | |
} | |
} | |
function makeArtClassLoaderVisitor (visit) { | |
return new ArtClassLoaderVisitor(visit); | |
} | |
const WalkKind = { | |
'include-inlined-frames': 0, | |
'skip-inlined-frames': 1 | |
}; | |
class ArtStackVisitor { | |
constructor (thread, context, walkKind, numFrames = 0, checkSuspended = true) { | |
const api = getApi(); | |
const baseSize = 512; /* Up to 488 bytes on 64-bit Android Q. */ | |
const vtableSize = 3 * pointerSize; | |
const visitor = Memory.alloc(baseSize + vtableSize); | |
api['art::StackVisitor::StackVisitor'](visitor, thread, context, WalkKind[walkKind], numFrames, | |
checkSuspended ? 1 : 0); | |
const vtable = visitor.add(baseSize); | |
visitor.writePointer(vtable); | |
const onVisitFrame = new NativeCallback(this._visitFrame.bind(this), 'bool', ['pointer']); | |
vtable.add(2 * pointerSize).writePointer(onVisitFrame); | |
this.handle = visitor; | |
this._onVisitFrame = onVisitFrame; | |
const curShadowFrame = visitor.add((pointerSize === 4) ? 12 : 24); | |
this._curShadowFrame = curShadowFrame; | |
this._curQuickFrame = curShadowFrame.add(pointerSize); | |
this._curQuickFramePc = curShadowFrame.add(2 * pointerSize); | |
this._curOatQuickMethodHeader = curShadowFrame.add(3 * pointerSize); | |
this._getMethodImpl = api['art::StackVisitor::GetMethod']; | |
this._descLocImpl = api['art::StackVisitor::DescribeLocation']; | |
this._getCQFIImpl = api['art::StackVisitor::GetCurrentQuickFrameInfo']; | |
} | |
walkStack (includeTransitions = false) { | |
getApi()['art::StackVisitor::WalkStack'](this.handle, includeTransitions ? 1 : 0); | |
} | |
_visitFrame () { | |
return this.visitFrame() ? 1 : 0; | |
} | |
visitFrame () { | |
throw new Error('Subclass must implement visitFrame'); | |
} | |
getMethod () { | |
const methodHandle = this._getMethodImpl(this.handle); | |
if (methodHandle.isNull()) { | |
return null; | |
} | |
return new ArtMethod(methodHandle); | |
} | |
getCurrentQuickFramePc () { | |
return this._curQuickFramePc.readPointer(); | |
} | |
getCurrentQuickFrame () { | |
return this._curQuickFrame.readPointer(); | |
} | |
getCurrentShadowFrame () { | |
return this._curShadowFrame.readPointer(); | |
} | |
describeLocation () { | |
const result = new StdString(); | |
this._descLocImpl(result, this.handle); | |
return result.disposeToString(); | |
} | |
getCurrentOatQuickMethodHeader () { | |
return this._curOatQuickMethodHeader.readPointer(); | |
} | |
getCurrentQuickFrameInfo () { | |
return this._getCQFIImpl(this.handle); | |
} | |
} | |
class ArtMethod { | |
constructor (handle) { | |
this.handle = handle; | |
} | |
prettyMethod (withSignature = true) { | |
const result = new StdString(); | |
getApi()['art::ArtMethod::PrettyMethod'](result, this.handle, withSignature ? 1 : 0); | |
return result.disposeToString(); | |
} | |
toString () { | |
return `ArtMethod(handle=${this.handle})`; | |
} | |
} | |
function makeArtQuickFrameInfoGetter (impl) { | |
return function (self) { | |
const result = Memory.alloc(12); | |
getArtQuickFrameInfoGetterThunk(impl)(result, self); | |
return { | |
frameSizeInBytes: result.readU32(), | |
coreSpillMask: result.add(4).readU32(), | |
fpSpillMask: result.add(8).readU32() | |
}; | |
}; | |
} | |
function _getArtQuickFrameInfoGetterThunk (impl) { | |
let thunk = NULL; | |
switch (Process.arch) { | |
case 'ia32': | |
thunk = makeThunk(32, writer => { | |
writer.putMovRegRegOffsetPtr('ecx', 'esp', 4); // result | |
writer.putMovRegRegOffsetPtr('edx', 'esp', 8); // self | |
writer.putCallAddressWithArguments(impl, ['ecx', 'edx']); | |
// Restore callee's stack frame | |
writer.putMovRegReg('esp', 'ebp'); | |
writer.putPopReg('ebp'); | |
writer.putRet(); | |
}); | |
break; | |
case 'x64': | |
thunk = makeThunk(32, writer => { | |
writer.putPushReg('rdi'); // preserve result buffer pointer | |
writer.putCallAddressWithArguments(impl, ['rsi']); // self | |
writer.putPopReg('rdi'); | |
// Struct is stored by value in the rax and edx registers | |
// Write struct to result buffer | |
writer.putMovRegPtrReg('rdi', 'rax'); | |
writer.putMovRegOffsetPtrReg('rdi', 8, 'edx'); | |
writer.putRet(); | |
}); | |
break; | |
case 'arm': | |
thunk = makeThunk(16, writer => { | |
// By calling convention, we pass a pointer for the result struct | |
writer.putCallAddressWithArguments(impl, ['r0', 'r1']); | |
writer.putPopRegs(['r0', 'lr']); | |
writer.putMovRegReg('pc', 'lr'); | |
}); | |
break; | |
case 'arm64': | |
thunk = makeThunk(64, writer => { | |
writer.putPushRegReg('x0', 'lr'); | |
writer.putCallAddressWithArguments(impl, ['x1']); | |
writer.putPopRegReg('x2', 'lr'); | |
writer.putStrRegRegOffset('x0', 'x2', 0); | |
writer.putStrRegRegOffset('w1', 'x2', 8); | |
writer.putRet(); | |
}); | |
break; | |
} | |
return new NativeFunction(thunk, 'void', ['pointer', 'pointer'], nativeFunctionOptions); | |
} | |
const thunkRelocators = { | |
ia32: global.X86Relocator, | |
x64: global.X86Relocator, | |
arm: global.ThumbRelocator, | |
arm64: global.Arm64Relocator | |
}; | |
const thunkWriters = { | |
ia32: global.X86Writer, | |
x64: global.X86Writer, | |
arm: global.ThumbWriter, | |
arm64: global.Arm64Writer | |
}; | |
function makeThunk (size, write) { | |
if (thunkPage === null) { | |
thunkPage = Memory.alloc(Process.pageSize); | |
} | |
const thunk = thunkPage.add(thunkOffset); | |
const arch = Process.arch; | |
const Writer = thunkWriters[arch]; | |
Memory.patchCode(thunk, size, code => { | |
const writer = new Writer(code, { pc: thunk }); | |
write(writer); | |
writer.flush(); | |
if (writer.offset > size) { | |
throw new Error(`Wrote ${writer.offset}, exceeding maximum of ${size}`); | |
} | |
}); | |
thunkOffset += size; | |
return (arch === 'arm') ? thunk.or(1) : thunk; | |
} | |
function notifyArtMethodHooked (method, vm) { | |
ensureArtKnowsHowToHandleMethodInstrumentation(vm); | |
ensureArtKnowsHowToHandleReplacementMethods(vm); | |
} | |
function makeArtController (vm) { | |
const threadOffsets = getArtThreadSpec(vm).offset; | |
const managedStackOffsets = getArtManagedStackSpec().offset; | |
const code = ` | |
#include <gum/guminterceptor.h> | |
extern GMutex lock; | |
extern GHashTable * methods; | |
extern GHashTable * replacements; | |
extern gpointer last_seen_art_method; | |
extern gpointer get_oat_quick_method_header_impl (gpointer method, gpointer pc); | |
void | |
init (void) | |
{ | |
g_mutex_init (&lock); | |
methods = g_hash_table_new_full (NULL, NULL, NULL, NULL); | |
replacements = g_hash_table_new_full (NULL, NULL, NULL, NULL); | |
} | |
void | |
finalize (void) | |
{ | |
g_hash_table_unref (replacements); | |
g_hash_table_unref (methods); | |
g_mutex_clear (&lock); | |
} | |
gboolean | |
is_replacement_method (gpointer method) | |
{ | |
gboolean is_replacement; | |
g_mutex_lock (&lock); | |
is_replacement = g_hash_table_contains (replacements, method); | |
g_mutex_unlock (&lock); | |
return is_replacement; | |
} | |
gpointer | |
get_replacement_method (gpointer original_method) | |
{ | |
gpointer replacement_method; | |
g_mutex_lock (&lock); | |
replacement_method = g_hash_table_lookup (methods, original_method); | |
g_mutex_unlock (&lock); | |
return replacement_method; | |
} | |
void | |
set_replacement_method (gpointer original_method, | |
gpointer replacement_method) | |
{ | |
g_mutex_lock (&lock); | |
g_hash_table_insert (methods, original_method, replacement_method); | |
g_hash_table_insert (replacements, replacement_method, original_method); | |
g_mutex_unlock (&lock); | |
} | |
void | |
delete_replacement_method (gpointer original_method) | |
{ | |
gpointer replacement_method; | |
g_mutex_lock (&lock); | |
replacement_method = g_hash_table_lookup (methods, original_method); | |
if (replacement_method != NULL) | |
{ | |
g_hash_table_remove (methods, original_method); | |
g_hash_table_remove (replacements, replacement_method); | |
} | |
g_mutex_unlock (&lock); | |
} | |
gpointer | |
translate_method (gpointer method) | |
{ | |
gpointer translated_method; | |
g_mutex_lock (&lock); | |
translated_method = g_hash_table_lookup (replacements, method); | |
g_mutex_unlock (&lock); | |
return (translated_method != NULL) ? translated_method : method; | |
} | |
gpointer | |
find_replacement_method_from_quick_code (gpointer method, | |
gpointer thread) | |
{ | |
gpointer replacement_method; | |
gpointer managed_stack; | |
gpointer top_quick_frame; | |
gpointer link_managed_stack; | |
gpointer * link_top_quick_frame; | |
replacement_method = get_replacement_method (method); | |
if (replacement_method == NULL) | |
return NULL; | |
/* | |
* Stack check. | |
* | |
* Return NULL to indicate that the original method should be invoked, otherwise | |
* return a pointer to the replacement ArtMethod. | |
* | |
* If the caller is our own JNI replacement stub, then a stack transition must | |
* have been pushed onto the current thread's linked list. | |
* | |
* Therefore, we invoke the original method if the following conditions are met: | |
* 1- The current managed stack is empty. | |
* 2- The ArtMethod * inside the linked managed stack's top quick frame is the | |
* same as our replacement. | |
*/ | |
managed_stack = thread + ${threadOffsets.managedStack}; | |
top_quick_frame = *((gpointer *) (managed_stack + ${managedStackOffsets.topQuickFrame})); | |
if (top_quick_frame != NULL) | |
return replacement_method; | |
link_managed_stack = *((gpointer *) (managed_stack + ${managedStackOffsets.link})); | |
if (link_managed_stack == NULL) | |
return replacement_method; | |
link_top_quick_frame = GSIZE_TO_POINTER (*((gsize *) (link_managed_stack + ${managedStackOffsets.topQuickFrame})) & ~((gsize) 1)); | |
if (link_top_quick_frame == NULL || *link_top_quick_frame != replacement_method) | |
return replacement_method; | |
return NULL; | |
} | |
void | |
on_interpreter_do_call (GumInvocationContext * ic) | |
{ | |
gpointer method, replacement_method; | |
method = gum_invocation_context_get_nth_argument (ic, 0); | |
replacement_method = get_replacement_method (method); | |
if (replacement_method != NULL) | |
gum_invocation_context_replace_nth_argument (ic, 0, replacement_method); | |
} | |
gpointer | |
on_art_method_get_oat_quick_method_header (gpointer method, | |
gpointer pc) | |
{ | |
if (is_replacement_method (method)) | |
return NULL; | |
return get_oat_quick_method_header_impl (method, pc); | |
} | |
void | |
on_art_method_pretty_method (GumInvocationContext * ic) | |
{ | |
const guint this_arg_index = ${(Process.arch === 'arm64') ? 0 : 1}; | |
gpointer method; | |
method = gum_invocation_context_get_nth_argument (ic, this_arg_index); | |
if (method == NULL) | |
gum_invocation_context_replace_nth_argument (ic, this_arg_index, last_seen_art_method); | |
else | |
last_seen_art_method = method; | |
} | |
void | |
on_leave_gc_concurrent_copying_copying_phase (GumInvocationContext * ic) | |
{ | |
GHashTableIter iter; | |
gpointer hooked_method, replacement_method; | |
g_mutex_lock (&lock); | |
g_hash_table_iter_init (&iter, methods); | |
while (g_hash_table_iter_next (&iter, &hooked_method, &replacement_method)) | |
*((uint32_t *) replacement_method) = *((uint32_t *) hooked_method); | |
g_mutex_unlock (&lock); | |
} | |
`; | |
const lockSize = 8; | |
const methodsSize = pointerSize; | |
const replacementsSize = pointerSize; | |
const lastSeenArtMethodSize = pointerSize; | |
const data = Memory.alloc(lockSize + methodsSize + replacementsSize + lastSeenArtMethodSize); | |
const lock = data; | |
const methods = lock.add(lockSize); | |
const replacements = methods.add(methodsSize); | |
const lastSeenArtMethod = replacements.add(replacementsSize); | |
const getOatQuickMethodHeaderImpl = Module.findExportByName('libart.so', | |
(pointerSize === 4) | |
? '_ZN3art9ArtMethod23GetOatQuickMethodHeaderEj' | |
: '_ZN3art9ArtMethod23GetOatQuickMethodHeaderEm'); | |
const cm = new CModule(code, { | |
lock, | |
methods, | |
replacements, | |
last_seen_art_method: lastSeenArtMethod, | |
get_oat_quick_method_header_impl: getOatQuickMethodHeaderImpl ?? ptr('0xdeadbeef') | |
}); | |
const fastOptions = { exceptions: 'propagate', scheduling: 'exclusive' }; | |
return { | |
handle: cm, | |
replacedMethods: { | |
isReplacement: new NativeFunction(cm.is_replacement_method, 'bool', ['pointer'], fastOptions), | |
get: new NativeFunction(cm.get_replacement_method, 'pointer', ['pointer'], fastOptions), | |
set: new NativeFunction(cm.set_replacement_method, 'void', ['pointer', 'pointer'], fastOptions), | |
delete: new NativeFunction(cm.delete_replacement_method, 'void', ['pointer'], fastOptions), | |
translate: new NativeFunction(cm.translate_method, 'pointer', ['pointer'], fastOptions), | |
findReplacementFromQuickCode: cm.find_replacement_method_from_quick_code | |
}, | |
getOatQuickMethodHeaderImpl, | |
hooks: { | |
Interpreter: { | |
doCall: cm.on_interpreter_do_call | |
}, | |
ArtMethod: { | |
getOatQuickMethodHeader: cm.on_art_method_get_oat_quick_method_header, | |
prettyMethod: cm.on_art_method_pretty_method | |
}, | |
Gc: { | |
copyingPhase: { | |
onLeave: cm.on_leave_gc_concurrent_copying_copying_phase | |
}, | |
runFlip: { | |
onEnter: cm.on_leave_gc_concurrent_copying_copying_phase | |
} | |
} | |
} | |
}; | |
} | |
function ensureArtKnowsHowToHandleMethodInstrumentation (vm) { | |
if (taughtArtAboutMethodInstrumentation) { | |
return; | |
} | |
taughtArtAboutMethodInstrumentation = true; | |
instrumentArtQuickEntrypoints(vm); | |
instrumentArtMethodInvocationFromInterpreter(); | |
} | |
function instrumentArtQuickEntrypoints (vm) { | |
const api = getApi(); | |
// Entrypoints that dispatch method invocation from the quick ABI. | |
const quickEntrypoints = [ | |
api.artQuickGenericJniTrampoline, | |
api.artQuickToInterpreterBridge, | |
api.artQuickResolutionTrampoline | |
]; | |
quickEntrypoints.forEach(entrypoint => { | |
Memory.protect(entrypoint, 32, 'rwx'); | |
const interceptor = new ArtQuickCodeInterceptor(entrypoint); | |
interceptor.activate(vm); | |
artQuickInterceptors.push(interceptor); | |
}); | |
} | |
function instrumentArtMethodInvocationFromInterpreter () { | |
const apiLevel = getAndroidApiLevel(); | |
let artInterpreterDoCallExportRegex; | |
if (apiLevel <= 22) { | |
artInterpreterDoCallExportRegex = /^_ZN3art11interpreter6DoCallILb[0-1]ELb[0-1]EEEbPNS_6mirror9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE$/; | |
} else if (apiLevel <= 33) { | |
artInterpreterDoCallExportRegex = /^_ZN3art11interpreter6DoCallILb[0-1]ELb[0-1]EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE$/; | |
} else { | |
artInterpreterDoCallExportRegex = /^_ZN3art11interpreter6DoCallILb[0-1]EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtbPNS_6JValueE$/; | |
} | |
let docallFound = false; | |
for (const exp of Module.enumerateExports('libart.so').filter(exp => artInterpreterDoCallExportRegex.test(exp.name))) { | |
docallFound = true; | |
Interceptor.attach(exp.address, artController.hooks.Interpreter.doCall); | |
} | |
if (!docallFound && Process.arch === 'arm64') { | |
let Invoking_percent_s_string_found_addr; | |
let Invoking_percent_s_string = '49 6e 76 6f 6b 69 6e 67 20 25 73'; | |
const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0]; | |
for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, Invoking_percent_s_string)) { | |
if (match) { | |
Invoking_percent_s_string_found_addr = match.address.toString(); | |
break; | |
} | |
} | |
// adrp, add sample | |
// E2 E8 FF F0 42 20 1C 91 || 62 E8 FF 90 42 20 1C 91 | |
// 42 E9 FF F0 42 38 10 91 || 62 E2 FF F0 42 38 10 91 | |
// E2 E8 FF F0 42 20 1C 91 || 62 E8 FF 90 42 20 1C 91 | |
// 02 E7 FF 90 42 F4 0F 91 || E2 E6 FF F0 42 F4 0F 91 | |
let adrp, add; | |
let adrp_add_pattern = '?2 E? FF ?0 42 ?? ?? 91'; | |
let adrp_add_pattern_found_addr; | |
let doCall_func_addr = []; | |
let doCall_func_found_count = 0; | |
const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0]; | |
for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) { | |
let disasm = Instruction.parse(match.address); | |
if (disasm.mnemonic === "adrp") { | |
adrp = disasm.operands.find(op => op.type === 'imm')?.value; | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic !== "add") { | |
disasm = Instruction.parse(disasm.next); | |
} | |
add = disasm.operands.find(op => op.type === 'imm')?.value; | |
if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === Invoking_percent_s_string_found_addr.toString()) { | |
adrp_add_pattern_found_addr = match.address; | |
for (let off = 0;; off += 4) { | |
disasm = Instruction.parse(adrp_add_pattern_found_addr.sub(off)); | |
if (disasm.mnemonic === "str") { | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic === "stp") { | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic === "stp") { | |
doCall_func_found_count++; | |
doCall_func_addr.push(disasm.address.sub(0x8)); | |
break; | |
} | |
} | |
} | |
} | |
if (doCall_func_found_count == 2) { | |
break; | |
} | |
} | |
} | |
} | |
for (const address of doCall_func_addr) { | |
Interceptor.attach(address, artController.hooks.Interpreter.doCall); | |
} | |
} | |
} | |
function ensureArtKnowsHowToHandleReplacementMethods (vm) { | |
if (taughtArtAboutReplacementMethods) { | |
return; | |
} | |
taughtArtAboutReplacementMethods = true; | |
if (!maybeInstrumentGetOatQuickMethodHeaderInlineCopies()) { | |
const { getOatQuickMethodHeaderImpl } = artController; | |
if (getOatQuickMethodHeaderImpl === null) { | |
return; | |
} | |
try { | |
Interceptor.replace(getOatQuickMethodHeaderImpl, artController.hooks.ArtMethod.getOatQuickMethodHeader); | |
} catch (e) { | |
/* | |
* Already replaced by another script. For now we don't support replacing methods from multiple scripts, | |
* but we'll allow users to try it if they're feeling adventurous. | |
*/ | |
} | |
} | |
const apiLevel = getAndroidApiLevel(); | |
let copyingPhase = null; | |
if (apiLevel > 28) { | |
copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12CopyingPhaseEv'); | |
} else if (apiLevel > 22) { | |
copyingPhase = Module.findExportByName('libart.so', '_ZN3art2gc9collector17ConcurrentCopying12MarkingPhaseEv'); | |
} | |
if (Process.arch === 'arm64' && copyingPhase === null) { | |
let CopyingPhase_string_found_addr; | |
let CopyingPhase_string = '43 6f 70 79 69 6e 67 50 68 61 73 65'; | |
const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0]; | |
for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, CopyingPhase_string)) { | |
if (match) { | |
CopyingPhase_string_found_addr = match.address.toString(); | |
break; | |
} | |
} | |
let adrp, add; | |
let adrp_add_pattern = '?1 ?? FF ?0 21 ?? ?? 91'; | |
let adrp_add_in_CopyingPhase_func; | |
const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0]; | |
for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) { | |
let disasm = Instruction.parse(match.address); | |
if (disasm.mnemonic === "adrp") { | |
adrp = disasm.operands.find(op => op.type === 'imm')?.value; | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic !== "add") { | |
disasm = Instruction.parse(disasm.next); | |
} | |
add = disasm.operands.find(op => op.type === 'imm')?.value; | |
if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === CopyingPhase_string_found_addr.toString()) { | |
if (adrp_add_in_CopyingPhase_func === undefined) { | |
adrp_add_in_CopyingPhase_func = match.address; | |
} | |
for (let off = 0;; off += 4) { | |
disasm = Instruction.parse(adrp_add_in_CopyingPhase_func.sub(off)); | |
if (disasm.mnemonic === "sub") { | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic === "stp") { | |
copyingPhase = disasm.address.sub(0x4); | |
break; | |
} | |
} | |
} | |
break; | |
} | |
} | |
} | |
} | |
if (copyingPhase !== null) { | |
Interceptor.attach(copyingPhase, artController.hooks.Gc.copyingPhase); | |
} | |
let runFlip = null; | |
runFlip = Module.findExportByName('libart.so', '_ZN3art6Thread15RunFlipFunctionEPS0_b'); | |
if (runFlip === null) { | |
runFlip = Module.findExportByName('libart.so', '_ZN3art6Thread15RunFlipFunctionEPS0_'); // api 35 | |
} | |
if (runFlip !== null) { | |
Interceptor.attach(runFlip, artController.hooks.Gc.runFlip); | |
} | |
} | |
const artGetOatQuickMethodHeaderInlinedCopyHandler = { | |
arm: { | |
signatures: [ | |
{ | |
pattern: [ | |
'b0 68', // ldr r0, [r6, #8] | |
'01 30', // adds r0, #1 | |
'0c d0', // beq #0x16fcd4 | |
'1b 98', // ldr r0, [sp, #0x6c] | |
':', | |
'c0 ff', | |
'c0 ff', | |
'00 ff', | |
'00 2f' | |
], | |
validateMatch: validateGetOatQuickMethodHeaderInlinedMatchArm | |
}, | |
{ | |
pattern: [ | |
'd8 f8 08 00', // ldr r0, [r8, #8] | |
'01 30', // adds r0, #1 | |
'0c d0', // beq #0x16fcd4 | |
'1b 98', // ldr r0, [sp, #0x6c] | |
':', | |
'f0 ff ff 0f', | |
'ff ff', | |
'00 ff', | |
'00 2f' | |
], | |
validateMatch: validateGetOatQuickMethodHeaderInlinedMatchArm | |
}, | |
{ | |
pattern: [ | |
'b0 68', // ldr r0, [r6, #8] | |
'01 30', // adds r0, #1 | |
'40 f0 c3 80', // bne #0x203bf0 | |
'00 25', // movs r5, #0 | |
':', | |
'c0 ff', | |
'c0 ff', | |
'c0 fb 00 d0', | |
'ff f8' | |
], | |
validateMatch: validateGetOatQuickMethodHeaderInlinedMatchArm | |
} | |
], | |
instrument: instrumentGetOatQuickMethodHeaderInlinedCopyArm | |
}, | |
arm64: { | |
signatures: [ | |
{ | |
pattern: [ | |
/* e8 */ '0a 40 b9', // ldr w8, [x23, #0x8] | |
'1f 05 00 31', // cmn w8, #0x1 | |
'40 01 00 54', // b.eq 0x2e4204 | |
'88 39 00 f0', // adrp x8, 0xa17000 | |
':', | |
/* 00 */ 'fc ff ff', | |
'1f fc ff ff', | |
'1f 00 00 ff', | |
'00 00 00 9f' | |
], | |
offset: 1, | |
validateMatch: validateGetOatQuickMethodHeaderInlinedMatchArm64 | |
}, | |
{ | |
pattern: [ | |
/* e8 */ '0a 40 b9', // ldr w8, [x23, #0x8] | |
'1f 05 00 31', // cmn w8, #0x1 | |
'01 34 00 54', // b.ne 0x3d8e50 | |
'e0 03 1f aa', // mov x0, xzr | |
':', | |
/* 00 */ 'fc ff ff', | |
'1f fc ff ff', | |
'1f 00 00 ff', | |
'e0 ff ff ff' | |
], | |
offset: 1, | |
validateMatch: validateGetOatQuickMethodHeaderInlinedMatchArm64 | |
} | |
], | |
instrument: instrumentGetOatQuickMethodHeaderInlinedCopyArm64 | |
} | |
}; | |
function validateGetOatQuickMethodHeaderInlinedMatchArm ({ address, size }) { | |
const ldr = Instruction.parse(address.or(1)); | |
const [ldrDst, ldrSrc] = ldr.operands; | |
const methodReg = ldrSrc.value.base; | |
const scratchReg = ldrDst.value; | |
const branch = Instruction.parse(ldr.next.add(2)); | |
const targetWhenTrue = ptr(branch.operands[0].value); | |
const targetWhenFalse = branch.address.add(branch.size); | |
let targetWhenRegularMethod, targetWhenRuntimeMethod; | |
if (branch.mnemonic === 'beq') { | |
targetWhenRegularMethod = targetWhenFalse; | |
targetWhenRuntimeMethod = targetWhenTrue; | |
} else { | |
targetWhenRegularMethod = targetWhenTrue; | |
targetWhenRuntimeMethod = targetWhenFalse; | |
} | |
return parseInstructionsAt(targetWhenRegularMethod.or(1), tryParse, { limit: 3 }); | |
function tryParse (insn) { | |
const { mnemonic } = insn; | |
if (!(mnemonic === 'ldr' || mnemonic === 'ldr.w')) { | |
return null; | |
} | |
const { base, disp } = insn.operands[1].value; | |
if (!(base === methodReg && disp === 0x14)) { | |
return null; | |
} | |
return { | |
methodReg, | |
scratchReg, | |
target: { | |
whenTrue: targetWhenTrue, | |
whenRegularMethod: targetWhenRegularMethod, | |
whenRuntimeMethod: targetWhenRuntimeMethod | |
} | |
}; | |
} | |
} | |
function validateGetOatQuickMethodHeaderInlinedMatchArm64 ({ address, size }) { | |
const [ldrDst, ldrSrc] = Instruction.parse(address).operands; | |
const methodReg = ldrSrc.value.base; | |
const scratchReg = 'x' + ldrDst.value.substring(1); | |
const branch = Instruction.parse(address.add(8)); | |
const targetWhenTrue = ptr(branch.operands[0].value); | |
const targetWhenFalse = address.add(12); | |
let targetWhenRegularMethod, targetWhenRuntimeMethod; | |
if (branch.mnemonic === 'b.eq') { | |
targetWhenRegularMethod = targetWhenFalse; | |
targetWhenRuntimeMethod = targetWhenTrue; | |
} else { | |
targetWhenRegularMethod = targetWhenTrue; | |
targetWhenRuntimeMethod = targetWhenFalse; | |
} | |
return parseInstructionsAt(targetWhenRegularMethod, tryParse, { limit: 3 }); | |
function tryParse (insn) { | |
if (insn.mnemonic !== 'ldr') { | |
return null; | |
} | |
const { base, disp } = insn.operands[1].value; | |
if (!(base === methodReg && disp === 0x18)) { | |
return null; | |
} | |
return { | |
methodReg, | |
scratchReg, | |
target: { | |
whenTrue: targetWhenTrue, | |
whenRegularMethod: targetWhenRegularMethod, | |
whenRuntimeMethod: targetWhenRuntimeMethod | |
} | |
}; | |
} | |
} | |
function maybeInstrumentGetOatQuickMethodHeaderInlineCopies () { | |
if (getAndroidApiLevel() < 31) { | |
return false; | |
} | |
const handler = artGetOatQuickMethodHeaderInlinedCopyHandler[Process.arch]; | |
if (handler === undefined) { | |
// Not needed on x86 and x64, at least not for now... | |
return false; | |
} | |
const signatures = handler.signatures.map(({ pattern, offset = 0, validateMatch = returnEmptyObject }) => { | |
return { | |
pattern: new MatchPattern(pattern.join('')), | |
offset, | |
validateMatch | |
}; | |
}); | |
const impls = []; | |
for (const { base, size } of getApi().module.enumerateRanges('--x')) { | |
for (const { pattern, offset, validateMatch } of signatures) { | |
const matches = Memory.scanSync(base, size, pattern) | |
.map(({ address, size }) => { | |
return { address: address.sub(offset), size: size + offset }; | |
}) | |
.filter(match => { | |
const validationResult = validateMatch(match); | |
if (validationResult === null) { | |
return false; | |
} | |
match.validationResult = validationResult; | |
return true; | |
}); | |
impls.push(...matches); | |
} | |
} | |
if (impls.length === 0) { | |
return false; | |
} | |
impls.forEach(handler.instrument); | |
return true; | |
} | |
function returnEmptyObject () { | |
return {}; | |
} | |
class InlineHook { | |
constructor (address, size, trampoline) { | |
this.address = address; | |
this.size = size; | |
this.originalCode = address.readByteArray(size); | |
this.trampoline = trampoline; | |
} | |
revert () { | |
Memory.patchCode(this.address, this.size, code => { | |
code.writeByteArray(this.originalCode); | |
}); | |
} | |
} | |
function instrumentGetOatQuickMethodHeaderInlinedCopyArm ({ address, size, validationResult }) { | |
const { methodReg, target } = validationResult; | |
const trampoline = Memory.alloc(Process.pageSize); | |
let redirectCapacity = size; | |
Memory.patchCode(trampoline, 256, code => { | |
const writer = new ThumbWriter(code, { pc: trampoline }); | |
const relocator = new ThumbRelocator(address, writer); | |
for (let i = 0; i !== 2; i++) { | |
relocator.readOne(); | |
} | |
relocator.writeAll(); | |
relocator.readOne(); | |
relocator.skipOne(); | |
writer.putBCondLabel('eq', 'runtime_or_replacement_method'); | |
const vpushFpRegs = [0x2d, 0xed, 0x10, 0x0a]; /* vpush {s0-s15} */ | |
writer.putBytes(vpushFpRegs); | |
const savedRegs = ['r0', 'r1', 'r2', 'r3']; | |
writer.putPushRegs(savedRegs); | |
writer.putCallAddressWithArguments(artController.replacedMethods.isReplacement, [methodReg]); | |
writer.putCmpRegImm('r0', 0); | |
writer.putPopRegs(savedRegs); | |
const vpopFpRegs = [0xbd, 0xec, 0x10, 0x0a]; /* vpop {s0-s15} */ | |
writer.putBytes(vpopFpRegs); | |
writer.putBCondLabel('ne', 'runtime_or_replacement_method'); | |
writer.putBLabel('regular_method'); | |
relocator.readOne(); | |
const tailIsRegular = relocator.input.address.equals(target.whenRegularMethod); | |
writer.putLabel(tailIsRegular ? 'regular_method' : 'runtime_or_replacement_method'); | |
relocator.writeOne(); | |
while (redirectCapacity < 10) { | |
const offset = relocator.readOne(); | |
if (offset === 0) { | |
redirectCapacity = 10; | |
break; | |
} | |
redirectCapacity = offset; | |
} | |
relocator.writeAll(); | |
writer.putBranchAddress(address.add(redirectCapacity + 1)); | |
writer.putLabel(tailIsRegular ? 'runtime_or_replacement_method' : 'regular_method'); | |
writer.putBranchAddress(target.whenTrue); | |
writer.flush(); | |
}); | |
inlineHooks.push(new InlineHook(address, redirectCapacity, trampoline)); | |
Memory.patchCode(address, redirectCapacity, code => { | |
const writer = new ThumbWriter(code, { pc: address }); | |
writer.putLdrRegAddress('pc', trampoline.or(1)); | |
writer.flush(); | |
}); | |
} | |
function instrumentGetOatQuickMethodHeaderInlinedCopyArm64 ({ address, size, validationResult }) { | |
const { methodReg, scratchReg, target } = validationResult; | |
const trampoline = Memory.alloc(Process.pageSize); | |
Memory.patchCode(trampoline, 256, code => { | |
const writer = new Arm64Writer(code, { pc: trampoline }); | |
const relocator = new Arm64Relocator(address, writer); | |
for (let i = 0; i !== 2; i++) { | |
relocator.readOne(); | |
} | |
relocator.writeAll(); | |
relocator.readOne(); | |
relocator.skipOne(); | |
writer.putBCondLabel('eq', 'runtime_or_replacement_method'); | |
const savedRegs = [ | |
'd0', 'd1', | |
'd2', 'd3', | |
'd4', 'd5', | |
'd6', 'd7', | |
'x0', 'x1', | |
'x2', 'x3', | |
'x4', 'x5', | |
'x6', 'x7', | |
'x8', 'x9', | |
'x10', 'x11', | |
'x12', 'x13', | |
'x14', 'x15', | |
'x16', 'x17' | |
]; | |
const numSavedRegs = savedRegs.length; | |
for (let i = 0; i !== numSavedRegs; i += 2) { | |
writer.putPushRegReg(savedRegs[i], savedRegs[i + 1]); | |
} | |
writer.putCallAddressWithArguments(artController.replacedMethods.isReplacement, [methodReg]); | |
writer.putCmpRegReg('x0', 'xzr'); | |
for (let i = numSavedRegs - 2; i >= 0; i -= 2) { | |
writer.putPopRegReg(savedRegs[i], savedRegs[i + 1]); | |
} | |
writer.putBCondLabel('ne', 'runtime_or_replacement_method'); | |
writer.putBLabel('regular_method'); | |
relocator.readOne(); | |
const tailInstruction = relocator.input; | |
const tailIsRegular = tailInstruction.address.equals(target.whenRegularMethod); | |
writer.putLabel(tailIsRegular ? 'regular_method' : 'runtime_or_replacement_method'); | |
relocator.writeOne(); | |
writer.putBranchAddress(tailInstruction.next); | |
writer.putLabel(tailIsRegular ? 'runtime_or_replacement_method' : 'regular_method'); | |
writer.putBranchAddress(target.whenTrue); | |
writer.flush(); | |
}); | |
inlineHooks.push(new InlineHook(address, size, trampoline)); | |
Memory.patchCode(address, size, code => { | |
const writer = new Arm64Writer(code, { pc: address }); | |
writer.putLdrRegAddress(scratchReg, trampoline); | |
writer.putBrReg(scratchReg); | |
writer.flush(); | |
}); | |
} | |
function makeMethodMangler (methodId) { | |
return new MethodMangler(methodId); | |
} | |
function translateMethod (methodId) { | |
return artController.replacedMethods.translate(methodId); | |
} | |
function backtrace (vm, options = {}) { | |
const { limit = 16 } = options; | |
const env = vm.getEnv(); | |
if (backtraceModule === null) { | |
backtraceModule = makeBacktraceModule(vm, env); | |
} | |
return backtraceModule.backtrace(env, limit); | |
} | |
function makeBacktraceModule (vm, env) { | |
const api = getApi(); | |
const performImpl = Memory.alloc(Process.pointerSize); | |
const cm = new CModule(` | |
#include <glib.h> | |
#include <stdbool.h> | |
#include <string.h> | |
#include <gum/gumtls.h> | |
#include <json-glib/json-glib.h> | |
typedef struct _ArtBacktrace ArtBacktrace; | |
typedef struct _ArtStackFrame ArtStackFrame; | |
typedef struct _ArtStackVisitor ArtStackVisitor; | |
typedef struct _ArtStackVisitorVTable ArtStackVisitorVTable; | |
typedef struct _ArtClass ArtClass; | |
typedef struct _ArtMethod ArtMethod; | |
typedef struct _ArtThread ArtThread; | |
typedef struct _ArtContext ArtContext; | |
typedef struct _JNIEnv JNIEnv; | |
typedef struct _StdString StdString; | |
typedef struct _StdTinyString StdTinyString; | |
typedef struct _StdLargeString StdLargeString; | |
typedef enum { | |
STACK_WALK_INCLUDE_INLINED_FRAMES, | |
STACK_WALK_SKIP_INLINED_FRAMES, | |
} StackWalkKind; | |
struct _StdTinyString | |
{ | |
guint8 unused; | |
gchar data[(3 * sizeof (gpointer)) - 1]; | |
}; | |
struct _StdLargeString | |
{ | |
gsize capacity; | |
gsize size; | |
gchar * data; | |
}; | |
struct _StdString | |
{ | |
union | |
{ | |
guint8 flags; | |
StdTinyString tiny; | |
StdLargeString large; | |
}; | |
}; | |
struct _ArtBacktrace | |
{ | |
GChecksum * id; | |
GArray * frames; | |
gchar * frames_json; | |
}; | |
struct _ArtStackFrame | |
{ | |
ArtMethod * method; | |
gsize dexpc; | |
StdString description; | |
}; | |
struct _ArtStackVisitorVTable | |
{ | |
void (* unused1) (void); | |
void (* unused2) (void); | |
bool (* visit) (ArtStackVisitor * visitor); | |
}; | |
struct _ArtStackVisitor | |
{ | |
ArtStackVisitorVTable * vtable; | |
guint8 padding[512]; | |
ArtStackVisitorVTable vtable_storage; | |
ArtBacktrace * backtrace; | |
}; | |
struct _ArtMethod | |
{ | |
guint32 declaring_class; | |
guint32 access_flags; | |
}; | |
extern GumTlsKey current_backtrace; | |
extern void (* perform_art_thread_state_transition) (JNIEnv * env); | |
extern ArtContext * art_thread_get_long_jump_context (ArtThread * thread); | |
extern void art_stack_visitor_init (ArtStackVisitor * visitor, ArtThread * thread, void * context, StackWalkKind walk_kind, | |
size_t num_frames, bool check_suspended); | |
extern void art_stack_visitor_walk_stack (ArtStackVisitor * visitor, bool include_transitions); | |
extern ArtMethod * art_stack_visitor_get_method (ArtStackVisitor * visitor); | |
extern void art_stack_visitor_describe_location (StdString * description, ArtStackVisitor * visitor); | |
extern ArtMethod * translate_method (ArtMethod * method); | |
extern void translate_location (ArtMethod * method, guint32 pc, const gchar ** source_file, gint32 * line_number); | |
extern void get_class_location (StdString * result, ArtClass * klass); | |
extern void cxx_delete (void * mem); | |
extern unsigned long strtoul (const char * str, char ** endptr, int base); | |
static bool visit_frame (ArtStackVisitor * visitor); | |
static void art_stack_frame_destroy (ArtStackFrame * frame); | |
static void append_jni_type_name (GString * s, const gchar * name, gsize length); | |
static void std_string_destroy (StdString * str); | |
static gchar * std_string_get_data (StdString * str); | |
void | |
init (void) | |
{ | |
current_backtrace = gum_tls_key_new (); | |
} | |
void | |
finalize (void) | |
{ | |
gum_tls_key_free (current_backtrace); | |
} | |
ArtBacktrace * | |
_create (JNIEnv * env, | |
guint limit) | |
{ | |
ArtBacktrace * bt; | |
bt = g_new (ArtBacktrace, 1); | |
bt->id = g_checksum_new (G_CHECKSUM_SHA1); | |
bt->frames = (limit != 0) | |
? g_array_sized_new (FALSE, FALSE, sizeof (ArtStackFrame), limit) | |
: g_array_new (FALSE, FALSE, sizeof (ArtStackFrame)); | |
g_array_set_clear_func (bt->frames, (GDestroyNotify) art_stack_frame_destroy); | |
bt->frames_json = NULL; | |
gum_tls_key_set_value (current_backtrace, bt); | |
perform_art_thread_state_transition (env); | |
gum_tls_key_set_value (current_backtrace, NULL); | |
return bt; | |
} | |
void | |
_on_thread_state_transition_complete (ArtThread * thread) | |
{ | |
ArtContext * context; | |
ArtStackVisitor visitor = { | |
.vtable_storage = { | |
.visit = visit_frame, | |
}, | |
}; | |
context = art_thread_get_long_jump_context (thread); | |
art_stack_visitor_init (&visitor, thread, context, STACK_WALK_SKIP_INLINED_FRAMES, 0, true); | |
visitor.vtable = &visitor.vtable_storage; | |
visitor.backtrace = gum_tls_key_get_value (current_backtrace); | |
art_stack_visitor_walk_stack (&visitor, false); | |
cxx_delete (context); | |
} | |
static bool | |
visit_frame (ArtStackVisitor * visitor) | |
{ | |
ArtBacktrace * bt = visitor->backtrace; | |
ArtStackFrame frame; | |
const gchar * description, * dexpc_part; | |
frame.method = art_stack_visitor_get_method (visitor); | |
art_stack_visitor_describe_location (&frame.description, visitor); | |
description = std_string_get_data (&frame.description); | |
if (strstr (description, " '<") != NULL) | |
goto skip; | |
dexpc_part = strstr (description, " at dex PC 0x"); | |
if (dexpc_part == NULL) | |
goto skip; | |
frame.dexpc = strtoul (dexpc_part + 13, NULL, 16); | |
g_array_append_val (bt->frames, frame); | |
g_checksum_update (bt->id, (guchar *) &frame.method, sizeof (frame.method)); | |
g_checksum_update (bt->id, (guchar *) &frame.dexpc, sizeof (frame.dexpc)); | |
return true; | |
skip: | |
std_string_destroy (&frame.description); | |
return true; | |
} | |
static void | |
art_stack_frame_destroy (ArtStackFrame * frame) | |
{ | |
std_string_destroy (&frame->description); | |
} | |
void | |
_destroy (ArtBacktrace * backtrace) | |
{ | |
g_free (backtrace->frames_json); | |
g_array_free (backtrace->frames, TRUE); | |
g_checksum_free (backtrace->id); | |
g_free (backtrace); | |
} | |
const gchar * | |
_get_id (ArtBacktrace * backtrace) | |
{ | |
return g_checksum_get_string (backtrace->id); | |
} | |
const gchar * | |
_get_frames (ArtBacktrace * backtrace) | |
{ | |
GArray * frames = backtrace->frames; | |
JsonBuilder * b; | |
guint i; | |
JsonNode * root; | |
if (backtrace->frames_json != NULL) | |
return backtrace->frames_json; | |
b = json_builder_new_immutable (); | |
json_builder_begin_array (b); | |
for (i = 0; i != frames->len; i++) | |
{ | |
ArtStackFrame * frame = &g_array_index (frames, ArtStackFrame, i); | |
gchar * description, * ret_type, * paren_open, * paren_close, * arg_types, * token, * method_name, * class_name; | |
GString * signature; | |
gchar * cursor; | |
ArtMethod * translated_method; | |
StdString location; | |
gsize dexpc; | |
const gchar * source_file; | |
gint32 line_number; | |
description = std_string_get_data (&frame->description); | |
ret_type = strchr (description, '\\'') + 1; | |
paren_open = strchr (ret_type, '('); | |
paren_close = strchr (paren_open, ')'); | |
*paren_open = '\\0'; | |
*paren_close = '\\0'; | |
arg_types = paren_open + 1; | |
token = strrchr (ret_type, '.'); | |
*token = '\\0'; | |
method_name = token + 1; | |
token = strrchr (ret_type, ' '); | |
*token = '\\0'; | |
class_name = token + 1; | |
signature = g_string_sized_new (128); | |
append_jni_type_name (signature, class_name, method_name - class_name - 1); | |
g_string_append_c (signature, ','); | |
g_string_append (signature, method_name); | |
g_string_append (signature, ",("); | |
if (arg_types != paren_close) | |
{ | |
for (cursor = arg_types; cursor != NULL;) | |
{ | |
gsize length; | |
gchar * next; | |
token = strstr (cursor, ", "); | |
if (token != NULL) | |
{ | |
length = token - cursor; | |
next = token + 2; | |
} | |
else | |
{ | |
length = paren_close - cursor; | |
next = NULL; | |
} | |
append_jni_type_name (signature, cursor, length); | |
cursor = next; | |
} | |
} | |
g_string_append_c (signature, ')'); | |
append_jni_type_name (signature, ret_type, class_name - ret_type - 1); | |
translated_method = translate_method (frame->method); | |
dexpc = (translated_method == frame->method) ? frame->dexpc : 0; | |
get_class_location (&location, GSIZE_TO_POINTER (translated_method->declaring_class)); | |
translate_location (translated_method, dexpc, &source_file, &line_number); | |
json_builder_begin_object (b); | |
json_builder_set_member_name (b, "signature"); | |
json_builder_add_string_value (b, signature->str); | |
json_builder_set_member_name (b, "origin"); | |
json_builder_add_string_value (b, std_string_get_data (&location)); | |
json_builder_set_member_name (b, "className"); | |
json_builder_add_string_value (b, class_name); | |
json_builder_set_member_name (b, "methodName"); | |
json_builder_add_string_value (b, method_name); | |
json_builder_set_member_name (b, "methodFlags"); | |
json_builder_add_int_value (b, translated_method->access_flags); | |
json_builder_set_member_name (b, "fileName"); | |
json_builder_add_string_value (b, source_file); | |
json_builder_set_member_name (b, "lineNumber"); | |
json_builder_add_int_value (b, line_number); | |
json_builder_end_object (b); | |
std_string_destroy (&location); | |
g_string_free (signature, TRUE); | |
} | |
json_builder_end_array (b); | |
root = json_builder_get_root (b); | |
backtrace->frames_json = json_to_string (root, FALSE); | |
json_node_unref (root); | |
return backtrace->frames_json; | |
} | |
static void | |
append_jni_type_name (GString * s, | |
const gchar * name, | |
gsize length) | |
{ | |
gchar shorty = '\\0'; | |
gsize i; | |
switch (name[0]) | |
{ | |
case 'b': | |
if (strncmp (name, "boolean", length) == 0) | |
shorty = 'Z'; | |
else if (strncmp (name, "byte", length) == 0) | |
shorty = 'B'; | |
break; | |
case 'c': | |
if (strncmp (name, "char", length) == 0) | |
shorty = 'C'; | |
break; | |
case 'd': | |
if (strncmp (name, "double", length) == 0) | |
shorty = 'D'; | |
break; | |
case 'f': | |
if (strncmp (name, "float", length) == 0) | |
shorty = 'F'; | |
break; | |
case 'i': | |
if (strncmp (name, "int", length) == 0) | |
shorty = 'I'; | |
break; | |
case 'l': | |
if (strncmp (name, "long", length) == 0) | |
shorty = 'J'; | |
break; | |
case 's': | |
if (strncmp (name, "short", length) == 0) | |
shorty = 'S'; | |
break; | |
case 'v': | |
if (strncmp (name, "void", length) == 0) | |
shorty = 'V'; | |
break; | |
} | |
if (shorty != '\\0') | |
{ | |
g_string_append_c (s, shorty); | |
return; | |
} | |
if (length > 2 && name[length - 2] == '[' && name[length - 1] == ']') | |
{ | |
g_string_append_c (s, '['); | |
append_jni_type_name (s, name, length - 2); | |
return; | |
} | |
g_string_append_c (s, 'L'); | |
for (i = 0; i != length; i++) | |
{ | |
gchar ch = name[i]; | |
if (ch != '.') | |
g_string_append_c (s, ch); | |
else | |
g_string_append_c (s, '/'); | |
} | |
g_string_append_c (s, ';'); | |
} | |
static void | |
std_string_destroy (StdString * str) | |
{ | |
bool is_large = (str->flags & 1) != 0; | |
if (is_large) | |
cxx_delete (str->large.data); | |
} | |
static gchar * | |
std_string_get_data (StdString * str) | |
{ | |
bool is_large = (str->flags & 1) != 0; | |
return is_large ? str->large.data : str->tiny.data; | |
} | |
`, { | |
current_backtrace: Memory.alloc(Process.pointerSize), | |
perform_art_thread_state_transition: performImpl, | |
art_thread_get_long_jump_context: api['art::Thread::GetLongJumpContext'], | |
art_stack_visitor_init: api['art::StackVisitor::StackVisitor'], | |
art_stack_visitor_walk_stack: api['art::StackVisitor::WalkStack'], | |
art_stack_visitor_get_method: api['art::StackVisitor::GetMethod'], | |
art_stack_visitor_describe_location: api['art::StackVisitor::DescribeLocation'], | |
translate_method: artController.replacedMethods.translate, | |
translate_location: api['art::Monitor::TranslateLocation'], | |
get_class_location: api['art::mirror::Class::GetLocation'], | |
cxx_delete: api.$delete, | |
strtoul: Module.getExportByName('libc.so', 'strtoul') | |
}); | |
const _create = new NativeFunction(cm._create, 'pointer', ['pointer', 'uint'], nativeFunctionOptions); | |
const _destroy = new NativeFunction(cm._destroy, 'void', ['pointer'], nativeFunctionOptions); | |
const fastOptions = { exceptions: 'propagate', scheduling: 'exclusive' }; | |
const _getId = new NativeFunction(cm._get_id, 'pointer', ['pointer'], fastOptions); | |
const _getFrames = new NativeFunction(cm._get_frames, 'pointer', ['pointer'], fastOptions); | |
const performThreadStateTransition = makeArtThreadStateTransitionImpl(vm, env, cm._on_thread_state_transition_complete); | |
cm._performData = performThreadStateTransition; | |
performImpl.writePointer(performThreadStateTransition); | |
cm.backtrace = (env, limit) => { | |
const handle = _create(env, limit); | |
const bt = new Backtrace(handle); | |
Script.bindWeak(bt, destroy.bind(null, handle)); | |
return bt; | |
}; | |
function destroy (handle) { | |
_destroy(handle); | |
} | |
cm.getId = handle => { | |
return _getId(handle).readUtf8String(); | |
}; | |
cm.getFrames = handle => { | |
return JSON.parse(_getFrames(handle).readUtf8String()); | |
}; | |
return cm; | |
} | |
class Backtrace { | |
constructor (handle) { | |
this.handle = handle; | |
} | |
get id () { | |
return backtraceModule.getId(this.handle); | |
} | |
get frames () { | |
return backtraceModule.getFrames(this.handle); | |
} | |
} | |
function revertGlobalPatches () { | |
patchedClasses.forEach(entry => { | |
entry.vtablePtr.writePointer(entry.vtable); | |
entry.vtableCountPtr.writeS32(entry.vtableCount); | |
}); | |
patchedClasses.clear(); | |
for (const interceptor of artQuickInterceptors.splice(0)) { | |
interceptor.deactivate(); | |
} | |
for (const hook of inlineHooks.splice(0)) { | |
hook.revert(); | |
} | |
} | |
function unwrapMethodId (methodId) { | |
const api = getApi(); | |
const runtimeOffset = getArtRuntimeSpec(api).offset; | |
const jniIdManagerOffset = runtimeOffset.jniIdManager; | |
const jniIdsIndirectionOffset = runtimeOffset.jniIdsIndirection; | |
if (jniIdManagerOffset !== null && jniIdsIndirectionOffset !== null) { | |
const runtime = api.artRuntime; | |
const jniIdsIndirection = runtime.add(jniIdsIndirectionOffset).readInt(); | |
if (jniIdsIndirection !== kPointer) { | |
const jniIdManager = runtime.add(jniIdManagerOffset).readPointer(); | |
return api['art::jni::JniIdManager::DecodeMethodId'](jniIdManager, methodId); | |
} | |
} | |
return methodId; | |
} | |
const artQuickCodeReplacementTrampolineWriters = { | |
ia32: writeArtQuickCodeReplacementTrampolineIA32, | |
x64: writeArtQuickCodeReplacementTrampolineX64, | |
arm: writeArtQuickCodeReplacementTrampolineArm, | |
arm64: writeArtQuickCodeReplacementTrampolineArm64 | |
}; | |
function writeArtQuickCodeReplacementTrampolineIA32 (trampoline, target, redirectSize, constraints, vm) { | |
const threadOffsets = getArtThreadSpec(vm).offset; | |
const artMethodOffsets = getArtMethodSpec(vm).offset; | |
let offset; | |
Memory.patchCode(trampoline, 128, code => { | |
const writer = new X86Writer(code, { pc: trampoline }); | |
const relocator = new X86Relocator(target, writer); | |
const fxsave = [0x0f, 0xae, 0x04, 0x24]; /* fxsave [esp] */ | |
const fxrstor = [0x0f, 0xae, 0x0c, 0x24]; /* fxrstor [esp] */ | |
// Save core args & callee-saves. | |
writer.putPushax(); | |
writer.putMovRegReg('ebp', 'esp'); | |
// Save FPRs + alignment padding. | |
writer.putAndRegU32('esp', 0xfffffff0); | |
writer.putSubRegImm('esp', 512); | |
writer.putBytes(fxsave); | |
writer.putMovRegFsU32Ptr('ebx', threadOffsets.self); | |
writer.putCallAddressWithAlignedArguments(artController.replacedMethods.findReplacementFromQuickCode, ['eax', 'ebx']); | |
writer.putTestRegReg('eax', 'eax'); | |
writer.putJccShortLabel('je', 'restore_registers', 'no-hint'); | |
// Set value of eax in the current frame. | |
writer.putMovRegOffsetPtrReg('ebp', 7 * 4, 'eax'); | |
writer.putLabel('restore_registers'); | |
// Restore FPRs. | |
writer.putBytes(fxrstor); | |
writer.putMovRegReg('esp', 'ebp'); | |
// Restore core args & callee-saves. | |
writer.putPopax(); | |
writer.putJccShortLabel('jne', 'invoke_replacement', 'no-hint'); | |
do { | |
offset = relocator.readOne(); | |
} while (offset < redirectSize && !relocator.eoi); | |
relocator.writeAll(); | |
if (!relocator.eoi) { | |
writer.putJmpAddress(target.add(offset)); | |
} | |
writer.putLabel('invoke_replacement'); | |
writer.putJmpRegOffsetPtr('eax', artMethodOffsets.quickCode); | |
writer.flush(); | |
}); | |
return offset; | |
} | |
function writeArtQuickCodeReplacementTrampolineX64 (trampoline, target, redirectSize, constraints, vm) { | |
const threadOffsets = getArtThreadSpec(vm).offset; | |
const artMethodOffsets = getArtMethodSpec(vm).offset; | |
let offset; | |
Memory.patchCode(trampoline, 256, code => { | |
const writer = new X86Writer(code, { pc: trampoline }); | |
const relocator = new X86Relocator(target, writer); | |
const fxsave = [0x0f, 0xae, 0x04, 0x24]; /* fxsave [rsp] */ | |
const fxrstor = [0x0f, 0xae, 0x0c, 0x24]; /* fxrstor [rsp] */ | |
// Save core args & callee-saves. | |
writer.putPushax(); | |
writer.putMovRegReg('rbp', 'rsp'); | |
// Save FPRs + alignment padding. | |
writer.putAndRegU32('rsp', 0xfffffff0); | |
writer.putSubRegImm('rsp', 512); | |
writer.putBytes(fxsave); | |
writer.putMovRegGsU32Ptr('rbx', threadOffsets.self); | |
writer.putCallAddressWithAlignedArguments(artController.replacedMethods.findReplacementFromQuickCode, ['rdi', 'rbx']); | |
writer.putTestRegReg('rax', 'rax'); | |
writer.putJccShortLabel('je', 'restore_registers', 'no-hint'); | |
// Set value of rdi in the current frame. | |
writer.putMovRegOffsetPtrReg('rbp', 8 * 8, 'rax'); | |
writer.putLabel('restore_registers'); | |
// Restore FPRs. | |
writer.putBytes(fxrstor); | |
writer.putMovRegReg('rsp', 'rbp'); | |
// Restore core args & callee-saves. | |
writer.putPopax(); | |
writer.putJccShortLabel('jne', 'invoke_replacement', 'no-hint'); | |
do { | |
offset = relocator.readOne(); | |
} while (offset < redirectSize && !relocator.eoi); | |
relocator.writeAll(); | |
if (!relocator.eoi) { | |
writer.putJmpAddress(target.add(offset)); | |
} | |
writer.putLabel('invoke_replacement'); | |
writer.putJmpRegOffsetPtr('rdi', artMethodOffsets.quickCode); | |
writer.flush(); | |
}); | |
return offset; | |
} | |
function writeArtQuickCodeReplacementTrampolineArm (trampoline, target, redirectSize, constraints, vm) { | |
const artMethodOffsets = getArtMethodSpec(vm).offset; | |
const targetAddress = target.and(THUMB_BIT_REMOVAL_MASK); | |
let offset; | |
Memory.patchCode(trampoline, 128, code => { | |
const writer = new ThumbWriter(code, { pc: trampoline }); | |
const relocator = new ThumbRelocator(targetAddress, writer); | |
const vpushFpRegs = [0x2d, 0xed, 0x10, 0x0a]; /* vpush {s0-s15} */ | |
const vpopFpRegs = [0xbd, 0xec, 0x10, 0x0a]; /* vpop {s0-s15} */ | |
// Save core args, callee-saves, LR. | |
writer.putPushRegs([ | |
'r1', | |
'r2', | |
'r3', | |
'r5', | |
'r6', | |
'r7', | |
'r8', | |
'r10', | |
'r11', | |
'lr' | |
]); | |
// Save FPRs. | |
writer.putBytes(vpushFpRegs); | |
// Save ArtMethod* + alignment padding. | |
writer.putSubRegRegImm('sp', 'sp', 8); | |
writer.putStrRegRegOffset('r0', 'sp', 0); | |
writer.putCallAddressWithArguments(artController.replacedMethods.findReplacementFromQuickCode, ['r0', 'r9']); | |
writer.putCmpRegImm('r0', 0); | |
writer.putBCondLabel('eq', 'restore_registers'); | |
// Set value of r0 in the current frame. | |
writer.putStrRegRegOffset('r0', 'sp', 0); | |
writer.putLabel('restore_registers'); | |
// Restore ArtMethod* | |
writer.putLdrRegRegOffset('r0', 'sp', 0); | |
writer.putAddRegRegImm('sp', 'sp', 8); | |
// Restore FPRs. | |
writer.putBytes(vpopFpRegs); | |
// Restore LR, callee-saves & core args. | |
writer.putPopRegs([ | |
'lr', | |
'r11', | |
'r10', | |
'r8', | |
'r7', | |
'r6', | |
'r5', | |
'r3', | |
'r2', | |
'r1' | |
]); | |
writer.putBCondLabel('ne', 'invoke_replacement'); | |
do { | |
offset = relocator.readOne(); | |
} while (offset < redirectSize && !relocator.eoi); | |
relocator.writeAll(); | |
if (!relocator.eoi) { | |
writer.putLdrRegAddress('pc', target.add(offset)); | |
} | |
writer.putLabel('invoke_replacement'); | |
writer.putLdrRegRegOffset('pc', 'r0', artMethodOffsets.quickCode); | |
writer.flush(); | |
}); | |
return offset; | |
} | |
function writeArtQuickCodeReplacementTrampolineArm64 (trampoline, target, redirectSize, { availableScratchRegs }, vm) { | |
const artMethodOffsets = getArtMethodSpec(vm).offset; | |
let offset; | |
Memory.patchCode(trampoline, 256, code => { | |
const writer = new Arm64Writer(code, { pc: trampoline }); | |
const relocator = new Arm64Relocator(target, writer); | |
// Save FPRs. | |
writer.putPushRegReg('d0', 'd1'); | |
writer.putPushRegReg('d2', 'd3'); | |
writer.putPushRegReg('d4', 'd5'); | |
writer.putPushRegReg('d6', 'd7'); | |
// Save core args, callee-saves & LR. | |
writer.putPushRegReg('x1', 'x2'); | |
writer.putPushRegReg('x3', 'x4'); | |
writer.putPushRegReg('x5', 'x6'); | |
writer.putPushRegReg('x7', 'x20'); | |
writer.putPushRegReg('x21', 'x22'); | |
writer.putPushRegReg('x23', 'x24'); | |
writer.putPushRegReg('x25', 'x26'); | |
writer.putPushRegReg('x27', 'x28'); | |
writer.putPushRegReg('x29', 'lr'); | |
// Save ArtMethod* + alignment padding. | |
writer.putSubRegRegImm('sp', 'sp', 16); | |
writer.putStrRegRegOffset('x0', 'sp', 0); | |
writer.putCallAddressWithArguments(artController.replacedMethods.findReplacementFromQuickCode, ['x0', 'x19']); | |
writer.putCmpRegReg('x0', 'xzr'); | |
writer.putBCondLabel('eq', 'restore_registers'); | |
// Set value of x0 in the current frame. | |
writer.putStrRegRegOffset('x0', 'sp', 0); | |
writer.putLabel('restore_registers'); | |
// Restore ArtMethod* | |
writer.putLdrRegRegOffset('x0', 'sp', 0); | |
writer.putAddRegRegImm('sp', 'sp', 16); | |
// Restore core args, callee-saves & LR. | |
writer.putPopRegReg('x29', 'lr'); | |
writer.putPopRegReg('x27', 'x28'); | |
writer.putPopRegReg('x25', 'x26'); | |
writer.putPopRegReg('x23', 'x24'); | |
writer.putPopRegReg('x21', 'x22'); | |
writer.putPopRegReg('x7', 'x20'); | |
writer.putPopRegReg('x5', 'x6'); | |
writer.putPopRegReg('x3', 'x4'); | |
writer.putPopRegReg('x1', 'x2'); | |
// Restore FPRs. | |
writer.putPopRegReg('d6', 'd7'); | |
writer.putPopRegReg('d4', 'd5'); | |
writer.putPopRegReg('d2', 'd3'); | |
writer.putPopRegReg('d0', 'd1'); | |
writer.putBCondLabel('ne', 'invoke_replacement'); | |
do { | |
offset = relocator.readOne(); | |
} while (offset < redirectSize && !relocator.eoi); | |
relocator.writeAll(); | |
if (!relocator.eoi) { | |
const scratchReg = Array.from(availableScratchRegs)[0]; | |
writer.putLdrRegAddress(scratchReg, target.add(offset)); | |
writer.putBrReg(scratchReg); | |
} | |
writer.putLabel('invoke_replacement'); | |
writer.putLdrRegRegOffset('x16', 'x0', artMethodOffsets.quickCode); | |
writer.putBrReg('x16'); | |
writer.flush(); | |
}); | |
return offset; | |
} | |
const artQuickCodePrologueWriters = { | |
ia32: writeArtQuickCodePrologueX86, | |
x64: writeArtQuickCodePrologueX86, | |
arm: writeArtQuickCodePrologueArm, | |
arm64: writeArtQuickCodePrologueArm64 | |
}; | |
function writeArtQuickCodePrologueX86 (target, trampoline, redirectSize) { | |
Memory.patchCode(target, 16, code => { | |
const writer = new X86Writer(code, { pc: target }); | |
writer.putJmpAddress(trampoline); | |
writer.flush(); | |
}); | |
} | |
function writeArtQuickCodePrologueArm (target, trampoline, redirectSize) { | |
const targetAddress = target.and(THUMB_BIT_REMOVAL_MASK); | |
Memory.patchCode(targetAddress, 16, code => { | |
const writer = new ThumbWriter(code, { pc: targetAddress }); | |
writer.putLdrRegAddress('pc', trampoline.or(1)); | |
writer.flush(); | |
}); | |
} | |
function writeArtQuickCodePrologueArm64 (target, trampoline, redirectSize) { | |
Memory.patchCode(target, 16, code => { | |
const writer = new Arm64Writer(code, { pc: target }); | |
if (redirectSize === 16) { | |
writer.putLdrRegAddress('x16', trampoline); | |
} else { | |
writer.putAdrpRegAddress('x16', trampoline); | |
} | |
writer.putBrReg('x16'); | |
writer.flush(); | |
}); | |
} | |
const artQuickCodeHookRedirectSize = { | |
ia32: 5, | |
x64: 16, | |
arm: 8, | |
arm64: 16 | |
}; | |
class ArtQuickCodeInterceptor { | |
constructor (quickCode) { | |
this.quickCode = quickCode; | |
this.quickCodeAddress = (Process.arch === 'arm') | |
? quickCode.and(THUMB_BIT_REMOVAL_MASK) | |
: quickCode; | |
this.redirectSize = 0; | |
this.trampoline = null; | |
this.overwrittenPrologue = null; | |
this.overwrittenPrologueLength = 0; | |
} | |
_canRelocateCode (relocationSize, constraints) { | |
const Writer = thunkWriters[Process.arch]; | |
const Relocator = thunkRelocators[Process.arch]; | |
const { quickCodeAddress } = this; | |
const writer = new Writer(quickCodeAddress); | |
const relocator = new Relocator(quickCodeAddress, writer); | |
let offset; | |
if (Process.arch === 'arm64') { | |
let availableScratchRegs = new Set(['x16', 'x17']); | |
do { | |
const nextOffset = relocator.readOne(); | |
const nextScratchRegs = new Set(availableScratchRegs); | |
const { read, written } = relocator.input.regsAccessed; | |
for (const regs of [read, written]) { | |
for (const reg of regs) { | |
let name; | |
if (reg.startsWith('w')) { | |
name = 'x' + reg.substring(1); | |
} else { | |
name = reg; | |
} | |
nextScratchRegs.delete(name); | |
} | |
} | |
if (nextScratchRegs.size === 0) { | |
break; | |
} | |
offset = nextOffset; | |
availableScratchRegs = nextScratchRegs; | |
} while (offset < relocationSize && !relocator.eoi); | |
constraints.availableScratchRegs = availableScratchRegs; | |
} else { | |
do { | |
offset = relocator.readOne(); | |
} while (offset < relocationSize && !relocator.eoi); | |
} | |
return offset >= relocationSize; | |
} | |
_allocateTrampoline () { | |
if (trampolineAllocator === null) { | |
const trampolineSize = (pointerSize === 4) ? 128 : 256; | |
trampolineAllocator = makeCodeAllocator(trampolineSize); | |
} | |
const maxRedirectSize = artQuickCodeHookRedirectSize[Process.arch]; | |
let redirectSize, spec; | |
let alignment = 1; | |
const constraints = {}; | |
if (pointerSize === 4 || this._canRelocateCode(maxRedirectSize, constraints)) { | |
redirectSize = maxRedirectSize; | |
spec = {}; | |
} else { | |
let maxDistance; | |
if (Process.arch === 'x64') { | |
redirectSize = 5; | |
maxDistance = X86_JMP_MAX_DISTANCE; | |
} else if (Process.arch === 'arm64') { | |
redirectSize = 8; | |
maxDistance = ARM64_ADRP_MAX_DISTANCE; | |
alignment = 4096; | |
} | |
spec = { near: this.quickCodeAddress, maxDistance }; | |
} | |
this.redirectSize = redirectSize; | |
this.trampoline = trampolineAllocator.allocateSlice(spec, alignment); | |
return constraints; | |
} | |
_destroyTrampoline () { | |
trampolineAllocator.freeSlice(this.trampoline); | |
} | |
activate (vm) { | |
const constraints = this._allocateTrampoline(); | |
const { trampoline, quickCode, redirectSize } = this; | |
const writeTrampoline = artQuickCodeReplacementTrampolineWriters[Process.arch]; | |
const prologueLength = writeTrampoline(trampoline, quickCode, redirectSize, constraints, vm); | |
this.overwrittenPrologueLength = prologueLength; | |
this.overwrittenPrologue = Memory.dup(this.quickCodeAddress, prologueLength); | |
const writePrologue = artQuickCodePrologueWriters[Process.arch]; | |
writePrologue(quickCode, trampoline, redirectSize); | |
} | |
deactivate () { | |
const { quickCodeAddress, overwrittenPrologueLength: prologueLength } = this; | |
const Writer = thunkWriters[Process.arch]; | |
Memory.patchCode(quickCodeAddress, prologueLength, code => { | |
const writer = new Writer(code, { pc: quickCodeAddress }); | |
const { overwrittenPrologue } = this; | |
writer.putBytes(overwrittenPrologue.readByteArray(prologueLength)); | |
writer.flush(); | |
}); | |
this._destroyTrampoline(); | |
} | |
} | |
function isArtQuickEntrypoint (address) { | |
const api = getApi(); | |
const { module: m, artClassLinker } = api; | |
return address.equals(artClassLinker.quickGenericJniTrampoline) || | |
address.equals(artClassLinker.quickToInterpreterBridgeTrampoline) || | |
address.equals(artClassLinker.quickResolutionTrampoline) || | |
address.equals(artClassLinker.quickImtConflictTrampoline) || | |
(address.compare(m.base) >= 0 && address.compare(m.base.add(m.size)) < 0); | |
} | |
class ArtMethodMangler { | |
constructor (opaqueMethodId) { | |
const methodId = unwrapMethodId(opaqueMethodId); | |
this.methodId = methodId; | |
this.originalMethod = null; | |
this.hookedMethodId = methodId; | |
this.replacementMethodId = null; | |
this.interceptor = null; | |
} | |
replace (impl, isInstanceMethod, argTypes, vm, api) { | |
const { kAccCompileDontBother, artNterpEntryPoint } = api; | |
this.originalMethod = fetchArtMethod(this.methodId, vm); | |
const originalFlags = this.originalMethod.accessFlags; | |
if ((originalFlags & kAccXposedHookedMethod) !== 0 && xposedIsSupported()) { | |
const hookInfo = this.originalMethod.jniCode; | |
this.hookedMethodId = hookInfo.add(2 * pointerSize).readPointer(); | |
this.originalMethod = fetchArtMethod(this.hookedMethodId, vm); | |
} | |
const { hookedMethodId } = this; | |
const replacementMethodId = cloneArtMethod(hookedMethodId, vm); | |
this.replacementMethodId = replacementMethodId; | |
patchArtMethod(replacementMethodId, { | |
jniCode: impl, | |
accessFlags: ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0, | |
quickCode: api.artClassLinker.quickGenericJniTrampoline, | |
interpreterCode: api.artInterpreterToCompiledCodeBridge | |
}, vm); | |
// Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path | |
// in interpreter_common.h | |
let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag; | |
if ((originalFlags & kAccNative) === 0) { | |
hookedMethodRemovedFlags |= kAccSkipAccessChecks; | |
} | |
patchArtMethod(hookedMethodId, { | |
accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0 | |
}, vm); | |
const quickCode = this.originalMethod.quickCode; | |
// Replace Nterp quick entrypoints with art_quick_to_interpreter_bridge to force stepping out | |
// of ART's next-generation interpreter and use the quick stub instead. | |
if (artNterpEntryPoint !== undefined && artNterpEntryPoint !== 0 && quickCode.equals(artNterpEntryPoint)) { | |
patchArtMethod(hookedMethodId, { | |
quickCode: api.artQuickToInterpreterBridge | |
}, vm); | |
} | |
if (!isArtQuickEntrypoint(quickCode)) { | |
const interceptor = new ArtQuickCodeInterceptor(quickCode); | |
interceptor.activate(vm); | |
this.interceptor = interceptor; | |
} | |
artController.replacedMethods.set(hookedMethodId, replacementMethodId); | |
notifyArtMethodHooked(hookedMethodId, vm); | |
} | |
revert (vm) { | |
const { hookedMethodId, interceptor } = this; | |
patchArtMethod(hookedMethodId, this.originalMethod, vm); | |
artController.replacedMethods.delete(hookedMethodId); | |
if (interceptor !== null) { | |
interceptor.deactivate(); | |
this.interceptor = null; | |
} | |
} | |
resolveTarget (wrapper, isInstanceMethod, env, api) { | |
return this.hookedMethodId; | |
} | |
} | |
function xposedIsSupported () { | |
return getAndroidApiLevel() < 28; | |
} | |
function fetchArtMethod (methodId, vm) { | |
const artMethodSpec = getArtMethodSpec(vm); | |
const artMethodOffset = artMethodSpec.offset; | |
return (['jniCode', 'accessFlags', 'quickCode', 'interpreterCode'] | |
.reduce((original, name) => { | |
const offset = artMethodOffset[name]; | |
if (offset === undefined) { | |
return original; | |
} | |
const address = methodId.add(offset); | |
const read = (name === 'accessFlags') ? readU32 : readPointer; | |
original[name] = read.call(address); | |
return original; | |
}, {})); | |
} | |
function patchArtMethod (methodId, patches, vm) { | |
const artMethodSpec = getArtMethodSpec(vm); | |
const artMethodOffset = artMethodSpec.offset; | |
Object.keys(patches).forEach(name => { | |
const offset = artMethodOffset[name]; | |
if (offset === undefined) { | |
return; | |
} | |
const address = methodId.add(offset); | |
const write = (name === 'accessFlags') ? writeU32 : writePointer; | |
write.call(address, patches[name]); | |
}); | |
} | |
class DalvikMethodMangler { | |
constructor (methodId) { | |
this.methodId = methodId; | |
this.originalMethod = null; | |
} | |
replace (impl, isInstanceMethod, argTypes, vm, api) { | |
const { methodId } = this; | |
this.originalMethod = Memory.dup(methodId, DVM_METHOD_SIZE); | |
let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0); | |
if (isInstanceMethod) { | |
argsSize++; | |
} | |
/* | |
* make method native (with kAccNative) | |
* insSize and registersSize are set to arguments size | |
*/ | |
const accessFlags = (methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).readU32() | kAccNative) >>> 0; | |
const registersSize = argsSize; | |
const outsSize = 0; | |
const insSize = argsSize; | |
methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS).writeU32(accessFlags); | |
methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE).writeU16(registersSize); | |
methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE).writeU16(outsSize); | |
methodId.add(DVM_METHOD_OFFSET_INS_SIZE).writeU16(insSize); | |
methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO).writeU32(computeDalvikJniArgInfo(methodId)); | |
api.dvmUseJNIBridge(methodId, impl); | |
} | |
revert (vm) { | |
Memory.copy(this.methodId, this.originalMethod, DVM_METHOD_SIZE); | |
} | |
resolveTarget (wrapper, isInstanceMethod, env, api) { | |
const thread = env.handle.add(DVM_JNI_ENV_OFFSET_SELF).readPointer(); | |
let objectPtr; | |
if (isInstanceMethod) { | |
objectPtr = api.dvmDecodeIndirectRef(thread, wrapper.$h); | |
} else { | |
const h = wrapper.$borrowClassHandle(env); | |
objectPtr = api.dvmDecodeIndirectRef(thread, h.value); | |
h.unref(env); | |
} | |
let classObject; | |
if (isInstanceMethod) { | |
classObject = objectPtr.add(DVM_OBJECT_OFFSET_CLAZZ).readPointer(); | |
} else { | |
classObject = objectPtr; | |
} | |
const classKey = classObject.toString(16); | |
let entry = patchedClasses.get(classKey); | |
if (entry === undefined) { | |
const vtablePtr = classObject.add(DVM_CLASS_OBJECT_OFFSET_VTABLE); | |
const vtableCountPtr = classObject.add(DVM_CLASS_OBJECT_OFFSET_VTABLE_COUNT); | |
const vtable = vtablePtr.readPointer(); | |
const vtableCount = vtableCountPtr.readS32(); | |
const vtableSize = vtableCount * pointerSize; | |
const shadowVtable = Memory.alloc(2 * vtableSize); | |
Memory.copy(shadowVtable, vtable, vtableSize); | |
vtablePtr.writePointer(shadowVtable); | |
entry = { | |
classObject, | |
vtablePtr, | |
vtableCountPtr, | |
vtable, | |
vtableCount, | |
shadowVtable, | |
shadowVtableCount: vtableCount, | |
targetMethods: new Map() | |
}; | |
patchedClasses.set(classKey, entry); | |
} | |
const methodKey = this.methodId.toString(16); | |
let targetMethod = entry.targetMethods.get(methodKey); | |
if (targetMethod === undefined) { | |
targetMethod = Memory.dup(this.originalMethod, DVM_METHOD_SIZE); | |
const methodIndex = entry.shadowVtableCount++; | |
entry.shadowVtable.add(methodIndex * pointerSize).writePointer(targetMethod); | |
targetMethod.add(DVM_METHOD_OFFSET_METHOD_INDEX).writeU16(methodIndex); | |
entry.vtableCountPtr.writeS32(entry.shadowVtableCount); | |
entry.targetMethods.set(methodKey, targetMethod); | |
} | |
return targetMethod; | |
} | |
} | |
function computeDalvikJniArgInfo (methodId) { | |
if (Process.arch !== 'ia32') { | |
return DALVIK_JNI_NO_ARG_INFO; | |
} | |
// For the x86 ABI, valid hints should always be generated. | |
const shorty = methodId.add(DVM_METHOD_OFFSET_SHORTY).readPointer().readCString(); | |
if (shorty === null || shorty.length === 0 || shorty.length > 0xffff) { | |
return DALVIK_JNI_NO_ARG_INFO; | |
} | |
let returnType; | |
switch (shorty[0]) { | |
case 'V': | |
returnType = DALVIK_JNI_RETURN_VOID; | |
break; | |
case 'F': | |
returnType = DALVIK_JNI_RETURN_FLOAT; | |
break; | |
case 'D': | |
returnType = DALVIK_JNI_RETURN_DOUBLE; | |
break; | |
case 'J': | |
returnType = DALVIK_JNI_RETURN_S8; | |
break; | |
case 'Z': | |
case 'B': | |
returnType = DALVIK_JNI_RETURN_S1; | |
break; | |
case 'C': | |
returnType = DALVIK_JNI_RETURN_U2; | |
break; | |
case 'S': | |
returnType = DALVIK_JNI_RETURN_S2; | |
break; | |
default: | |
returnType = DALVIK_JNI_RETURN_S4; | |
break; | |
} | |
let hints = 0; | |
for (let i = shorty.length - 1; i > 0; i--) { | |
const ch = shorty[i]; | |
hints += (ch === 'D' || ch === 'J') ? 2 : 1; | |
} | |
return (returnType << DALVIK_JNI_RETURN_SHIFT) | hints; | |
} | |
function cloneArtMethod (method, vm) { | |
const api = getApi(); | |
if (getAndroidApiLevel() < 23) { | |
const thread = api['art::Thread::CurrentFromGdb'](); | |
return api['art::mirror::Object::Clone'](method, thread); | |
} | |
return Memory.dup(method, getArtMethodSpec(vm).size); | |
} | |
function deoptimizeMethod (vm, env, method) { | |
requestDeoptimization(vm, env, kSelectiveDeoptimization, method); | |
} | |
function deoptimizeEverything (vm, env) { | |
requestDeoptimization(vm, env, kFullDeoptimization); | |
} | |
function deoptimizeBootImage (vm, env) { | |
const api = getApi(); | |
if (getAndroidApiLevel() < 26) { | |
throw new Error('This API is only available on Android >= 8.0'); | |
} | |
withRunnableArtThread(vm, env, thread => { | |
api['art::Runtime::DeoptimizeBootImage'](api.artRuntime); | |
}); | |
} | |
function requestDeoptimization (vm, env, kind, method) { | |
const api = getApi(); | |
if (getAndroidApiLevel() < 24) { | |
throw new Error('This API is only available on Android >= 7.0'); | |
} | |
withRunnableArtThread(vm, env, thread => { | |
if (getAndroidApiLevel() < 30) { | |
if (!api.isJdwpStarted()) { | |
const session = startJdwp(api); | |
jdwpSessions.push(session); | |
} | |
if (!api.isDebuggerActive()) { | |
api['art::Dbg::GoActive'](); | |
} | |
const request = Memory.alloc(8 + pointerSize); | |
request.writeU32(kind); | |
switch (kind) { | |
case kFullDeoptimization: | |
break; | |
case kSelectiveDeoptimization: | |
request.add(8).writePointer(method); | |
break; | |
default: | |
throw new Error('Unsupported deoptimization kind'); | |
} | |
api['art::Dbg::RequestDeoptimization'](request); | |
api['art::Dbg::ManageDeoptimization'](); | |
} else { | |
const instrumentation = api.artInstrumentation; | |
if (instrumentation === null) { | |
throw new Error('Unable to find Instrumentation class in ART; please file a bug'); | |
} | |
const enableDeopt = api['art::Instrumentation::EnableDeoptimization']; | |
if (enableDeopt !== undefined) { | |
const deoptimizationEnabled = !!instrumentation.add(getArtInstrumentationSpec().offset.deoptimizationEnabled).readU8(); | |
if (!deoptimizationEnabled) { | |
enableDeopt(instrumentation); | |
} | |
} | |
switch (kind) { | |
case kFullDeoptimization: | |
api['art::Instrumentation::DeoptimizeEverything'](instrumentation, Memory.allocUtf8String('frida')); | |
break; | |
case kSelectiveDeoptimization: | |
api['art::Instrumentation::Deoptimize'](instrumentation, method); | |
break; | |
default: | |
throw new Error('Unsupported deoptimization kind'); | |
} | |
} | |
}); | |
} | |
class JdwpSession { | |
constructor () { | |
/* | |
* We partially stub out the ADB JDWP transport to ensure we always | |
* succeed in starting JDWP. Failure will crash the process. | |
*/ | |
const acceptImpl = Module.getExportByName('libart.so', '_ZN3art4JDWP12JdwpAdbState6AcceptEv'); | |
const receiveClientFdImpl = Module.getExportByName('libart.so', '_ZN3art4JDWP12JdwpAdbState15ReceiveClientFdEv'); | |
const controlPair = makeSocketPair(); | |
const clientPair = makeSocketPair(); | |
this._controlFd = controlPair[0]; | |
this._clientFd = clientPair[0]; | |
let acceptListener = null; | |
acceptListener = Interceptor.attach(acceptImpl, function (args) { | |
const state = args[0]; | |
const controlSockPtr = Memory.scanSync(state.add(8252), 256, '00 ff ff ff ff 00')[0].address.add(1); | |
/* | |
* This will make JdwpAdbState::Accept() skip the control socket() and connect(), | |
* and skip right to calling ReceiveClientFd(), replaced below. | |
*/ | |
controlSockPtr.writeS32(controlPair[1]); | |
acceptListener.detach(); | |
}); | |
Interceptor.replace(receiveClientFdImpl, new NativeCallback(function (state) { | |
Interceptor.revert(receiveClientFdImpl); | |
return clientPair[1]; | |
}, 'int', ['pointer'])); | |
Interceptor.flush(); | |
this._handshakeRequest = this._performHandshake(); | |
} | |
async _performHandshake () { | |
const input = new UnixInputStream(this._clientFd, { autoClose: false }); | |
const output = new UnixOutputStream(this._clientFd, { autoClose: false }); | |
const handshakePacket = [0x4a, 0x44, 0x57, 0x50, 0x2d, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65]; | |
try { | |
await output.writeAll(handshakePacket); | |
await input.readAll(handshakePacket.length); | |
} catch (e) { | |
} | |
} | |
} | |
function startJdwp (api) { | |
const session = new JdwpSession(); | |
api['art::Dbg::SetJdwpAllowed'](1); | |
const options = makeJdwpOptions(); | |
api['art::Dbg::ConfigureJdwp'](options); | |
const startDebugger = api['art::InternalDebuggerControlCallback::StartDebugger']; | |
if (startDebugger !== undefined) { | |
startDebugger(NULL); | |
} else { | |
api['art::Dbg::StartJdwp'](); | |
} | |
return session; | |
} | |
function makeJdwpOptions () { | |
const kJdwpTransportAndroidAdb = getAndroidApiLevel() < 28 ? 2 : 3; | |
const kJdwpPortFirstAvailable = 0; | |
const transport = kJdwpTransportAndroidAdb; | |
const server = true; | |
const suspend = false; | |
const port = kJdwpPortFirstAvailable; | |
const size = 8 + STD_STRING_SIZE + 2; | |
const result = Memory.alloc(size); | |
result | |
.writeU32(transport).add(4) | |
.writeU8(server ? 1 : 0).add(1) | |
.writeU8(suspend ? 1 : 0).add(1) | |
.add(STD_STRING_SIZE) // We leave `host` zeroed, i.e. empty string | |
.writeU16(port); | |
return result; | |
} | |
function makeSocketPair () { | |
if (socketpair === null) { | |
socketpair = new NativeFunction( | |
Module.getExportByName('libc.so', 'socketpair'), | |
'int', | |
['int', 'int', 'int', 'pointer']); | |
} | |
const buf = Memory.alloc(8); | |
if (socketpair(AF_UNIX, SOCK_STREAM, 0, buf) === -1) { | |
throw new Error('Unable to create socketpair for JDWP'); | |
} | |
return [ | |
buf.readS32(), | |
buf.add(4).readS32() | |
]; | |
} | |
function makeAddGlobalRefFallbackForAndroid5 (api) { | |
const offset = getArtVMSpec().offset; | |
const lock = api.vm.add(offset.globalsLock); | |
const table = api.vm.add(offset.globals); | |
const add = api['art::IndirectReferenceTable::Add']; | |
const acquire = api['art::ReaderWriterMutex::ExclusiveLock']; | |
const release = api['art::ReaderWriterMutex::ExclusiveUnlock']; | |
const IRT_FIRST_SEGMENT = 0; | |
return function (vm, thread, obj) { | |
acquire(lock, thread); | |
try { | |
return add(table, IRT_FIRST_SEGMENT, obj); | |
} finally { | |
release(lock, thread); | |
} | |
}; | |
} | |
function makeDecodeGlobalFallback (api) { | |
/* | |
* Fallback for art::JavaVMExt::DecodeGlobal, which is | |
* unavailable in Android versions <= 5 and >= 15. | |
*/ | |
const decode = api['art::Thread::DecodeJObject']; | |
if (decode === undefined) { | |
throw new Error('art::Thread::DecodeJObject is not available; please file a bug'); | |
} | |
return function (vm, thread, ref) { | |
return decode(thread, ref); | |
}; | |
} | |
/* | |
* In order to call internal ART APIs we need to transition our native thread's | |
* art::Thread to the proper state. The ScopedObjectAccess (SOA) helper that ART | |
* uses internally is what we would like to use to accomplish this goal. | |
* | |
* There is however a challenge. The SOA implementation is fully inlined, so | |
* we cannot just allocate a chunk of memory and call its constructor and | |
* destructor to get the desired setup and teardown. | |
* | |
* We could however precompile such code using a C++ compiler, but considering | |
* how many versions of ART we would need to compile it for, multiplied by the | |
* number of supported architectures, we really don't want to go there. | |
* | |
* Reimplementing it in JavaScript is not desirable either, as we would need | |
* to keep track of even more internals prone to change as ART evolves. | |
* | |
* So our least terrible option is to find a really simple C++ method in ART | |
* that sets up a SOA object, performs as few and distinct operations as | |
* possible, and then returns. If we clone that implementation we can swap | |
* out the few/distinct operations with our own. | |
* | |
* We can accomplish this by using Frida's relocator API, and detecting the | |
* few/distinct operations happening between setup and teardown of the scope. | |
* We skip those when making our copy and instead put a call to a NativeCallback | |
* there. Our NativeCallback is thus able to call internal ART APIs safely. | |
* | |
* The ExceptionClear() implementation that's part of the JNIEnv's vtable is | |
* a perfect fit, as all it does is clear one field of the art::Thread. | |
* (Except on older versions where it also clears a bit more... but still | |
* pretty simple.) | |
* | |
* However, checked JNI might be enabled, making ExceptionClear() a bit more | |
* complex, and essentially a wrapper around the unchecked version. | |
* | |
* One last thing to note is that we also look up the address of FatalError(), | |
* as ExceptionClear() typically ends with a __stack_chk_fail() noreturn call | |
* that's followed by the next JNIEnv vtable method, FatalError(). We don't want | |
* to recompile its code as well, so we try to detect it. There might however be | |
* padding between the two functions, which we need to ignore. Ideally we would | |
* know that the call is to __stack_chk_fail(), so we can stop at that point, | |
* but detecting that isn't trivial. | |
*/ | |
const threadStateTransitionRecompilers = { | |
ia32: recompileExceptionClearForX86, | |
x64: recompileExceptionClearForX86, | |
arm: recompileExceptionClearForArm, | |
arm64: recompileExceptionClearForArm64 | |
}; | |
function makeArtThreadStateTransitionImpl (vm, env, callback) { | |
const envVtable = env.handle.readPointer(); | |
let exceptionClearImpl = envVtable.add(ENV_VTABLE_OFFSET_EXCEPTION_CLEAR).readPointer(); | |
let nextFuncImpl = envVtable.add(ENV_VTABLE_OFFSET_FATAL_ERROR).readPointer(); | |
// I think if we can find the JNI_FatalError function symbol and its address matches nextFuncImpl, then it should be fine. | |
let checkFatalError = Module.enumerateSymbolsSync('libart.so').filter(m => m.name.indexOf('art3JNI') >= 0 && | |
m.name.indexOf('FatalError') >=0 && | |
m.address.toString() === nextFuncImpl.toString())[0]; | |
if (Process.arch === 'arm64' && checkFatalError === undefined) { | |
let JNI_FatalError_Called_string_found_addr; | |
let JNI_FatalError_Called_string = '4A 4E 49 20 46 61 74 61 6C 45 72 72 6F 72 20 63 61 6C'; | |
const rodata_seciton = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.rodata')[0]; | |
for (const match of Memory.scanSync(rodata_seciton.address, rodata_seciton.size, JNI_FatalError_Called_string)) { | |
if (match) { | |
JNI_FatalError_Called_string_found_addr = match.address.toString(); | |
break; | |
} | |
} | |
let adrp, add; | |
let adrp_add_pattern = '?1 ?? FF ?0 21 ?? ?? 91'; | |
let adrp_add_in_JNI_false_FatalError_func; | |
let adrp_add_in_JNI_true_FatalError_func; | |
let JNI_true_FatalError_func; | |
let JNI_ExceptionClear_func; | |
const text_section = Module.enumerateSectionsSync('libart.so').filter(s => s.name == '.text')[0]; | |
for (const match of Memory.scanSync(text_section.address, text_section.size, adrp_add_pattern)) { | |
let disasm = Instruction.parse(match.address); | |
if (disasm.mnemonic === "adrp") { | |
adrp = disasm.operands.find(op => op.type === 'imm')?.value; | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic !== "add") { | |
disasm = Instruction.parse(disasm.next); | |
} | |
add = disasm.operands.find(op => op.type === 'imm')?.value; | |
if (adrp !== undefined && add !== undefined && ptr(adrp).add(add).toString() === JNI_FatalError_Called_string_found_addr.toString()) { | |
if (adrp_add_in_JNI_false_FatalError_func === undefined) { | |
adrp_add_in_JNI_false_FatalError_func = match.address; | |
continue; | |
} | |
if (adrp_add_in_JNI_true_FatalError_func === undefined) { | |
adrp_add_in_JNI_true_FatalError_func = match.address; | |
} | |
for (let off = 0;; off += 4) { | |
disasm = Instruction.parse(adrp_add_in_JNI_true_FatalError_func.sub(off)); | |
if (disasm.mnemonic === "sub") { | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic === "stp") { | |
JNI_true_FatalError_func = disasm.address.sub(0x4); | |
break; | |
} | |
} | |
} | |
if (JNI_true_FatalError_func !== undefined) { | |
for (let off = 0;; off += 4) { | |
disasm = Instruction.parse(JNI_true_FatalError_func.sub(0x4).sub(off)); | |
if (disasm.mnemonic === "sub") { | |
disasm = Instruction.parse(disasm.next); | |
if (disasm.mnemonic === "stp") { | |
JNI_ExceptionClear_func = disasm.address.sub(0x4); | |
break; | |
} | |
} | |
} | |
} | |
break; | |
} | |
} | |
} | |
if (JNI_true_FatalError_func !== undefined && JNI_ExceptionClear_func !== undefined) { | |
exceptionClearImpl = JNI_ExceptionClear_func; | |
nextFuncImpl = JNI_true_FatalError_func; | |
} | |
} | |
const recompile = threadStateTransitionRecompilers[Process.arch]; | |
if (recompile === undefined) { | |
throw new Error('Not yet implemented for ' + Process.arch); | |
} | |
let perform = null; | |
const threadOffsets = getArtThreadSpec(vm).offset; | |
const exceptionOffset = threadOffsets.exception; | |
const neuteredOffsets = new Set(); | |
const isReportedOffset = threadOffsets.isExceptionReportedToInstrumentation; | |
if (isReportedOffset !== null) { | |
neuteredOffsets.add(isReportedOffset); | |
} | |
const throwLocationStartOffset = threadOffsets.throwLocation; | |
if (throwLocationStartOffset !== null) { | |
neuteredOffsets.add(throwLocationStartOffset); | |
neuteredOffsets.add(throwLocationStartOffset + pointerSize); | |
neuteredOffsets.add(throwLocationStartOffset + (2 * pointerSize)); | |
} | |
const codeSize = 65536; | |
const code = Memory.alloc(codeSize); | |
Memory.patchCode(code, codeSize, buffer => { | |
perform = recompile(buffer, code, exceptionClearImpl, nextFuncImpl, exceptionOffset, neuteredOffsets, callback); | |
}); | |
perform._code = code; | |
perform._callback = callback; | |
return perform; | |
} | |
function recompileExceptionClearForX86 (buffer, pc, exceptionClearImpl, nextFuncImpl, exceptionOffset, neuteredOffsets, callback) { | |
const blocks = {}; | |
const branchTargets = new Set(); | |
const pending = [exceptionClearImpl]; | |
while (pending.length > 0) { | |
let current = pending.shift(); | |
const alreadyCovered = Object.values(blocks).some(({ begin, end }) => current.compare(begin) >= 0 && current.compare(end) < 0); | |
if (alreadyCovered) { | |
continue; | |
} | |
const blockAddressKey = current.toString(); | |
let block = { | |
begin: current | |
}; | |
let lastInsn = null; | |
let reachedEndOfBlock = false; | |
do { | |
if (current.equals(nextFuncImpl)) { | |
reachedEndOfBlock = true; | |
break; | |
} | |
const insn = Instruction.parse(current); | |
lastInsn = insn; | |
const existingBlock = blocks[insn.address.toString()]; | |
if (existingBlock !== undefined) { | |
delete blocks[existingBlock.begin.toString()]; | |
blocks[blockAddressKey] = existingBlock; | |
existingBlock.begin = block.begin; | |
block = null; | |
break; | |
} | |
let branchTarget = null; | |
switch (insn.mnemonic) { | |
case 'jmp': | |
branchTarget = ptr(insn.operands[0].value); | |
reachedEndOfBlock = true; | |
break; | |
case 'je': | |
case 'jg': | |
case 'jle': | |
case 'jne': | |
case 'js': | |
branchTarget = ptr(insn.operands[0].value); | |
break; | |
case 'ret': | |
reachedEndOfBlock = true; | |
break; | |
} | |
if (branchTarget !== null) { | |
branchTargets.add(branchTarget.toString()); | |
pending.push(branchTarget); | |
pending.sort((a, b) => a.compare(b)); | |
} | |
current = insn.next; | |
} while (!reachedEndOfBlock); | |
if (block !== null) { | |
block.end = lastInsn.address.add(lastInsn.size); | |
blocks[blockAddressKey] = block; | |
} | |
} | |
const blocksOrdered = Object.keys(blocks).map(key => blocks[key]); | |
blocksOrdered.sort((a, b) => a.begin.compare(b.begin)); | |
const entryBlock = blocks[exceptionClearImpl.toString()]; | |
blocksOrdered.splice(blocksOrdered.indexOf(entryBlock), 1); | |
blocksOrdered.unshift(entryBlock); | |
const writer = new X86Writer(buffer, { pc }); | |
let foundCore = false; | |
let threadReg = null; | |
blocksOrdered.forEach(block => { | |
const size = block.end.sub(block.begin).toInt32(); | |
const relocator = new X86Relocator(block.begin, writer); | |
let offset; | |
while ((offset = relocator.readOne()) !== 0) { | |
const insn = relocator.input; | |
const { mnemonic } = insn; | |
const insnAddressId = insn.address.toString(); | |
if (branchTargets.has(insnAddressId)) { | |
writer.putLabel(insnAddressId); | |
} | |
let keep = true; | |
switch (mnemonic) { | |
case 'jmp': | |
writer.putJmpNearLabel(branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'je': | |
case 'jg': | |
case 'jle': | |
case 'jne': | |
case 'js': | |
writer.putJccNearLabel(mnemonic, branchLabelFromOperand(insn.operands[0]), 'no-hint'); | |
keep = false; | |
break; | |
/* | |
* JNI::ExceptionClear(), when checked JNI is off. | |
*/ | |
case 'mov': { | |
const [dst, src] = insn.operands; | |
if (dst.type === 'mem' && src.type === 'imm') { | |
const dstValue = dst.value; | |
const dstOffset = dstValue.disp; | |
if (dstOffset === exceptionOffset && src.value.valueOf() === 0) { | |
threadReg = dstValue.base; | |
writer.putPushfx(); | |
writer.putPushax(); | |
writer.putMovRegReg('xbp', 'xsp'); | |
if (pointerSize === 4) { | |
writer.putAndRegU32('esp', 0xfffffff0); | |
} else { | |
const scratchReg = (threadReg !== 'rdi') ? 'rdi' : 'rsi'; | |
writer.putMovRegU64(scratchReg, uint64('0xfffffffffffffff0')); | |
writer.putAndRegReg('rsp', scratchReg); | |
} | |
writer.putCallAddressWithAlignedArguments(callback, [threadReg]); | |
writer.putMovRegReg('xsp', 'xbp'); | |
writer.putPopax(); | |
writer.putPopfx(); | |
foundCore = true; | |
keep = false; | |
} else if (neuteredOffsets.has(dstOffset) && dstValue.base === threadReg) { | |
keep = false; | |
} | |
} | |
break; | |
} | |
/* | |
* CheckJNI::ExceptionClear, when checked JNI is on. Wrapper that calls JNI::ExceptionClear(). | |
*/ | |
case 'call': { | |
const target = insn.operands[0]; | |
if (target.type === 'mem' && target.value.disp === ENV_VTABLE_OFFSET_EXCEPTION_CLEAR) { | |
/* | |
* Get art::Thread * from JNIEnv * | |
*/ | |
if (pointerSize === 4) { | |
writer.putPopReg('eax'); | |
writer.putMovRegRegOffsetPtr('eax', 'eax', 4); | |
writer.putPushReg('eax'); | |
} else { | |
writer.putMovRegRegOffsetPtr('rdi', 'rdi', 8); | |
} | |
writer.putCallAddressWithArguments(callback, []); | |
foundCore = true; | |
keep = false; | |
} | |
break; | |
} | |
} | |
if (keep) { | |
relocator.writeAll(); | |
} else { | |
relocator.skipOne(); | |
} | |
if (offset === size) { | |
break; | |
} | |
} | |
relocator.dispose(); | |
}); | |
writer.dispose(); | |
if (!foundCore) { | |
throwThreadStateTransitionParseError(); | |
} | |
return new NativeFunction(pc, 'void', ['pointer'], nativeFunctionOptions); | |
} | |
function recompileExceptionClearForArm (buffer, pc, exceptionClearImpl, nextFuncImpl, exceptionOffset, neuteredOffsets, callback) { | |
const blocks = {}; | |
const branchTargets = new Set(); | |
const thumbBitRemovalMask = ptr(1).not(); | |
const pending = [exceptionClearImpl]; | |
while (pending.length > 0) { | |
let current = pending.shift(); | |
const alreadyCovered = Object.values(blocks).some(({ begin, end }) => current.compare(begin) >= 0 && current.compare(end) < 0); | |
if (alreadyCovered) { | |
continue; | |
} | |
const begin = current.and(thumbBitRemovalMask); | |
const blockId = begin.toString(); | |
const thumbBit = current.and(1); | |
let block = { | |
begin | |
}; | |
let lastInsn = null; | |
let reachedEndOfBlock = false; | |
let ifThenBlockRemaining = 0; | |
do { | |
if (current.equals(nextFuncImpl)) { | |
reachedEndOfBlock = true; | |
break; | |
} | |
const insn = Instruction.parse(current); | |
const { mnemonic } = insn; | |
lastInsn = insn; | |
const currentAddress = current.and(thumbBitRemovalMask); | |
const insnId = currentAddress.toString(); | |
const existingBlock = blocks[insnId]; | |
if (existingBlock !== undefined) { | |
delete blocks[existingBlock.begin.toString()]; | |
blocks[blockId] = existingBlock; | |
existingBlock.begin = block.begin; | |
block = null; | |
break; | |
} | |
const isOutsideIfThenBlock = ifThenBlockRemaining === 0; | |
let branchTarget = null; | |
switch (mnemonic) { | |
case 'b': | |
branchTarget = ptr(insn.operands[0].value); | |
reachedEndOfBlock = isOutsideIfThenBlock; | |
break; | |
case 'beq.w': | |
case 'beq': | |
case 'bne': | |
case 'bgt': | |
branchTarget = ptr(insn.operands[0].value); | |
break; | |
case 'cbz': | |
case 'cbnz': | |
branchTarget = ptr(insn.operands[1].value); | |
break; | |
case 'pop.w': | |
if (isOutsideIfThenBlock) { | |
reachedEndOfBlock = insn.operands.filter(op => op.value === 'pc').length === 1; | |
} | |
break; | |
} | |
switch (mnemonic) { | |
case 'it': | |
ifThenBlockRemaining = 1; | |
break; | |
case 'itt': | |
ifThenBlockRemaining = 2; | |
break; | |
case 'ittt': | |
ifThenBlockRemaining = 3; | |
break; | |
case 'itttt': | |
ifThenBlockRemaining = 4; | |
break; | |
default: | |
if (ifThenBlockRemaining > 0) { | |
ifThenBlockRemaining--; | |
} | |
break; | |
} | |
if (branchTarget !== null) { | |
branchTargets.add(branchTarget.toString()); | |
pending.push(branchTarget.or(thumbBit)); | |
pending.sort((a, b) => a.compare(b)); | |
} | |
current = insn.next; | |
} while (!reachedEndOfBlock); | |
if (block !== null) { | |
block.end = lastInsn.address.add(lastInsn.size); | |
blocks[blockId] = block; | |
} | |
} | |
const blocksOrdered = Object.keys(blocks).map(key => blocks[key]); | |
blocksOrdered.sort((a, b) => a.begin.compare(b.begin)); | |
const entryBlock = blocks[exceptionClearImpl.and(thumbBitRemovalMask).toString()]; | |
blocksOrdered.splice(blocksOrdered.indexOf(entryBlock), 1); | |
blocksOrdered.unshift(entryBlock); | |
const writer = new ThumbWriter(buffer, { pc }); | |
let foundCore = false; | |
let threadReg = null; | |
let realImplReg = null; | |
blocksOrdered.forEach(block => { | |
const relocator = new ThumbRelocator(block.begin, writer); | |
let address = block.begin; | |
const end = block.end; | |
let size = 0; | |
do { | |
const offset = relocator.readOne(); | |
if (offset === 0) { | |
throw new Error('Unexpected end of block'); | |
} | |
const insn = relocator.input; | |
address = insn.address; | |
size = insn.size; | |
const { mnemonic } = insn; | |
const insnAddressId = address.toString(); | |
if (branchTargets.has(insnAddressId)) { | |
writer.putLabel(insnAddressId); | |
} | |
let keep = true; | |
switch (mnemonic) { | |
case 'b': | |
writer.putBLabel(branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'beq.w': | |
writer.putBCondLabelWide('eq', branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'beq': | |
case 'bne': | |
case 'bgt': | |
writer.putBCondLabelWide(mnemonic.substr(1), branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'cbz': { | |
const ops = insn.operands; | |
writer.putCbzRegLabel(ops[0].value, branchLabelFromOperand(ops[1])); | |
keep = false; | |
break; | |
} | |
case 'cbnz': { | |
const ops = insn.operands; | |
writer.putCbnzRegLabel(ops[0].value, branchLabelFromOperand(ops[1])); | |
keep = false; | |
break; | |
} | |
/* | |
* JNI::ExceptionClear(), when checked JNI is off. | |
*/ | |
case 'str': | |
case 'str.w': { | |
const dstValue = insn.operands[1].value; | |
const dstOffset = dstValue.disp; | |
if (dstOffset === exceptionOffset) { | |
threadReg = dstValue.base; | |
const nzcvqReg = (threadReg !== 'r4') ? 'r4' : 'r5'; | |
const clobberedRegs = ['r0', 'r1', 'r2', 'r3', nzcvqReg, 'r9', 'r12', 'lr']; | |
writer.putPushRegs(clobberedRegs); | |
writer.putMrsRegReg(nzcvqReg, 'apsr-nzcvq'); | |
writer.putCallAddressWithArguments(callback, [threadReg]); | |
writer.putMsrRegReg('apsr-nzcvq', nzcvqReg); | |
writer.putPopRegs(clobberedRegs); | |
foundCore = true; | |
keep = false; | |
} else if (neuteredOffsets.has(dstOffset) && dstValue.base === threadReg) { | |
keep = false; | |
} | |
break; | |
} | |
/* | |
* CheckJNI::ExceptionClear, when checked JNI is on. Wrapper that calls JNI::ExceptionClear(). | |
*/ | |
case 'ldr': { | |
const [dstOp, srcOp] = insn.operands; | |
if (srcOp.type === 'mem') { | |
const src = srcOp.value; | |
if (src.base[0] === 'r' && src.disp === ENV_VTABLE_OFFSET_EXCEPTION_CLEAR) { | |
realImplReg = dstOp.value; | |
} | |
} | |
break; | |
} | |
case 'blx': | |
if (insn.operands[0].value === realImplReg) { | |
writer.putLdrRegRegOffset('r0', 'r0', 4); // Get art::Thread * from JNIEnv * | |
writer.putCallAddressWithArguments(callback, ['r0']); | |
foundCore = true; | |
realImplReg = null; | |
keep = false; | |
} | |
break; | |
} | |
if (keep) { | |
relocator.writeAll(); | |
} else { | |
relocator.skipOne(); | |
} | |
} while (!address.add(size).equals(end)); | |
relocator.dispose(); | |
}); | |
writer.dispose(); | |
if (!foundCore) { | |
throwThreadStateTransitionParseError(); | |
} | |
return new NativeFunction(pc.or(1), 'void', ['pointer'], nativeFunctionOptions); | |
} | |
function recompileExceptionClearForArm64 (buffer, pc, exceptionClearImpl, nextFuncImpl, exceptionOffset, neuteredOffsets, callback) { | |
const blocks = {}; | |
const branchTargets = new Set(); | |
const pending = [exceptionClearImpl]; | |
while (pending.length > 0) { | |
let current = pending.shift(); | |
const alreadyCovered = Object.values(blocks).some(({ begin, end }) => current.compare(begin) >= 0 && current.compare(end) < 0); | |
if (alreadyCovered) { | |
continue; | |
} | |
const blockAddressKey = current.toString(); | |
let block = { | |
begin: current | |
}; | |
let lastInsn = null; | |
let reachedEndOfBlock = false; | |
do { | |
if (current.equals(nextFuncImpl)) { | |
reachedEndOfBlock = true; | |
break; | |
} | |
let insn; | |
try { | |
insn = Instruction.parse(current); | |
} catch (e) { | |
if (current.readU32() === 0x00000000) { | |
reachedEndOfBlock = true; | |
break; | |
} else { | |
throw e; | |
} | |
} | |
lastInsn = insn; | |
const existingBlock = blocks[insn.address.toString()]; | |
if (existingBlock !== undefined) { | |
delete blocks[existingBlock.begin.toString()]; | |
blocks[blockAddressKey] = existingBlock; | |
existingBlock.begin = block.begin; | |
block = null; | |
break; | |
} | |
let branchTarget = null; | |
switch (insn.mnemonic) { | |
case 'b': | |
branchTarget = ptr(insn.operands[0].value); | |
reachedEndOfBlock = true; | |
break; | |
case 'b.eq': | |
case 'b.ne': | |
case 'b.le': | |
case 'b.gt': | |
branchTarget = ptr(insn.operands[0].value); | |
break; | |
case 'cbz': | |
case 'cbnz': | |
branchTarget = ptr(insn.operands[1].value); | |
break; | |
case 'tbz': | |
case 'tbnz': | |
branchTarget = ptr(insn.operands[2].value); | |
break; | |
case 'ret': | |
reachedEndOfBlock = true; | |
break; | |
} | |
if (branchTarget !== null) { | |
branchTargets.add(branchTarget.toString()); | |
pending.push(branchTarget); | |
pending.sort((a, b) => a.compare(b)); | |
} | |
current = insn.next; | |
} while (!reachedEndOfBlock); | |
if (block !== null) { | |
block.end = lastInsn.address.add(lastInsn.size); | |
blocks[blockAddressKey] = block; | |
} | |
} | |
const blocksOrdered = Object.keys(blocks).map(key => blocks[key]); | |
blocksOrdered.sort((a, b) => a.begin.compare(b.begin)); | |
const entryBlock = blocks[exceptionClearImpl.toString()]; | |
blocksOrdered.splice(blocksOrdered.indexOf(entryBlock), 1); | |
blocksOrdered.unshift(entryBlock); | |
const writer = new Arm64Writer(buffer, { pc }); | |
writer.putBLabel('performTransition'); | |
const invokeCallback = pc.add(writer.offset); | |
writer.putPushAllXRegisters(); | |
writer.putCallAddressWithArguments(callback, ['x0']); | |
writer.putPopAllXRegisters(); | |
writer.putRet(); | |
writer.putLabel('performTransition'); | |
let foundCore = false; | |
let threadReg = null; | |
let realImplReg = null; | |
blocksOrdered.forEach(block => { | |
const size = block.end.sub(block.begin).toInt32(); | |
const relocator = new Arm64Relocator(block.begin, writer); | |
let offset; | |
while ((offset = relocator.readOne()) !== 0) { | |
const insn = relocator.input; | |
const { mnemonic } = insn; | |
const insnAddressId = insn.address.toString(); | |
if (branchTargets.has(insnAddressId)) { | |
writer.putLabel(insnAddressId); | |
} | |
let keep = true; | |
switch (mnemonic) { | |
case 'b': | |
writer.putBLabel(branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'b.eq': | |
case 'b.ne': | |
case 'b.le': | |
case 'b.gt': | |
writer.putBCondLabel(mnemonic.substr(2), branchLabelFromOperand(insn.operands[0])); | |
keep = false; | |
break; | |
case 'cbz': { | |
const ops = insn.operands; | |
writer.putCbzRegLabel(ops[0].value, branchLabelFromOperand(ops[1])); | |
keep = false; | |
break; | |
} | |
case 'cbnz': { | |
const ops = insn.operands; | |
writer.putCbnzRegLabel(ops[0].value, branchLabelFromOperand(ops[1])); | |
keep = false; | |
break; | |
} | |
case 'tbz': { | |
const ops = insn.operands; | |
writer.putTbzRegImmLabel(ops[0].value, ops[1].value.valueOf(), branchLabelFromOperand(ops[2])); | |
keep = false; | |
break; | |
} | |
case 'tbnz': { | |
const ops = insn.operands; | |
writer.putTbnzRegImmLabel(ops[0].value, ops[1].value.valueOf(), branchLabelFromOperand(ops[2])); | |
keep = false; | |
break; | |
} | |
/* | |
* JNI::ExceptionClear(), when checked JNI is off. | |
*/ | |
case 'str': { | |
const ops = insn.operands; | |
const srcReg = ops[0].value; | |
const dstValue = ops[1].value; | |
const dstOffset = dstValue.disp; | |
if (srcReg === 'xzr' && dstOffset === exceptionOffset) { | |
threadReg = dstValue.base; | |
writer.putPushRegReg('x0', 'lr'); | |
writer.putMovRegReg('x0', threadReg); | |
writer.putBlImm(invokeCallback); | |
writer.putPopRegReg('x0', 'lr'); | |
foundCore = true; | |
keep = false; | |
} else if (neuteredOffsets.has(dstOffset) && dstValue.base === threadReg) { | |
keep = false; | |
} | |
break; | |
} | |
/* | |
* CheckJNI::ExceptionClear, when checked JNI is on. Wrapper that calls JNI::ExceptionClear(). | |
*/ | |
case 'ldr': { | |
const ops = insn.operands; | |
const src = ops[1].value; | |
if (src.base[0] === 'x' && src.disp === ENV_VTABLE_OFFSET_EXCEPTION_CLEAR) { | |
realImplReg = ops[0].value; | |
} | |
break; | |
} | |
case 'blr': | |
if (insn.operands[0].value === realImplReg) { | |
writer.putLdrRegRegOffset('x0', 'x0', 8); // Get art::Thread * from JNIEnv * | |
writer.putCallAddressWithArguments(callback, ['x0']); | |
foundCore = true; | |
realImplReg = null; | |
keep = false; | |
} | |
break; | |
} | |
if (keep) { | |
relocator.writeAll(); | |
} else { | |
relocator.skipOne(); | |
} | |
if (offset === size) { | |
break; | |
} | |
} | |
relocator.dispose(); | |
}); | |
writer.dispose(); | |
if (!foundCore) { | |
throwThreadStateTransitionParseError(); | |
} | |
return new NativeFunction(pc, 'void', ['pointer'], nativeFunctionOptions); | |
} | |
function throwThreadStateTransitionParseError () { | |
throw new Error('Unable to parse ART internals; please file a bug'); | |
} | |
function fixupArtQuickDeliverExceptionBug (api) { | |
const prettyMethod = api['art::ArtMethod::PrettyMethod']; | |
if (prettyMethod === undefined) { | |
return; | |
} | |
/* | |
* There is a bug in art::Thread::QuickDeliverException() where it assumes | |
* there is a Java stack frame present on the art::Thread's stack. This is | |
* not the case if a native thread calls a throwing method like FindClass(). | |
* | |
* We work around this bug here by detecting when method->PrettyMethod() | |
* happens with method == nullptr. | |
*/ | |
Interceptor.attach(prettyMethod.impl, artController.hooks.ArtMethod.prettyMethod); | |
Interceptor.flush(); | |
} | |
function branchLabelFromOperand (op) { | |
return ptr(op.value).toString(); | |
} | |
function makeCxxMethodWrapperReturningPointerByValueGeneric (address, argTypes) { | |
return new NativeFunction(address, 'pointer', argTypes, nativeFunctionOptions); | |
} | |
function makeCxxMethodWrapperReturningPointerByValueInFirstArg (address, argTypes) { | |
const impl = new NativeFunction(address, 'void', ['pointer'].concat(argTypes), nativeFunctionOptions); | |
return function () { | |
const resultPtr = Memory.alloc(pointerSize); | |
impl(resultPtr, ...arguments); | |
return resultPtr.readPointer(); | |
}; | |
} | |
function makeCxxMethodWrapperReturningStdStringByValue (impl, argTypes) { | |
const { arch } = Process; | |
switch (arch) { | |
case 'ia32': | |
case 'arm64': { | |
let thunk; | |
if (arch === 'ia32') { | |
thunk = makeThunk(64, writer => { | |
const argCount = 1 + argTypes.length; | |
const argvSize = argCount * 4; | |
writer.putSubRegImm('esp', argvSize); | |
for (let i = 0; i !== argCount; i++) { | |
const offset = i * 4; | |
writer.putMovRegRegOffsetPtr('eax', 'esp', argvSize + 4 + offset); | |
writer.putMovRegOffsetPtrReg('esp', offset, 'eax'); | |
} | |
writer.putCallAddress(impl); | |
writer.putAddRegImm('esp', argvSize - 4); | |
writer.putRet(); | |
}); | |
} else { | |
thunk = makeThunk(32, writer => { | |
writer.putMovRegReg('x8', 'x0'); | |
argTypes.forEach((t, i) => { | |
writer.putMovRegReg('x' + i, 'x' + (i + 1)); | |
}); | |
writer.putLdrRegAddress('x7', impl); | |
writer.putBrReg('x7'); | |
}); | |
} | |
const invokeThunk = new NativeFunction(thunk, 'void', ['pointer'].concat(argTypes), nativeFunctionOptions); | |
const wrapper = function (...args) { | |
invokeThunk(...args); | |
}; | |
wrapper.handle = thunk; | |
wrapper.impl = impl; | |
return wrapper; | |
} | |
default: { | |
const result = new NativeFunction(impl, 'void', ['pointer'].concat(argTypes), nativeFunctionOptions); | |
result.impl = impl; | |
return result; | |
} | |
} | |
} | |
class StdString { | |
constructor () { | |
this.handle = Memory.alloc(STD_STRING_SIZE); | |
} | |
dispose () { | |
const [data, isTiny] = this._getData(); | |
if (!isTiny) { | |
getApi().$delete(data); | |
} | |
} | |
disposeToString () { | |
const result = this.toString(); | |
this.dispose(); | |
return result; | |
} | |
toString () { | |
const [data] = this._getData(); | |
return data.readUtf8String(); | |
} | |
_getData () { | |
const str = this.handle; | |
const isTiny = (str.readU8() & 1) === 0; | |
const data = isTiny ? str.add(1) : str.add(2 * pointerSize).readPointer(); | |
return [data, isTiny]; | |
} | |
} | |
class StdVector { | |
$delete () { | |
this.dispose(); | |
getApi().$delete(this); | |
} | |
constructor (storage, elementSize) { | |
this.handle = storage; | |
this._begin = storage; | |
this._end = storage.add(pointerSize); | |
this._storage = storage.add(2 * pointerSize); | |
this._elementSize = elementSize; | |
} | |
init () { | |
this.begin = NULL; | |
this.end = NULL; | |
this.storage = NULL; | |
} | |
dispose () { | |
getApi().$delete(this.begin); | |
} | |
get begin () { | |
return this._begin.readPointer(); | |
} | |
set begin (value) { | |
this._begin.writePointer(value); | |
} | |
get end () { | |
return this._end.readPointer(); | |
} | |
set end (value) { | |
this._end.writePointer(value); | |
} | |
get storage () { | |
return this._storage.readPointer(); | |
} | |
set storage (value) { | |
this._storage.writePointer(value); | |
} | |
get size () { | |
return this.end.sub(this.begin).toInt32() / this._elementSize; | |
} | |
} | |
class HandleVector extends StdVector { | |
static $new () { | |
const vector = new HandleVector(getApi().$new(STD_VECTOR_SIZE)); | |
vector.init(); | |
return vector; | |
} | |
constructor (storage) { | |
super(storage, pointerSize); | |
} | |
get handles () { | |
const result = []; | |
let cur = this.begin; | |
const end = this.end; | |
while (!cur.equals(end)) { | |
result.push(cur.readPointer()); | |
cur = cur.add(pointerSize); | |
} | |
return result; | |
} | |
} | |
const BHS_OFFSET_LINK = 0; | |
const BHS_OFFSET_NUM_REFS = pointerSize; | |
const BHS_SIZE = BHS_OFFSET_NUM_REFS + 4; | |
const kNumReferencesVariableSized = -1; | |
class BaseHandleScope { | |
$delete () { | |
this.dispose(); | |
getApi().$delete(this); | |
} | |
constructor (storage) { | |
this.handle = storage; | |
this._link = storage.add(BHS_OFFSET_LINK); | |
this._numberOfReferences = storage.add(BHS_OFFSET_NUM_REFS); | |
} | |
init (link, numberOfReferences) { | |
this.link = link; | |
this.numberOfReferences = numberOfReferences; | |
} | |
dispose () { | |
} | |
get link () { | |
return new BaseHandleScope(this._link.readPointer()); | |
} | |
set link (value) { | |
this._link.writePointer(value); | |
} | |
get numberOfReferences () { | |
return this._numberOfReferences.readS32(); | |
} | |
set numberOfReferences (value) { | |
this._numberOfReferences.writeS32(value); | |
} | |
} | |
const VSHS_OFFSET_SELF = alignPointerOffset(BHS_SIZE); | |
const VSHS_OFFSET_CURRENT_SCOPE = VSHS_OFFSET_SELF + pointerSize; | |
const VSHS_SIZE = VSHS_OFFSET_CURRENT_SCOPE + pointerSize; | |
class VariableSizedHandleScope extends BaseHandleScope { | |
static $new (thread, vm) { | |
const scope = new VariableSizedHandleScope(getApi().$new(VSHS_SIZE)); | |
scope.init(thread, vm); | |
return scope; | |
} | |
constructor (storage) { | |
super(storage); | |
this._self = storage.add(VSHS_OFFSET_SELF); | |
this._currentScope = storage.add(VSHS_OFFSET_CURRENT_SCOPE); | |
const kLocalScopeSize = 64; | |
const kSizeOfReferencesPerScope = kLocalScopeSize - pointerSize - 4 - 4; | |
const kNumReferencesPerScope = kSizeOfReferencesPerScope / 4; | |
this._scopeLayout = FixedSizeHandleScope.layoutForCapacity(kNumReferencesPerScope); | |
this._topHandleScopePtr = null; | |
} | |
init (thread, vm) { | |
const topHandleScopePtr = thread.add(getArtThreadSpec(vm).offset.topHandleScope); | |
this._topHandleScopePtr = topHandleScopePtr; | |
super.init(topHandleScopePtr.readPointer(), kNumReferencesVariableSized); | |
this.self = thread; | |
this.currentScope = FixedSizeHandleScope.$new(this._scopeLayout); | |
topHandleScopePtr.writePointer(this); | |
} | |
dispose () { | |
this._topHandleScopePtr.writePointer(this.link); | |
let scope; | |
while ((scope = this.currentScope) !== null) { | |
const next = scope.link; | |
scope.$delete(); | |
this.currentScope = next; | |
} | |
} | |
get self () { | |
return this._self.readPointer(); | |
} | |
set self (value) { | |
this._self.writePointer(value); | |
} | |
get currentScope () { | |
const storage = this._currentScope.readPointer(); | |
if (storage.isNull()) { | |
return null; | |
} | |
return new FixedSizeHandleScope(storage, this._scopeLayout); | |
} | |
set currentScope (value) { | |
this._currentScope.writePointer(value); | |
} | |
newHandle (object) { | |
return this.currentScope.newHandle(object); | |
} | |
} | |
class FixedSizeHandleScope extends BaseHandleScope { | |
static $new (layout) { | |
const scope = new FixedSizeHandleScope(getApi().$new(layout.size), layout); | |
scope.init(); | |
return scope; | |
} | |
constructor (storage, layout) { | |
super(storage); | |
const { offset } = layout; | |
this._refsStorage = storage.add(offset.refsStorage); | |
this._pos = storage.add(offset.pos); | |
this._layout = layout; | |
} | |
init () { | |
super.init(NULL, this._layout.numberOfReferences); | |
this.pos = 0; | |
} | |
get pos () { | |
return this._pos.readU32(); | |
} | |
set pos (value) { | |
this._pos.writeU32(value); | |
} | |
newHandle (object) { | |
const pos = this.pos; | |
const handle = this._refsStorage.add(pos * 4); | |
handle.writeS32(object.toInt32()); | |
this.pos = pos + 1; | |
return handle; | |
} | |
static layoutForCapacity (numRefs) { | |
const refsStorage = BHS_SIZE; | |
const pos = refsStorage + (numRefs * 4); | |
return { | |
size: pos + 4, | |
numberOfReferences: numRefs, | |
offset: { | |
refsStorage, | |
pos | |
} | |
}; | |
} | |
} | |
const objectVisitorPredicateFactories = { | |
arm: function (needle, onMatch) { | |
const size = Process.pageSize; | |
const predicate = Memory.alloc(size); | |
Memory.protect(predicate, size, 'rwx'); | |
const onMatchCallback = new NativeCallback(onMatch, 'void', ['pointer']); | |
predicate._onMatchCallback = onMatchCallback; | |
const instructions = [ | |
0x6801, // ldr r1, [r0] | |
0x4a03, // ldr r2, =needle | |
0x4291, // cmp r1, r2 | |
0xd101, // bne mismatch | |
0x4b02, // ldr r3, =onMatch | |
0x4718, // bx r3 | |
0x4770, // bx lr | |
0xbf00 // nop | |
]; | |
const needleOffset = instructions.length * 2; | |
const onMatchOffset = needleOffset + 4; | |
const codeSize = onMatchOffset + 4; | |
Memory.patchCode(predicate, codeSize, function (address) { | |
instructions.forEach((instruction, index) => { | |
address.add(index * 2).writeU16(instruction); | |
}); | |
address.add(needleOffset).writeS32(needle); | |
address.add(onMatchOffset).writePointer(onMatchCallback); | |
}); | |
return predicate.or(1); | |
}, | |
arm64: function (needle, onMatch) { | |
const size = Process.pageSize; | |
const predicate = Memory.alloc(size); | |
Memory.protect(predicate, size, 'rwx'); | |
const onMatchCallback = new NativeCallback(onMatch, 'void', ['pointer']); | |
predicate._onMatchCallback = onMatchCallback; | |
const instructions = [ | |
0xb9400001, // ldr w1, [x0] | |
0x180000c2, // ldr w2, =needle | |
0x6b02003f, // cmp w1, w2 | |
0x54000061, // b.ne mismatch | |
0x58000083, // ldr x3, =onMatch | |
0xd61f0060, // br x3 | |
0xd65f03c0 // ret | |
]; | |
const needleOffset = instructions.length * 4; | |
const onMatchOffset = needleOffset + 4; | |
const codeSize = onMatchOffset + 8; | |
Memory.patchCode(predicate, codeSize, function (address) { | |
instructions.forEach((instruction, index) => { | |
address.add(index * 4).writeU32(instruction); | |
}); | |
address.add(needleOffset).writeS32(needle); | |
address.add(onMatchOffset).writePointer(onMatchCallback); | |
}); | |
return predicate; | |
} | |
}; | |
function makeObjectVisitorPredicate (needle, onMatch) { | |
const factory = objectVisitorPredicateFactories[Process.arch] || makeGenericObjectVisitorPredicate; | |
return factory(needle, onMatch); | |
} | |
function makeGenericObjectVisitorPredicate (needle, onMatch) { | |
return new NativeCallback(object => { | |
const klass = object.readS32(); | |
if (klass === needle) { | |
onMatch(object); | |
} | |
}, 'void', ['pointer', 'pointer']); | |
} | |
function alignPointerOffset (offset) { | |
const remainder = offset % pointerSize; | |
if (remainder !== 0) { | |
return offset + pointerSize - remainder; | |
} | |
return offset; | |
} | |
module.exports = { | |
getApi, | |
ensureClassInitialized, | |
getAndroidVersion, | |
getAndroidApiLevel, | |
getArtClassSpec, | |
getArtMethodSpec, | |
getArtFieldSpec, | |
getArtThreadSpec, | |
getArtThreadFromEnv, | |
withRunnableArtThread, | |
withAllArtThreadsSuspended, | |
makeArtClassVisitor, | |
makeArtClassLoaderVisitor, | |
ArtStackVisitor, | |
ArtMethod, | |
makeMethodMangler, | |
translateMethod, | |
backtrace, | |
revertGlobalPatches, | |
deoptimizeEverything, | |
deoptimizeBootImage, | |
deoptimizeMethod, | |
HandleVector, | |
VariableSizedHandleScope, | |
makeObjectVisitorPredicate, | |
DVM_JNI_ENV_OFFSET_SELF | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment