查看原文
其他

java函数转Native化的一些实践

一颗金柚子 看雪学院 2021-03-06
本文为看雪论坛优秀文章
看雪论坛作者ID:一颗金柚子



目的:完成MainActivity中的onCreate和replaceClassloader两个java函数的jni等价实现。


首先声明一下AS工程写的是dex动态加载,然后是为了去加载另一个1.dex里的TestActivity类。


接着使用GDA去看要加载的1.dex中查看:



如果Native转化成功,则会在logcat窗口中看到i am from TestActivity.onCreate


开始JNI转化:



0x01 使用JNI实现onCreate函数的等价转换


1. 首先是注释掉onCreate,并添加声明。


protected native void onCreate(Bundle SavedInstanceState);


2. 其中,setContentView(R.layout.activity_main);这个语句中会有R类,这是自动生成类,我们需要在GDA中观看它的反编译结果。


发现其中都是些静态Static int 属性,一会儿用Field的时候选择StaticInt 和Static。


3. 转Jni层实现


onCreate的前两行都没什么大问题,一般的反射思路。


//第一行:super.onCreate(savedInstanceState);因此首先需要获取superclassjclass AppCompatActivity_cls1 = env->FindClass("androidx/appcompat/app/AppCompatActivity");//已经知道类名jclass MainActivity_cls1 = env->FindClass("com/kanxue/loaddex/MainActivity");//调用这个函数protected void onCreate(@Nullable Bundle savedInstanceState)jmethodID superclassOnCreate_mid = env->GetMethodID(AppCompatActivity_cls1,"onCreate","(Landroid/os/Bundle;)V");env->CallNonvirtualVoidMethod(thiz,AppCompatActivity_cls1,superclassOnCreate_mid,Bundle);
//第二行:setContentView(R.layout.activity_main);jmethodID setContentView_mid = env->GetMethodID(MainActivity_cls1,"setContentView","(I)V");jclass R_layout_cls = env->FindClass("com/kanxue/loaddex/R$layout");jfieldID activity_main_fid=env->GetStaticFieldID(R_layout_cls,"activity_main","I");//这里对应R$layout那个图看出来的jint activity_main_value = env->GetStaticIntField(R_layout_cls,activity_main_fid);env->CallVoidMethod(thiz,setContentView_mid,activity_main_value);


第三行,我们跟进并观察getApplication的函数实现:


//第三行:mApplication = this.getApplication();//其中,public final Application getApplication()jmethodID getApplication_mid=env->GetMethodID(MainActivity_cls1,"getApplication","()Landroid/app/Application;");jobject mApplication = env->CallObjectMethod(thiz,getApplication_mid);


第四行,原理和第三行一致:


//第四行:appContext = this.getApplicationContext();jmethodID getApplicationContext_mid=env->GetMethodID(MainActivity_cls1,"getApplicationContext","()Landroid/content/Context;");jobject appContext = env->CallObjectMethod(thiz,getApplicationContext_mid);


第五行,调用了copAssetAndWrite,因此需要注意私有属性和Static类型:


//copyAssetAndWrite函数private static boolean copyAssetAndWrite(String fileName) {        try {            File cacheDir = appContext.getCacheDir();            if (!cacheDir.exists()) {                cacheDir.mkdirs();            }            File outFile = new File(cacheDir, fileName);            if (!outFile.exists()) {                boolean res = outFile.createNewFile();                if (!res) {                    return false;                }            } else {               if (outFile.length() > 10) {                 return true;                }            }            InputStream is = appContext.getAssets().open(fileName);            FileOutputStream fos = new FileOutputStream(outFile);            byte[] buffer = new byte[1024];            int byteCount;            while ((byteCount = is.read(buffer)) != -1) {                fos.write(buffer, 0, byteCount);            }            fos.flush();            is.close();            fos.close();            return true;        } catch (IOException e) {
            e.printStackTrace();        }
        return false;    }正在上传...



