最近在研究插件技术时遇到一个问题,用插件技术调起应用,应用里面的摄像头无法打开,我就查看了摄像头相关的源码,发现问题出在了android.hardware.Camera类的初始化里面。具体来说,当我们初始化一个Camera类对象时,在初始化过程中调用了一个native的方法:
private void final int native_setup(Object camera_this,int cameraId, int halVersion, String packageName);
这个方法在JNI层通过Binder通信请求Server端的ICameraService去初始化摄像头。在Server端ICameraService会检查Client端传过来的包名,然后去PackageManagerService那边请求该包名对应的应用是否声明了Camera相关的权限,如果有,则打开摄像头,如果没有在界面上就会提示去设置里面打开相应的权限。我们的问题就出在这里,当我们的插件调起初始化Camera类时,Camera类获取的包名是插件的包名,而插件没有安装,所以在Server端进行权限校验时就会失败,最终导致摄像头无法打开
解决问题的思路其实这个问题我们也好解,只要我们在宿主应用里面申请Camera相关的权限,然后Hook住Camera类里面的native_setup方法在调用该方法时传入宿主的包名,这样权限校验就能通过,摄像头就能打开。我们解决问题的具体的思路如下:
对于Java里面的native方法,在JNI层肯定有一个对应的函数 我们在JNI层找到保存这个函数地址的指针,先把指针保存起来 我们自己写一个代理函数,然后替换上面的那个指针
在我们的代理函数里面对传进来的参数做修改(针对我们的这个Case只需要替换包名,把“com.baidu.browser.apps”替换成我们的宿主包名),然后再调用我们在上面保存的函数指针,传进我们修改后的参数完成相应的操作 针对上面解决问题的思路,我们的目标就很清晰了,首先并且最重要的是:我们得在JNI层找到android.hardware.Camera.native_setup的对应的函数。
JNI中如何去调用Java方法
// 这是调用Java中的static方法 jmethodID method = env->GetStaticMethodID(g_jclass, JAVA_CALLBACK__ON_KILL_PROCESS, JAVA_CALLBACK__ON_KILL_PROCESS_SIGNATURE); env->CallStaticVoidMethod(g_jclass, method, pid, sig); // 这是调用Java中的非static方法 jmethodID method = env->GetMethodID(g_jclass, JAVA_CALLBACK__ON_KILL_PROCESS, JAVA_CALLBACK__ON_KILL_PROCESS_SIGNATURE); env->CallVoidMethod(g_jclass, method, pid, sig);
从上面的代码我们看到,不管是调用静态方法还是非静态方法,我们都得找到代表这个方法的jmethodID,然后通过这个jmethodID去调用JNIEnv的相应的CallXXXXMethod; jmethodID到底是什么呢?从jni.h文件里面可以看到下面这段定义: struct _jmethodID; /* opaque structure / typedef struct _jmethodID jmethodID; /* method IDs */
其实可以看到jmethodID其实就是一个结构体指针,这个结构体叫__jmethodID是如何定义的呢?其定义如下:
struct Method { ClassObject* clazz; // 代表这个方法所属的类 u4 accessFlags; // 访问标识符, 定义是public? native?synchronized? u2 methodIndex; // index,在ClassObject中一个保存Method对象的数组的位置。 u2 registersSize; // 仅对native方法有用,保存方法的参数的个数。 u2 outsSize; // unknown, 不重要 u2 insSize; // unknown, 不重要 const char* name; // 方法名 DexProto prototype; // 我们可以理解这是一个代表该方法签名的字符串(包含返回值以及参数类型) const char* shorty; // 跟上面的意义,方法的签名,like:(ILjava/lang/String)V const u2* insns; // 方法的指令代码 int jniArgInfo; // jni参数信息,好像没啥用 DalvikBridgeFunc nativeFunc; // native函数指针,可能是真正的native函数,也可能是JNI桥接函数 bool fastJni; // unknown, 不重要 bool noRef; // unknown, 不重要 bool shouldTrace; // unknown, 不重要 const RegisterMap* registerMap; // unknown, 不重要 bool inProfile; // unknown, 不重要 };
针对上面的这些参数,我们只挑几个重要的下面可能会用到的说一下:
accessFlags: 在Dalvik虚拟机中定义了一系列的值用来指示这个方法是public的还是private的,是static的还是非static的,是native 的还是非native的,有没有final、synchronized、absttact等等关键字。 我们可以修改这个值来修改一个方法的属性,比如说下面这段代码,可以讲一个非native的方法修改成native的方法:
public static final int ACC_NATIVE = 0x0100; (method->accessFlags & ACC_NATIVE) != 0
registersSize:这个保存方法的参数个数 nativeFuc:这个最重要,它是一个函数指针,这个指针可能指向该方法对应的native函数,也可能指向一个桥接函数。简单的说就是,在Dalvik虚拟机下面,这个指向的是桥接函数;在Art虚拟机下,指向的是native函数。至于这个桥接函数,它的声明如下:
typedef void (* DalvikBridgeFunc)(const u4* args, JValue* pResult, const Method* method, struct Thread* self);
这个桥接函数的参数我解释一下: args: 存储的是调研native方法传过来的参数,对于非static方法,args[0]存储的是this对象,args[1]存储的是第一个参数值,args[2]存储的是第二个参数值,以此类推。对于static对象,args[0]存储的是第一个参数值,args[1]存储的是第二个参数值, 举个例子,比如说对于我自己定一个一个方法getSubString(String aString, int aStart, int aLength), 这是一个非static方法。 我现在调用该方法,getString("This is a sentence", 5, 6); 当方法走到我们的桥接函数中时,我们可以通过args[1]获取到字符串"This is a sentence", 通过args[2]获取到5,通过args[3]获取到6。 pResult: 存储函数的返回值 method: 存储与该桥接函数对应的method对象。 self: 抱歉,这个我也没搞明白。-_- 对于native函数,这个就简单了,看下面的代码就一目了然了:
static JNINativeMethod gMethods[] = { {"hookNativeMethod", "(Ljava/lang/Object;Ljava/lang/String;Z)Z", (void *) native_hook} }; void native_hook(JNIEnv *env, jclass clazz, jobject method, jstring pkg, jboolean isArt) env->RegisterNatives(javaClass, gMethods, 1)
上面的"hookNativeMethod"对应的是java里面的声明为native的方法, native_hook代表的是native函数,我们通过JNIEnv 的RegisterNatives将这他们关联起来。 java层调用hookNativeMethod最终回转化成native层调用native_hook函数。在Art虚拟机下,Method结构体中存储的nativeFuc指针指向的就是这个native_hook函数。 上面已经解释清楚jmethodID代表啥之后,下面我们就来着手Hook住Camara的native_setup方法:我们分步来实现Hook: 第一步: 获取native_setup方法的jmethodID值: 我们直接看代码:
private static native boolean hookNativeMethod(Object method, String packageName); public static void fixCamera() { if (!sNativeFixed) { try { Method native_setup; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { native_setup = Reflect.on(Camera.class).exactMethod("native_setup", new Class[]{Object.class, int.class, int.class, String.class}); } else { native_setup = Reflect.on(Camera.class).exactMethod("native_setup", new Class[]{Object.class, int.class, String.class}); } hookNativeMethod(native_setup, VirtualCore.getCore().getHostPkg()); } catch (NoSuchMethodException e) { e.printStackTrace(); } sNativeFixed = true; } }
这是Java层代码,我们通过反射获取到Camera类里面native_setup方法对应的Method对象(注意,这是Java里面的Method类,不是我们上面说的Method结构体);然后我们通过一个native方法hookNativeMethod将这个Method传到JNI层。 下面是JNI层的代码:
jmethodID mtd_openDexNative = env->FromReflectedMethod(javaMethod);
在JNI层,我们调用JNIEnv的FromReflectedMethod方法,传入java层传过来的代表Method对象的jobject对象,得到了native_setup在native层的jmethodID对象。 第二步:我们找到jmethodID之后,接下来要找到这个结构体里面的nativeFuc指针 正如我们上面所说,这个jmethodID其实就是上面的我们解释的Method结构体了,我们现在就是要找到这个结构体里面的nativeFunc指针,然后把这个指针改成我们自己定义的一个函数,这样就完成了Hook。那么我们怎么去找到这个nativeFuc指针呢,一种比较简单的方法就是将jmethodID强制转换成结构题Method的指针,但是我们目前没这么做,原因很简单,上面的Method结构体里面还包含了另外一些结构题,如DexProto、RegisterMap等等,这两个结构题里面可能还包含了其他的结构体,我要么得把这些结构体都拷过来,要么我就得引用定义这些结构体的头文件,这个比较麻烦。我们换用另一种方法: 我们在java类里面定义一个mark()的native方法,在JNI里面也定义一个nativeMark()函数,然后通过JNIEnv的RegisterNatives函数将他们两个关联起来。然后我们通过JNIEnv的GetMethodId函数获取到mark方法的jmethodID,jmethodID是Method结构体的指针(其实就是这个结构体的内存起始地址),我们知道这个指针之后,下面要分两种情况来分析了:
Art虚拟机下我们上面说了Art虚拟机下面结构体Method里面的nativeFunc其实指向的就是native函数,在我们这个案例中指向的就是nativeMark函数。那下面就好办了:我们用一个while循环,从jmethodID所代表的内存地址开始,4个字节4个字节的往后寻址,然后通过“*”取出所指地址中存储的值,用这个值跟nativeHook指针比较,当相等时,我们就找到了结构体Method里面的nativeFuc的偏移量。 Dalvik虚拟机下 还是上面说的,在Dalvik虚拟机下结构体Method里面的nativeFunc指向的是一个桥接函数,那么我们如何去找到保存这个桥接函数的指针呢?可以通过下面这段代码找到:
char vmSoName[15] = {0}; __system_property_get("persist.sys.dalvik.vm.lib", vmSoName); LOGD("Find the so name : %s.", strlen(vmSoName) == 0 ? "" : vmSoName); void *vmHandle = dlopen(vmSoName, 0); if (!vmHandle) { LOGE("Unable to open the %s.", vmSoName); vmHandle = RTLD_DEFAULT; } gOffset.art_work_around_app_jni_bugs = dlsym(vmHandle, "art_work_around_app_jni_bugs");
上面这段代码可以找到桥接函数的指针,同样,我们从jmethodID代表的内存地址开始,4个字节4个字节的往后寻址,然后通过“*”取出所指地址中存储的值,用这个值跟我们刚刚找到的桥接函数的指针比较,当相等时,我们就找到了结构体Method里面的nativeFuc的偏移量。 下面这段代码就是寻址和比较的代码:
void mark() { // Do nothing }; void searchJniOffset(JNIEnv *env, bool isArt) { jclass g_class = env->FindClass(JAVA_CLASS); jmethodID mtd_nativeHook = env->GetStaticMethodID(g_class, "mark", "()V"); size_t startAddress = (size_t) mtd_nativeHook; size_t targetAddress = (size_t) nativeMark; if (isArt && gOffset.art_work_around_app_jni_bugs) { targetAddress = (size_t) gOffset.art_work_around_app_jni_bugs; } int offset = 0; bool found = false; while (true) { if (*((size_t *) (startAddress + offset)) == targetAddress) { found = true; break; } offset += 4; if (offset >= 100) { LOGE("Ops: Unable to find the jni function."); break; } } if (found) { gOffset.nativeOffset = offset; if (!isArt) { gOffset.nativeOffset += (sizeof(int) + sizeof(void *)); } LOGD("Hoho, Get the offset : %d.", gOffset.nativeOffset); } }
上面我们找到nativeFunc偏移量之后,我们前面又找到了Camera类的native_setup对应的jmethodID,jmethodID加上这个偏移量就是native_setup对应的Method结构体里的nativeFuc地址,这个地址里面存储了一个C函数指针,在Dalvik虚拟机下,这个是桥接函数的指针,在Art虚拟机下这个指针就是native函数的指针。 第三步:找到C函数指针之后,把指针替换掉,换成我们定义的Hook函数的函数指针 这一步就很简单了,直接看代码:
inline void replaceImplementation(JNIEnv *env, jobject javaMethod, jboolean isArt) { size_t mtd_openDexNative = (size_t) env->FromReflectedMethod(javaMethod); int nativeFuncOffset = gOffset.nativeOffset; void **jniFuncPtr = (void **) (mtd_openDexNative + nativeFuncOffset); if (!isArt) { LOGD("The vm is dalvik"); gOffset.orig_DalvikBridgeFunc = (Bridge_DalvikBridgeFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_bridge_nativeSetUpFunc; } else { char vmSoName[4] = {0}; __system_property_get("ro.build.version.sdk", vmSoName); int sdk; sscanf(vmSoName, "%d", &sdk); LOGD("The vm is art and the sdk int is %d", sdk); if (sdk < 21) { gOffset.orig_native_openDexNativeDalvikFunc = (Native_nativeSetUpDalvikFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_nativeSetUpDalvikFunc; } else { gOffset.orig_native_openDexNativeFunc = (Native_nativeSetUpFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_nativeSetUpFunc; } } }
这里我们解释一下,我们区分了Dalvik虚拟机和Art虚拟机两种情形,Art虚拟机下直接替换的就是native函数,而Dalvik虚拟机下我们替换的是桥接函数。其实更通俗的讲就是,Art虚拟机下面我们Hook的是真正的native函数,在Dalvik虚拟机下面我们要Hook的是桥接函数。 第四步: 定义Hook函数,在第三步中会用我们定义的Hook函数替换掉原来的函数 直接看代码,分Dalvik虚拟机和Art虚拟机: // 这个函数是Art虚拟机下的hook函数
static jobject new_nativeSetUpArtFunc(JNIEnv *env, jclass jclazz, jobject weak_this, jint halVersion, jstring clientPackageName) { jstring newPkg = env->NewStringUTF(newPkgName); return gOffset.orig_native_openArtNativeDalvikFunc(env, jclazz, weak_this, halVersion, newPkg); } // 这个是Dalvik虚拟机下面的hook函数,用该函数去Hook桥接函数 static void new_bridge_nativeSetUpFunc(const void **args, void *pResult, const void *method, void *self) { JNIEnv *env = NULL; g_vm->GetEnv((void **) &env, JNI_VERSION_1_6); g_vm->AttachCurrentThread(&env, NULL); typedef char* (*GetCstrFromString)(void *); typedef void* (*GetStringFromCstr)(const char*); const char* origPkg3 = args[3] == NULL ? NULL : gOffset.GetCstrFromString((void*) args[3]); LOGD("The original package3 is: %s", origPkg3); args[3] = gOffset.GetStringFromCstr(newPkgName); gOffset.orig_DalvikBridgeFunc(args, pResult, method, self); }
至此,我们分四步就已经完成了Camera类的native_setup的Hook。总结起来就是:
找到Camera.native_setup方法在jni层的jmethodID 在jmethodID中找到nativeFuc,将这个值改成我们自己定义的Hook函数的地址 自己写Hook函数,要分Dalvik虚拟机和Art虚拟机
进阶(如果你想,你就能) 其实上面的代码可以实现所有Java方法的Hook; 对于native的方法上面的代码稍微把方法名,方法签名修改一下,实现一下对应的Hook函数就可以了。 对于非native的方法,我们可以把Method结构体中的accessFlags改一下,就可以把它变成native方法,然后将nativeFuc赋值成我们写的Hook函数,然后再改一下registersSize和insSize的值,这就是保存参数个数的地方,最后再该一下jniArgsInfo值,具体的改法可以参照git上的代码AllHookInOne 下面是我写的完整版的代码 这是Java层的代码
public class CameraFixer { private static native boolean hookNativeMethod( Object method, String packageName, boolean isArt); private static native void hook(); }
这是C代码:
camera_hook.h #ifndef VIRTUALAPP_CAMERA_HOOK_H #define VIRTUALAPP_CAMERA_HOOK_H #include #include #include #include #include #include #include #include #define TAG "HOOKCAMERA" #define JAVA_CLASS "com/baidu/freeinstall/client/ContextFixer" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) __BEGIN_DECLS JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved); JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved); __END_DECLS #endif //VIRTUALAPP_CAMERA_HOOK_H camera_hook.c #include "camera_hook.h" JavaVM *g_vm; typedef void (*Bridge_DalvikBridgeFunc)(const void **, void *, const void *, void *); typedef jobject (*Native_nativeSetUpFunc)(JNIEnv *, jclass, jobject weak_this, jint cameraId, jint halVersion, jstring clientPackageName); typedef jobject (*Native_nativeSetUpDalvikFunc)(JNIEnv *, jclass, jobject weak_this, jint halVersion, jstring clientPackageName); void mark() { // Do nothing }; static struct { bool isArt; int nativeOffset; void *art_work_around_app_jni_bugs; char *(*GetCstrFromString)(void *); void *(*GetStringFromCstr)(const char *); Bridge_DalvikBridgeFunc orig_DalvikBridgeFunc; Native_nativeSetUpFunc orig_native_openDexNativeFunc; Native_nativeSetUpDalvikFunc orig_native_openDexNativeDalvikFunc; } gOffset; char *newPkgName; static jobject new_nativeSetUpFunc(JNIEnv *env, jclass jclazz, jobject weak_this, jint cameraId, jint halVersion, jstring clientPackageName) { jstring newPkg = env->NewStringUTF(newPkgName); return gOffset.orig_native_openDexNativeFunc(env, jclazz, weak_this, cameraId, halVersion, newPkg); } static jobject new_nativeSetUpDalvikFunc(JNIEnv *env, jclass jclazz, jobject weak_this, jint halVersion, jstring clientPackageName) { jstring newPkg = env->NewStringUTF(newPkgName); return gOffset.orig_native_openDexNativeDalvikFunc(env, jclazz, weak_this, halVersion, newPkg); } static void new_bridge_nativeSetUpFunc(const void **args, void *pResult, const void *method, void *self) { JNIEnv *env = NULL; g_vm->GetEnv((void **) &env, JNI_VERSION_1_6); g_vm->AttachCurrentThread(&env, NULL); typedef char* (*GetCstrFromString)(void *); typedef void* (*GetStringFromCstr)(const char*); const char* origPkg0 = args[0] == NULL ? NULL : gOffset.GetCstrFromString((void*) args[0]); LOGD("The original package0 is: %s", origPkg0); const char* origPkg = args[2] == NULL ? NULL : gOffset.GetCstrFromString((void*) args[2]); LOGD("The original package2 is: %s", origPkg); const char* origPkg3 = args[3] == NULL ? NULL : gOffset.GetCstrFromString((void*) args[3]); LOGD("The original package3 is: %s", origPkg3); args[3] = gOffset.GetStringFromCstr(newPkgName); gOffset.orig_DalvikBridgeFunc(args, pResult, method, self); } void searchJniOffset(JNIEnv *env, bool isArt) { jclass g_class = env->FindClass(JAVA_CLASS); jmethodID mtd_nativeHook = env->GetStaticMethodID(g_class, "nativeMark", "()V"); size_t startAddress = (size_t) mtd_nativeHook; size_t targetAddress = (size_t) mark; if (isArt && gOffset.art_work_around_app_jni_bugs) { targetAddress = (size_t) gOffset.art_work_around_app_jni_bugs; } int offset = 0; bool found = false; while (true) { if (*((size_t *) (startAddress + offset)) == targetAddress) { found = true; break; } offset += 4; if (offset >= 100) { LOGE("Ops: Unable to find the jni function."); break; } } if (found) { gOffset.nativeOffset = offset; if (!isArt) { gOffset.nativeOffset += (sizeof(int) + sizeof(void *)); } LOGD("Hoho, Get the offset : %d.", gOffset.nativeOffset); } } inline void replaceImplementation(JNIEnv *env, jobject javaMethod, jboolean isArt) { size_t mtd_openDexNative = (size_t) env->FromReflectedMethod(javaMethod); int nativeFuncOffset = gOffset.nativeOffset; void **jniFuncPtr = (void **) (mtd_openDexNative + nativeFuncOffset); if (!isArt) { LOGD("The vm is dalvik"); gOffset.orig_DalvikBridgeFunc = (Bridge_DalvikBridgeFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_bridge_nativeSetUpFunc; } else { char vmSoName[4] = {0}; __system_property_get("ro.build.version.sdk", vmSoName); int sdk; sscanf(vmSoName, "%d", &sdk); LOGD("The vm is art and the sdk int is %d", sdk); if (sdk < 21) { gOffset.orig_native_openDexNativeDalvikFunc = (Native_nativeSetUpDalvikFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_nativeSetUpDalvikFunc; } else { gOffset.orig_native_openDexNativeFunc = (Native_nativeSetUpFunc) (*jniFuncPtr); *jniFuncPtr = (void *) new_nativeSetUpFunc; } } } static JNINativeMethod gMarkMethods[] = { {"nativeMark", "()V", (void *) mark} }; void native_hook(JNIEnv *env, jclass clazz, jobject method, jstring pkg, jboolean isArt) { newPkgName = (char *) env->GetStringUTFChars(pkg, NULL); g_vm->GetEnv((void **) &env, JNI_VERSION_1_6); g_vm->AttachCurrentThread(&env, NULL); jclass g_class = env->FindClass(JAVA_CLASS); if (env->RegisterNatives(g_class, gMarkMethods, 1) < 0) { return; } gOffset.isArt = isArt; char vmSoName[15] = {0}; __system_property_get("persist.sys.dalvik.vm.lib", vmSoName); LOGD("Find the so name : %s.", strlen(vmSoName) == 0 ? "" : vmSoName); void *vmHandle = dlopen(vmSoName, 0); if (!vmHandle) { LOGE("Unable to open the %s.", vmSoName); vmHandle = RTLD_DEFAULT; } if (isArt) { gOffset.art_work_around_app_jni_bugs = dlsym(vmHandle, "art_work_around_app_jni_bugs"); } else { gOffset.GetCstrFromString = (char *(*)(void *)) dlsym(vmHandle, "_Z23dvmCreateCstrFromStringPK12StringObject"); if (!gOffset.GetCstrFromString) { gOffset.GetCstrFromString = (char *(*)(void *)) dlsym(vmHandle, "dvmCreateCstrFromString"); } gOffset.GetStringFromCstr = (void *(*)(const char *)) dlsym(vmHandle, "_Z23dvmCreateStringFromCstrPKc"); if (!gOffset.GetStringFromCstr) { gOffset.GetStringFromCstr = (void *(*)(const char *)) dlsym(vmHandle, "dvmCreateStringFromCstr"); } } searchJniOffset(env, isArt); replaceImplementation(env, method, isArt); } static JNINativeMethod gMethods[] = { { "hookNativeMethod", "(Ljava/lang/Object;Ljava/lang/String;Z)Z", (void *) native_hook } }; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { g_vm = vm; JNIEnv *env; LOGE("JNI_Onload start"); if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { LOGE("GetEnv() FAILED!!!"); return JNI_ERR; } jclass javaClass = env->FindClass(JAVA_CLASS); LOGE("we have found the class: %s", JAVA_CLASS); if (javaClass == NULL) { LOGE("unable to find class: %s", JAVA_CLASS); return JNI_ERR; } env->UnregisterNatives(javaClass); if (env->RegisterNatives(javaClass, gMethods, 1) < 0) { LOGE("register methods FAILED!!!"); return JNI_ERR; } env->DeleteLocalRef(javaClass); LOGI("JavaVM::GetEnv() SUCCESS!"); return JNI_VERSION_1_6; }
原创作者:Android开发奇技淫巧,原文链接:https://www.jianshu.com/p/052b6dd45659