frida源码阅读之frida-java
frida-java简介
frida的JavaScript API按功能划分了许多模块,frida-java具体实现了其中的Java模块,提供了Java Runtime相关的API。我们知道JNI连接了native世界和java世界,而frida-java相当于实现了一个js世界到java世界的单向通道。利用frida-java,我们可以使用js代码实现:调用java方法、创建java对象、对java函数进行hook等操作。
frida-java源码结构如下:
- index.js: 封装了JavaScript API中Java模块的api
- lib
- android.js: 封装了一些Android虚拟机的api
- api.js
- class-factory.js: java类的一些处理函数
- env.js: js中实现了JNIEnv的一个代理
- mkdex.js: dex文件的一些处理函数
- result.js: 检测jni调用是否产生异常
- vm.js: js中实现了JavaVM的一个代理
接下来通过下面两个问题来分析frida-java源码:
- frida-java是如何连通到java世界的?
- frida-java如何实现java方法hook?
frida-java是如何连通到java世界?
总的来说frida-java通过两步实现js世界到java世界的单向通道,首先利用frida-gum提供的js接口操作native世界,然后再基于jni连通到java世界。
主要步骤如下:
- 连通native世界,获得一些虚拟机的接口,如JNI_GetCreatedJavaVMs
- 获得JavaVM
- 获得JNIEnv
- 获得java类的class引用
- 操作该java类,如创建对象,调用方法等。
连通native世界
利用frida-gum中提供的Module、Memory、NativeFunction等模块,可以实现查找、调用、hook导出函数;读写、分配内存等操作。如下面例子所示, 可以在js代码中调用native函数。
var friendlyFunctionName = new NativeFunction(friendlyFunctionPtr, 'void', ['pointer', 'pointer']);
var returnValue = Memory.alloc(sizeOfLargeObject);
friendlyFunctionName(returnValue, thisPtr);
获得JavaVM
想要使用JNI连通java世界,首先需要获取的就是JavaVM,frida-java通过调用JNI_GetCreatedJavaVMs来获取JavaVM,Dalvik虚拟机和ART虚拟机均实现了该函数。核心代码如下:
//lib/android.js
const vms = Memory.alloc(pointerSize);
const vmCount = Memory.alloc(jsizeSize);
checkJniResult('JNI_GetCreatedJavaVMs', temporaryApi.JNI_GetCreatedJavaVMs(vms, 1, vmCount));
if (Memory.readInt(vmCount) === 0) {
return null;
}
temporaryApi.vm = Memory.readPointer(vms);
此时获取的vm只是JavaVM的一个指针,在此基础上frida-java还会构造一个VM对象,该对象相当于在js层实现了一个JavaVM的代理对象,封装了一些JavaVM的方法,如getEnv,其中vm.handle中保存的是原始的JavaVM对象。
//lib/vm.js
function VM (api) {
let handle = null; //保存JavaVM指针
let attachCurrentThread = null; //封装了JavaVM.AttachCurrentThread
let detachCurrentThread = null; //封装了JavaVM.DetachCurrentThread;
let getEnv = null; //封装了JavaVM.getEnv
const attachedThreads = {};
function initialize () {
handle = api.vm;
//获取JavaVM的虚函数表
const vtable = Memory.readPointer(handle);
attachCurrentThread = new NativeFunction(Memory.readPointer(vtable.add(4 * pointerSize)), 'int32', ['pointer', 'pointer', 'pointer']);
detachCurrentThread = new NativeFunction(Memory.readPointer(vtable.add(5 * pointerSize)), 'int32', ['pointer']);
getEnv = new NativeFunction(Memory.readPointer(vtable.add(6 * pointerSize)), 'int32', ['pointer', 'pointer', 'int32']);
}
}
VM的初始化过程为首先获取JavaVM的指针(通过JNI_GetCreatedJavaVMs调用),然后读取JavaVM的虚函数表,获得JavaVM的一些重要方法,并在js层包装一层,这样就在js层实现了一个JavaVM的代理,可以通过调用VM.getEnv来实现native层的JavaVM.getEnV调用。
获得JNIEnv
获取到JavaVM后,就可以通过将当前线程与JavaVM相关联,然后得到JNIEnv对象,进行后续操作。上述工作由VM.perform完成,看下VM.perform源码:
//vm.js
this.tryGetEnv = function () {
const envBuf = Memory.alloc(pointerSize);
const result = getEnv(handle, envBuf, JNI_VERSION_1_6);
if (result !== JNI_OK) {
return null;
}
return new Env(Memory.readPointer(envBuf), this);
};
this.perform = function (fn) {
let threadId = null;
//将当前线程附加到JavaVM,获取JNIEnv对象
let env = this.tryGetEnv();
const alreadyAttached = env !== null;
if (!alreadyAttached) {
env = this.attachCurrentThread();
threadId = Process.getCurrentThreadId();
attachedThreads[threadId] = true;
}
try {
fn(); //执行fn
} finally {
if (!alreadyAttached) {
const allowedToDetach = attachedThreads[threadId];
delete attachedThreads[threadId];
if (allowedToDetach) {
this.detachCurrentThread();
}
}
}
};
和JavaVM一样,frida-java也会在js层为JNIEnv建立一个代理,具体在env.js实现
获得Java类的class引用
和JNI操作方式一样,我们在native层获得了JNIEnv后,要想操作java类,可以通过调用env->findClass来获得java类的class引用。但是这里有个问题,因为frida-java所在的线程是通过pthread_create创造的,然后通过AttachCurrentThread获取的JNIEnv,此时FindClass只会从系统的classloader开始查找,所以app自身的类是无法通过env->findClass来获取。因此需要手工的获取到加载该app的classloader。Java.perform在调用VM.perform之前会先获取加载该app的classloader,并保存到classFactory.loader。
this.perform = function (fn) {
assertJavaApiIsAvailable();
//目标进程不是app,并且classloader已经初始化
if (!isAppProcess() || classFactory.loader !== null) {
threadsInPerform++;
try {
vm.perform(fn);
} catch (e) {
setTimeout(() => { throw e; }, 0);
} finally {
threadsInPerform--;
}
} else {
//第一次调用java.perform时,会先获取加载该app的classloader
pending.push(fn);
if (pending.length === 1) {
threadsInPerform++;
try {
vm.perform(() => {
const ActivityThread = classFactory.use('android.app.ActivityThread');
const app = ActivityThread.currentApplication();
if (app !== null) {
//获取到加载该app的classloader
classFactory.loader = app.getClassLoader();
performPending(); // already initialized, continue
} else {
const m = ActivityThread.getPackageInfoNoCheck;
let initialized = false;
m.implementation = function () {
const apk = m.apply(this, arguments);
if (!initialized) {
initialized = true;
classFactory.loader = apk.getClassLoader();
performPending();
}
return apk;
};
}
});
} finally {
threadsInPerform--;
}
}
}
};
frida-java使用Java.use来获得java类的class引用,Java.use(className),返回java类的一个wrapper,在js世界里,用该wrapper来操作对应的java类。Java.use直接调用了classFactory.use,代码如下:
//lib/class-factory.js
this.use = function (className) {
let C = classes[className]; //先从缓存中查找
if (!C) {
const env = vm.getEnv(); //获取jni_env, 调用native层的JavaVm.GetEnv
if (loader !== null) { //loader已经在Java.perform中初始化了
const usedLoader = loader;
if (cachedLoaderMethod === null) {
cachedLoaderInvoke = env.vaMethod('pointer', ['pointer']);
cachedLoaderMethod = loader.loadClass.overload('java.lang.String').handle;
}
const getClassHandle = function (env) {
const classNameValue = env.newStringUtf(className);
const tid = Process.getCurrentThreadId();
ignore(tid);
try { //env.handle 指向jni层的JNIEnv, 利用jni调用classloader.loadClass(className)
return cachedLoaderInvoke(env.handle, usedLoader.$handle, cachedLoaderMethod, classNameValue);
} finally {
unignore(tid);
env.deleteLocalRef(classNameValue);
}
};
//构建对应java类的wrapper
C = ensureClass(getClassHandle, className);
}
}
借助于该wrapper,可以对java类进行操作,如调用构造函数创建对象。该wrapper的初始化过程如下:
//lib/class-factory.js
function initializeClass () {
klass.__name__ = name;
let ctor = null;
let getCtor = function (type) {};
//定义了一些每个类都公有的函数和属性
Object.defineProperty(klass.prototype, '$new', {});
Object.defineProperty(klass.prototype, '$alloc', {});
Object.defineProperty(klass.prototype, '$init', {});
klass.prototype.$dispose = dispose;
klass.prototype.$isSameObject = function (obj) {});
Object.defineProperty(klass.prototype, 'class', {});
Object.defineProperty(klass.prototype, '$className', {});
//添加该类特有的函数和属性
addMethodsAndFields();
}
借助于该wrapper的$init方法,就可以创建java对象。
frida-java如何实现java方法调用与java方法hook?
frida-java Dalvik Hook原理
frida-java采用常见的Dalvik Hook方案,将待hook的java函数修改为native函数,当调用该函数时,会执行自定义的native函数。但是和其他hook框架不同的是,使用frida时,我们hook的代码是js实现的,所以有一个基于js代码生成native函数过程。具体hook实现代码如下:
function replaceDalvikImplementation (fn) {
if (fn === null && dalvikOriginalMethod === null) {
return;
}
//保存原Method结构
if (dalvikOriginalMethod === null) {
dalvikOriginalMethod = Memory.dup(methodId, DVM_METHOD_SIZE);
dalvikTargetMethodId = Memory.dup(methodId, DVM_METHOD_SIZE);
}
if (fn !== null) {
implementation = implement(f, fn); //由js代码生成对应的native函数
let argsSize = argTypes.reduce((acc, t) => (acc + t.size), 0);
if (type === INSTANCE_METHOD) {
argsSize++;
}
/*
* make method native (with kAccNative)
* insSize and registersSize are set to arguments size
*/
const accessFlags = (Memory.readU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS)) | kAccNative) >>> 0;
const registersSize = argsSize;
const outsSize = 0;
const insSize = argsSize;
Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_ACCESS_FLAGS), accessFlags);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_REGISTERS_SIZE), registersSize);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_OUTS_SIZE), outsSize);
Memory.writeU16(methodId.add(DVM_METHOD_OFFSET_INS_SIZE), insSize);
Memory.writeU32(methodId.add(DVM_METHOD_OFFSET_JNI_ARG_INFO), computeDalvikJniArgInfo(methodId));
//利用 dvmUSeJNIBridge完成替换Method结构的insns和NativeFunc
api.dvmUseJNIBridge(methodId, implementation);
patchedMethods.add(f);
} else {
patchedMethods.delete(f);
Memory.copy(methodId, dalvikOriginalMethod, DVM_METHOD_SIZE);
implementation = null;
}
}
hook后执行的native函数就是用implement函数实现的,该函数时最终调用frida-node的new NativeCallback接口实现将js函数转换为native函数。
frida-java ART Hook原理
常见的ART Hook方法为:替换方法的入口点,即ArtMethod的entry_point_from_quick_compiledcode,并将原方法的信息备份到entry_point_fromjni。替换后的入口点,会重新准备栈和寄存器,执行hook的方法。
frida-java采用的hook方法,我在其他地方并未遇见过,其原理为:首先将方法native化,然后将ArtMethod的entry_point_fromjni替换为hook的方法,并将entry_point_from_quick_compiledcode替换为art_quick_generic_jni_trampoline。当调用被hook的方法时,首先会跳转到art_quick_generic_jni_trampoline,该函数会做一些jni调用的准备,然后跳转到ArtMethod结构的entry_point_fromjni所指向的hook方法,这样就完成了一次hook。完成art hook的源码如下:
function replaceArtImplementation (fn) {
if (fn === null && artOriginalMethodInfo === null) {
return;
}
const artMethodSpec = getArtMethodSpec(vm);
const artMethodOffset = artMethodSpec.offset; //获取ArtMethod各个字段的偏移
if (artOriginalMethodInfo === null) {
artOriginalMethodInfo = fetchMethod(methodId); //保存原方法信息
}
if (fn !== null) {
implementation = implement(f, fn);
// kAccFastNative so that the VM doesn't get suspended while executing JNI
// (so that we can modify the ArtMethod on the fly)
patchMethod(methodId, {
//替换entry_point_from_jni_为hook的方法;
'jniCode': implementation,
//native化
'accessFlags': (Memory.readU32(methodId.add(artMethodOffset.accessFlags)) | kAccNative | kAccFastNative) >>> 0,
//替换entry_point_from_quick_compiled_code_为art_quick_generic_jni_trampoline;
'quickCode': api.artQuickGenericJniTrampoline,
//entry_point_from_interpreter_;
'interpreterCode': api.artInterpreterToCompiledCodeBridge
});
patchedMethods.add(f);
} else {
patchedMethods.delete(f);
patchMethod(methodId, artOriginalMethodInfo);
implementation = null;
}
}
然后看下art_quick_generic_jni_trampoline源码,art_quick_generic_jni_trampoline主要负责jni调用的准备,包括堆栈的设置,参数的设置等。个人能力有限只能大概看看流程。
ENTRY art_quick_generic_jni_trampoline
SETUP_REFS_AND_ARGS_CALLEE_SAVE_FRAME_WITH_METHOD_IN_R0
// Save rSELF
mov r11, rSELF
// Save SP , so we can have static CFI info. r10 is saved in ref_and_args.
mov r10, sp
.cfi_def_cfa_register r10
sub sp, sp, #5120
// prepare for artQuickGenericJniTrampoline call, 转为为c调用约定
// (Thread*, SP)
// r0 r1 <= C calling convention
// rSELF r10 <= where they are
mov r0, rSELF // Thread*
mov r1, r10 // ArtMethod**
//调用c的 artQuickGenericJniTrampoline
blx artQuickGenericJniTrampoline // (Thread*, sp)
// The C call will have registered the complete save-frame on success.
// The result of the call is:
// r0: pointer to native code, 0 on error.
// r1: pointer to the bottom of the used area of the alloca, can restore stack till there.
// Check for error = 0.
cbz r0, .Lexception_in_native
// Release part of the alloca.
mov sp, r1
// Save the code pointer
mov r12, r0
// Load parameters from frame into registers.
pop {r0-r3}
主要是将调用约定转换为c的调用约定,将参数准备好,然后调用artQuickGenericJniTrampoline,该函数源码如下:
extern "C" TwoWordReturn artQuickGenericJniTrampoline(Thread* self, ArtMethod** sp)
SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
ArtMethod* called = *sp;
DCHECK(called->IsNative()) << PrettyMethod(called, true);
uint32_t shorty_len = 0;
const char* shorty = called->GetShorty(&shorty_len);
// Run the visitor and update sp.
BuildGenericJniFrameVisitor visitor(self, called->IsStatic(), shorty, shorty_len, &sp);
visitor.VisitArguments();
visitor.FinalizeHandleScope(self);
// Fix up managed-stack things in Thread.
self->SetTopOfStack(sp);
self->VerifyStack();
// Start JNI, save the cookie.
uint32_t cookie;
if (called->IsSynchronized()) {
cookie = JniMethodStartSynchronized(visitor.GetFirstHandleScopeJObject(), self);
if (self->IsExceptionPending()) {
self->PopHandleScope();
// A negative value denotes an error.
return GetTwoWordFailureValue();
}
} else {
cookie = JniMethodStart(self);
}
uint32_t* sp32 = reinterpret_cast<uint32_t*>(sp);
*(sp32 - 1) = cookie;
// Retrieve the stored native code.
//获取到ArtMethod结构的entry_point_from_jni_
void* nativeCode = called->GetEntryPointFromJni();
// There are two cases for the content of nativeCode:
// 1) Pointer to the native function.
// 2) Pointer to the trampoline for native code binding.
// In the second case, we need to execute the binding and continue with the actual native function
// pointer.
DCHECK(nativeCode != nullptr);
if (nativeCode == GetJniDlsymLookupStub()) {
#if defined(__arm__) || defined(__aarch64__)
nativeCode = artFindNativeMethod();
#else
nativeCode = artFindNativeMethod(self);
#endif
if (nativeCode == nullptr) {
DCHECK(self->IsExceptionPending()); // There should be an exception pending now.
DCHECK(self->IsExceptionPending()); // There should be an exception pending now.
// End JNI, as the assembly will move to deliver the exception.
jobject lock = called->IsSynchronized() ? visitor.GetFirstHandleScopeJObject() : nullptr;
if (shorty[0] == 'L') {
artQuickGenericJniEndJNIRef(self, cookie, nullptr, lock);
} else {
artQuickGenericJniEndJNINonRef(self, cookie, lock);
}
return GetTwoWordFailureValue();
}
// Note that the native code pointer will be automatically set by artFindNativeMethod().
}
// Return native code addr(lo) and bottom of alloca address(hi).
return GetTwoWordSuccessValue(reinterpret_cast<uintptr_t>(visitor.GetBottomOfUsedArea()),
reinterpret_cast<uintptr_t>(nativeCode));
}
总结
笔记分析的还是很粗糙的,发出来是希望可以抛砖引玉,可以在论坛看到更多frida的文章,如有错误,恳请大佬指出。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法