//第五行:copyAssetAndWrite("1.dex");//其中,private static boolean copyAssetAndWrite(String fileName)jmethodID copyAssetAndWrite_mid=env->GetStaticMethodID(MainActivity_cls1,"copyAssetAndWrite","(Ljava/lang/String;)Z");jstring arg1 = env->NewStringUTF("1.dex");env->CallStaticBooleanMethod(MainActivity_cls1,copyAssetAndWrite_mid,arg1);


java.io.File android.content.Context.getCacheDir()' on a null object reference


这里遇到了一个问题,也就是当我在JNI里实现appContext的赋值后怎么让JAVA层的copyAssetAndWrite函数知道我改了值呢?


解决方法:给copyAssetAndWrite新增一个参数,就是Context类型的appContext,即我们自己来传给它:


//第五行:copyAssetAndWrite("1.dex");//其中,private static boolean copyAssetAndWrite(String fileName)jmethodID copyAssetAndWrite_mid=env->GetStaticMethodID(MainActivity_cls1,"copyAssetAndWrite","(Landroid/content/Context;Ljava/lang/String;)Z");jstring arg1 = env->NewStringUTF("1.dex");env->CallStaticBooleanMethod(MainActivity_cls1,copyAssetAndWrite_mid,appContext,arg1);


相对应地,MainActivity里把copyAssetAndWrite改成:


private static boolean copyAssetAndWrite(Context appContext, String fileName)


第六行,可以看到File是个构造函数,因此在最后调用的时候要用NewObject,而且寻找methodId的时候要用<init>。


因此File类,签名为:()Ljava/io/File;



//第六行:File dataFile = new File(this.getCacheDir(), "1.dex");//其中,public File getCacheDir() public File(File parent, String child)jclass File_cls = env->FindClass("java/io/File");jmethodID getCacheDir_mid=env->GetMethodID(AppActivity_cls1,"getCacheDir","()Ljava/io/File;");jobject Cachedir = env->CallObjectMethod(thiz,getCacheDir_mid);//对于构造函数File,函数名不是File而是<init>jmethodID File_mid=env->GetMethodID(File_cls,"<init>","(Ljava/io/File;Ljava/lang/String;)V");jobject dataFile = env->NewObject(File_cls,File_mid,Cachedir,arg1);


第七行:


//第七行:startTestActivityFirstMethod(this, dataFile.getAbsolutePath());//其中,public void startTestActivityFirstMethod(Context context, String dexfilepath)jmethodID startTestActivityFirstMethod_mid=env->GetMethodID(MainActivity_cls1,"startTestActivityFirstMethod", "(Landroid/content/Context;Ljava/lang/String;)V");//其中,public String getAbsolutePath()jmethodID getAbsolutePath_mid=env->GetMethodID(MainActivity_cls1,"getAbsolutePath","()Ljava/lang/String;");jstring arg2 = static_cast<jstring >( env->CallObjectMethod(thiz,getAbsolutePath_mid));env->CallVoidMethod(MainActivity_cls1,startTestActivityFirstMethod_mid,thiz,arg2);


报错:can't call void com.kanxue.loaddex.MainActivity.startTestActivityFirstMethod(android.content.Context, java.lang.String) on instance of java.lang.Class<com.kanxue.loaddex.MainActivity>


经研究发现,是CallNonvirtualVoid和Callvoid之间的区别问题。


CallNonVirtualVoidMethod是调用含子类重写父类函数的函数MainActivity中的onCreate是重写,不是继承。


CallVoidMethod 调用的是当前对象自己的函数。


因此正确应该是写成:


//第七行:startTestActivityFirstMethod(this, dataFile.getAbsolutePath());//其中,内部有:public String getAbsolutePath()jmethodID getAbsolutePath_mid=env->GetMethodID(File_cls,"getAbsolutePath","()Ljava/lang/String;");jstring arg2 = static_cast<jstring>( env->CallObjectMethod(dataFile,getAbsolutePath_mid));//其中,外部有:public void startTestActivityFirstMethod(Context context, String dexfilepath)jmethodID startTestActivityFirstMethod_mid=env->GetMethodID(MainActivity_cls1,"startTestActivityFirstMethod", "(Landroid/content/Context;Ljava/lang/String;)V");const char* content_ptr=env->GetStringUTFChars(arg2, nullptr);__android_log_print(4,"kanxue->homework", "arg2->%s",content_ptr );//打印动态加载1.dex的路径env->CallNonvirtualVoidMethod(thiz,MainActivity_cls1,startTestActivityFirstMethod_mid,thiz,arg2);


关于onCreate的Native化。


成功打印出log:



0x02 接下来使用JNI实现replaceClassloader


//replaceClassLoader的代码public void replaceClassloader(ClassLoader classloader){    Class<?> ActivityThreadClazz = classloader.loadClass("android.app.ActivityThread");    Method currentActivityThreadMethod = ActivityThreadClazz.getDeclaredMethod("currentActivityThread");    currentActivityThreadMethod.setAccessible(true);    Object activityThreadObj = currentActivityThreadMethod.invoke(null);    //final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();    Field mPackagesField = ActivityThreadClazz.getDeclaredField("mPackages");    mPackagesField.setAccessible(true);    ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj);    WeakReference wr = (WeakReference) mPackagesObj.get(this.getPackageName());    Object loadedApkObj = wr.get();    Class LoadedApkClazz = classloader.loadClass("android.app.LoadedApk");    Field mClassLoaderField = LoadedApkClazz.getDeclaredField("mClassLoader");    mClassLoaderField.setAccessible(true);    mClassLoaderField.set(loadedApkObj, classloader);}


这个replace函数可以有两种等价实现:


1. 一种是严格的翻译,使用java中反射相关的api;  根据前面的反射思路,我直接给出这部分代码,步骤非常的固定,“jclass->jmethodID->CallxxxxMethod”以这种方式循环:


extern "C" JNIEXPORT void JNICALLJava_com_kanxue_loaddex_MainActivity_JNI_1replaceClassloader(JNIEnv *env, jobject thiz,jobject classloader){ jclass Classloader_jclass = env->FindClass("java/lang/ClassLoader"); jmethodID loadClass_mid = env->GetMethodID(Classloader_jclass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); jstring ActivityThreadName = env->NewStringUTF("android.app.ActivityThread"); jobject ActivityThread_class = env->CallObjectMethod(classloader, loadClass_mid, ActivityThreadName); jclass Class_jclass = env->FindClass("java/lang/Class"); jclass Method_jclass = env->FindClass("java/lang/reflect/Method"); jmethodID setAccessible_mid = env->GetMethodID(Method_jclass, "setAccessible", "(Z)V"); jmethodID getDeclaredMethod_mid = env->GetMethodID(Class_jclass, "getDeclaredMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;"); jmethodID getDeclaredField_mid = env->GetMethodID(Class_jclass, "getDeclaredField", "(Ljava/lang/String;)Ljava/lang/reflect/Field;"); jstring currentActivityThread_jstring = env->NewStringUTF("currentActivityThread"); //下面的第二个参数不能省略 jobject currentActivityThreadMethod = env->CallObjectMethod(ActivityThread_class, getDeclaredMethod_mid, currentActivityThread_jstring, nullptr); env->CallVoidMethod(currentActivityThreadMethod, setAccessible_mid, true); //Object invoke(Object obj, Object... args) jmethodID invoke_mid = env->GetMethodID(Method_jclass, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;"); //下面的参数不能省略 jobject currentActivityThreadObj = env->CallObjectMethod(currentActivityThreadMethod, invoke_mid, nullptr, nullptr); /* Field mPackagesField = ActivityThreadClazz.getDeclaredField("mPackages"); mPackagesField.setAccessible(true); ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj); */ jstring mPackagesField_jstring = env->NewStringUTF("mPackages"); jobject mPackagesField = env->CallObjectMethod(ActivityThread_class, getDeclaredField_mid, mPackagesField_jstring); jclass Field_jclass = env->FindClass("java/lang/reflect/Field"); env->CallVoidMethod(mPackagesField, setAccessible_mid, true); jmethodID get_Method = env->GetMethodID(Field_jclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); jobject mPackagesObj = env->CallObjectMethod(mPackagesField, get_Method, currentActivityThreadObj); jclass ArrayMap_jclass = env->FindClass("android/util/ArrayMap"); jmethodID ArrayMap_get_mid = env->GetMethodID(ArrayMap_jclass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); jclass Context_jclass = env->FindClass("android/content/Context"); jmethodID getPackageName_mid = env->GetMethodID(Context_jclass, "getPackageName", "()Ljava/lang/String;"); jobject packagename = env->CallObjectMethod(thiz, getPackageName_mid); /*WeakReference wr = (WeakReference) mPackagesObj.get(this.getPackageName()); Object loadedApkObj = wr.get(); Class LoadedApkClazz = classloader.loadClass("android.app.LoadedApk"); Field mClassLoaderField = LoadedApkClazz.getDeclaredField("mClassLoader"); mClassLoaderField.setAccessible(true); mClassLoaderField.set(loadedApkObj, classloader); */ jobject wr = env->CallObjectMethod(mPackagesObj, ArrayMap_get_mid, packagename); jclass WeakReference_jclass = env->FindClass("java/lang/ref/WeakReference"); jmethodID WeakReference_get_mid = env->GetMethodID(WeakReference_jclass, "get", "()Ljava/lang/Object;"); jobject loadedApkObj = env->CallObjectMethod(wr, WeakReference_get_mid); jobject LoadedApkClazz = env->CallObjectMethod(classloader, loadClass_mid, env->NewStringUTF("android.app.LoadedApk")); jobject mClassLoaderField = env->CallObjectMethod(LoadedApkClazz, getDeclaredField_mid, env->NewStringUTF("mClassLoader")); env->CallVoidMethod(mClassLoaderField, setAccessible_mid, true); jmethodID set_mid = env->GetMethodID(Field_jclass, "set", "(Ljava/lang/Object;Ljava/lang/Object;)V"); env->CallVoidMethod(mClassLoaderField, set_mid, loadedApkObj, classloader);}


2. 另一种是不用,我们其实要明白一点:在ndk开发中是没有反射的概念的!

这部分的思路是几行几行地去理解代码究竟在干了什么,明确最终要调用的函数及其参数,不用一句句地利用反射去写,而是一种“前瞻式”翻译


extern "C" JNIEXPORT void JNICALLJava_com_kanxue_loaddex_MainActivity_JNI_1replaceClassloader( JNIEnv *env, jobject thiz,jobject classloader){ //第1行,Class<?> ActivityThreadClazz = classloader.loadClass("android.app.ActivityThread"); jclass Activi_cls = env->FindClass("android/app/ActivityThread"); if (Activi_cls != nullptr){ __android_log_print(4,"kanxue->homework", "Activi_cls is not null" );} //第2行,Method currentActivityThreadMethod = ActivityThreadClazz.getDeclaredMethod("currentActivityThread"); //第3行,currentActivityThreadMethod.setAccessible(true); //第4行,Object activityThreadObj = currentActivityThreadMethod.invoke(null); 这句说明null后面无参数,且有NuLL是静态的 //其实是调用一个private static函数 jmethodID currentActivityThread_mid=env->GetStaticMethodID(Activi_cls,"currentActivityThread", "()Landroid/app/ActivityThread;"); jobject activityThreadObj = env->CallStaticObjectMethod(Activi_cls,currentActivityThread_mid); if (activityThreadObj != nullptr){ __android_log_print(4,"kanxue->homework", "activityThreadObj is not null" );} //第5行,Field mPackagesField = ActivityThreadClazz.getDeclaredField("mPackages"); //第6行,mPackagesField.setAccessible(true); //第7行,ArrayMap mPackagesObj = (ArrayMap) mPackagesField.get(activityThreadObj); //其实,这是在取private 非静态域的mPackages属性值,有实例化对象activityThreadObj jfieldID mPackages_fid = env->GetFieldID(Activi_cls,"mPackages","Landroid/util/ArrayMap;"); jobject mPackages_obj= env->GetObjectField(activityThreadObj,mPackages_fid); if (mPackages_obj != nullptr){ __android_log_print(4,"kanxue->homework", "mPackages_obj is not null" );} //第8行,WeakReference wr = (WeakReference) mPackagesObj.get(this.getPackageName()); //其中, public String getPackageName() 位于 android.content.ContextWrapper中 jclass ContextWrapper_cls = env->FindClass("android/content/ContextWrapper"); jmethodID getPackageName_mid = env->GetMethodID(ContextWrapper_cls,"getPackageName", "()Ljava/lang/String;"); jobject PackageName = (env->CallObjectMethod(thiz, getPackageName_mid)); const char* content_ptr = env->GetStringUTFChars(static_cast<jstring>(PackageName),nullptr) ; if (PackageName != nullptr){ __android_log_print(4,"kanxue->homework", "PackageName is %s, ok!",content_ptr );} //觉得这里的get是个函数 public V get(Object key) 尝试过(Ljava/lang/Object;)LV 错了 //调用类实例对象mPackages(android.util.ArrayMap类型)的非静态成员方法get()函数 jclass ArrayMap_cls = env->FindClass("android/util/ArrayMap"); jmethodID Array_get = env->GetMethodID(ArrayMap_cls, "get","(Ljava/lang/Object;)Ljava/lang/Object;"); if (Array_get != nullptr){ __android_log_print(4,"kanxue->homework", "get_mid is not null");} jobject wr = env->CallNonvirtualObjectMethod(mPackages_obj,ArrayMap_cls,Array_get,PackageName); /*jobject wr= env->GetObjectField(PackageName, reinterpret_cast<jfieldID>(mPackages_obj));*/ if (wr != nullptr){ __android_log_print(4,"kanxue->homework", "wr is not null" );} //第9行, Object loadedApkObj = wr.get(); //其中,我们首先需要获取类java.lang.ref.WeakReference的非静态成员方法get的调用id //错误写法:jobject loadedApkObj = env->GetObjectField(classloader, reinterpret_cast<jfieldID>(wr));【服了我自己,当时在想什么】 //获取LoadedApk类对象实例的弱引用(WeakReference<LoadedApk>),弱引用的位置 :java/lang/ref/WeakReference jclass myWeakReference = env->FindClass("java/lang/ref/WeakReference"); jmethodID WeakReference_get = env->GetMethodID( myWeakReference, "get", "()Ljava/lang/Object;"); jobject loadedApkObj = env->CallObjectMethod(wr,WeakReference_get); if (loadedApkObj != nullptr){ __android_log_print(4,"kanxue->homework", "loadedApkObj is not null" );} //第10行, Class LoadedApkClazz = classloader.loadClass("android.app.LoadedApk"); //第11行,Field mClassLoaderField = LoadedApkClazz.getDeclaredField("mClassLoader"); //第12行,mClassLoaderField.setAccessible(true); //第13行,mClassLoaderField.set(loadedApkObj, classloader); //其实,这是在设置private 非静态的mClassLoader的属性值 //首先我们需要获取类android.app.LoadedApk的非静态私有成员mClassLoader的fid jclass LoadedApk_cls = env->FindClass("android/app/LoadedApk"); if (LoadedApk_cls != nullptr){ __android_log_print(4,"kanxue->homework", "LoadedApk_cls is not null" );} jfieldID mClassLoader_fid = env->GetFieldID(LoadedApk_cls,"mClassLoader","Ljava/lang/ClassLoader;"); env->SetObjectField(loadedApkObj,mClassLoader_fid,classloader);}


从以上第二部分的代码可以看出,如果我把注释和log打印信息全部删除,只保留精简部分,那么代码长度是远小于第一种方法的。


成功打印:



0x03 经验之谈


我在转化时遇到了大大小小的bug,我都是一步步打印log来观察正确与否的。


NDK开发当中有很多个地方需要加深学习,我的路还有很长,与君共勉!如果有幸能帮助到一些人,那我深感欣慰。




-End -





看雪ID:一颗金柚子

https://bbs.pediy.com/user-879358.htm 

*本文由看雪论坛 一颗金柚子 原创,转载请注明来自看雪社区。

推荐文章++++

*   D-Link DIR-645路由器栈溢出漏洞分析

*   TP-Link Archer路由器LAN RCE

*   记一次基于Soket通信的app的分析

*   开源一个Linux进程内存内核管理模块源码

*   由一道CTF对10种反调试的探究


好书推荐






公众号:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



“阅读原文” 一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存