SO入门 先自己测一下这个案例
抓个包
1 2 3 4 5 6 7 8 9 10 11 POST /user/api/v1/login/app/mobile HTTP/1.1 pa: MTczMjAxNzY1MzAzMyxhMTZhM2ZmOWVmYzU0M2IxYjlkMTBiMGI3NjY3NTk4MiwzZjg3MjNlMWNiMzBhYzYwM2RiYjY1ZGQ4N2QxMTA3Miw= appInfo: {"IMEI":"bf1821d5634d0ec8","appBuild":"21070602","appVersion":"6.2.0","deviceId":"bf1821d5634d0ec8","deviceName":"NX627J","osType":"android","osVersion":"9","phoneName":"NX627J","phoneSystemVersion":"9","vendor":"nubia"} User-Agent: PocketFans201807/6.2.0_21070602 (NX627J:Android 9;nubia PQ3B.190801.03191204 release-keys) Content-Type: application/json; charset=UTF-8 Content-Length: 43 Host: pocketapi.48.cn Connection: Keep-Alive Accept-Encoding: gzip {"mobile":"13112345678","pwd":"a123123123"}
1 2 3 4 5 6 感觉只有 pa 字段值得注意, MT ey 开头的一般是base64 MTczMjAxNzY1MzAzMyxhMTZhM2ZmOWVmYzU0M2IxYjlkMTBiMGI3NjY3NTk4MiwzZjg3MjNlMWNiMzBhYzYwM2RiYjY1ZGQ4N2QxMTA3Miw= base64解码 1732017653033,a16a3ff9efc543b1b9d10b0b76675982,3f8723e1cb30ac603dbb65dd87d11072, 看到有三节数据,第一节感觉像是时间戳
那么直觉告诉我剩下两个就是账号和密码了
1 2 3 a16a3ff9efc543b1b9d10b0b76675982 3f8723e1cb30ac603dbb65dd87d11072 32位密文感觉和MD5有关,但是不完全是,可能加了什么东西
hook了一下加密方法,出现了SHA1,不理解,进去看看
native表示调用SO层的方法
这有个libcipher.so可能有加密的方法,逆一下
打开IDA就麻爪了
视频是从base64开始入手,因为已经知道是Base64编码了,就hook base64的方法,尝试四个类
1 2 3 4 5 6 7 8 9 10 11 12 13 java.net .URLEncoder java.util .Base64 okio.Base64 okio.ByteString var base64 = Java .use ("android.util.Base64" ) ;base64.encodeToString .overload ('[B' , 'int' ).implementation = function (a, b ){ showStacks (); console .log ("base64.encodeToString:" , JSON .stringify (a)); var result = this .encodeToString (a, b); console .log ("base64.encodeToString result: " , result) return result; }
两个数据,要找的在下面
去反编译找 com.pocket.snh48.base.core.kotlin.net.KTokenHeaderInterceptor.intercept 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 str = String.valueOf(System.currentTimeMillis()) + ',' + StringsKt__StringsJVMKt.replace$default (UUID.randomUUID().toString(), "-" , "" , false , 4 , (Object) null ) + ',' + ((Object) EncryptlibUtils.MD5(AbstractC9602.m21775(), valueOf, replace$default , postkey)) + ',' + ((Object) postkeyVersion); 第一个是时间戳实锤了 第二部分 StringsKt__StringsJVMKt.replace$default (UUID.randomUUID().toString(), "-" , "" , false , 4 , (Object) null ) 这个东西是生成一个UUID,然后进行一些操作,这个东西看起来像MD5其实不是,只是一个随机数 最后一个是空值不管他 第三部分 ((Object) EncryptlibUtils.MD5(AbstractC9602.m21775(), valueOf, replace$default , postkey))
进入MD5方法
看到没有代码,这个时候有两种情况,1、被加固了,或者被加密了(详见消失的钥匙)2、代码在SO层
这里有native关键字,那就是SO层
上面有提到加载了那个SO文件 encryptlib
对比一下这两个流程,很显然下面这个更有逻辑,从base64入手。我自己就没什么逻辑了想到加密就hook一下。
IDA逆向,找到了MD5,这个 Java_+包名的 是jni的静态注册
空格切换视图,F5查看伪C代码
hook一下这个方法就好了
至于这么hook这个步子迈大了,先老老实实看一下SO的东西,再说hook
so中通常会解除到的东西
==jni调用==
jni相关的代码可以理解成so层的系统函数,下面这些就是jni的调用
这个调用名字固定,功能固定
GetStringUTFChars :将Java的字符串转换为C的字符串
NewStringUTF :将C语言字符串转换成Java字符串
关于jni还要学习的东西还很多,比如为什么调用的就是这个函数呢,是这么实现转换的呢,C是这么调用Java函数的,Java是这么调用C函数的,为什么传进去四个参数,接收了六个参数
==系统库函数==
比如这些 strlen 、memcpy C语言函数,来自libc.so,由libc.so来实现
比如 std:: 这样的C++函数,来自libC++.so
这些系统函数也是可以hook的
==加密算法==
==魔改算法==
这俩没啥好说的,学好基础就能看,熟能生巧
==系统调用==
所谓的Linux内核的东西了,app是有四层,最上方是第三方的app,然后是Java层,然后是so层,最后是Linux内核,在其他层的操作最终都是由Linux内核来实现的。那么有些app就直接对Linux内核进行调用,这个时候是hook不到的,除非魔改内核
==自定义算法==
自己写的算法,这个时候就要有汇编基础了
==SO加固、混淆==
SO有一个头表SHT,IDA就是从这个表开始,解析so文件,有的app运行起来会把这个表删掉,这个时候IDA就解析不了了,就需要去手机中进行 ==SO的dump== 就是将SO从内存中保存下来,保存的SO是损坏的,需要使用工具进行 ==SO的修复== ,想要理解就需要去了解 ==SO的文件结构== ,这个东西就很复杂了,如果工具解析不出来,说明进行了 ==自定义linker== ,否则工具一般没问题
自定义linker 是自己定义了SO的加载和解析
NDK介绍 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1、app为什么会把代码放到SO中 a C语言历史悠久,有很多现成的代码可用 b C代码的执行效率比Java高 c java代码容易被反编译,而且反编译以后逻辑比较清晰,C语言反编译的伪代码扣出来是难运行的 2、为什么学NDK开发 在安卓的so开发中,其他基本跟C/C++开发一致,而与Java交互需要用到jni 学习NDK的jni相关的内容 so中会接触到的:系统库调用、jni调用、加密算法、魔改算法、系统调用、自定义算法 3、什么是jni jni是Java Native Interface的缩写。从Java1.1开始,jni标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互 4、什么是NDK 交叉编译工具链,用来编译so代码的 NDK的配置 NDK、CMake、LLDB NDK是原生开发套件,可以让app中使用C/C++代码 CMake是外部构建工具,与Gradle搭配构建原生库 LLDB可以调试原生代码(stuid默认存在)
1 2 5、ABI与指令集 https://developer.android.com/ndk/guides/abis
ABI
支持的指令集
备注
armeabi-v7a arm32 32位汇编
armeabi Thumb-2 VFPv3-D16
arm64-v8a 64位
AArch64
x86 x86主要是来支持模拟器
x86(IA-32) MMX SSE/2/3 SSSE3
x86_64 如果没有模拟器的支持 就需要模拟器来模拟 arm的指令来运行app
x86_64 MMX SSE/2/3 SSSE3 SSE4.1、4.2 POPCNT
NDK和Java工程的区别 C工程比Java工程多静态代码块用于加载SO
需要有声明,声明完就可以使用了
1 public native String stringFromJNI();
C的代码是写在cpp文件夹下的,有个同名的cpp
build.gradle.kts 文件中指明了CMake的路径和C语言的版本,这个CMake是指导编译的文件,C语言版本下面可以选择支持的SOABI,默认是四种都支持
这些是C工程比Java工程多出来的东西,在Java工程中加上这些就是一个C工程了
jni注册 这里是一种静态注册,jni还有动态注册,这里默认是给SO层的方法传了两个参数,只要是跟Java对接的函数,都有这两个参数 JNIEnv 和 jobject/jclass
JNIEnv 后面再说,还是比较重要的
jobject是指的调用方法的对象,用 jclass是当方法为静态方法时使用
NewStringUTF 是将C语言的字符串转化为Java的字符串
extern “C” 是表示函数以C的形式解析,可能会给函数名加上一些东西
JNICALL 中没有什么东西
JNIEXPORT 这里定义了方法是否需要导出,默认是 default ,如果是hidden 之后这个函数名就不会出现在so的导出表内,如果是静态注册是必须要出现在导出表内的
so中常见的log输出 1 2 3 4 #include <android/log.h> __android_log_print(ANDROID_LOG_DEBUG, "demo" , "xxxxxx jni native %d %d" , 100 , 200 );
需要最少传入三个值,第一个是枚举类型,第二个是TAG,和安卓里面的tag一样,第三个是输出的结果,但是支持占位符,可以传超过三个的参数
这么写有点麻烦一般是使用 define 进行一个预先的声明
这个定义之后,再调用就方便很多
编译之后这俩东西就是一样的,预编译会将这个参数替换上去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <jni.h> #include <string> #include <android/log.h> #define TAG "demo hello" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__); #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); extern "C" JNIEXPORT jstring JNICALLJava_com_example_demo_MainActivity_stringFromJNI ( JNIEnv* env, jobject ) { std ::string hello = "Hello from C++" ; __android_log_print(ANDROID_LOG_DEBUG, "demo" , "xxxxxx jni native %d %d" , 100 , 200 ); LOGD("xxxxxx jni native %d %d" , 100 , 200 ); return env->NewStringUTF(hello.c_str()); }
NDK多线程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <jni.h> #include <string> #include <android/log.h> #define TAG "demo hello" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__); void myThread () { LOGD("this is from myThread" ) } extern "C" JNIEXPORT jstring JNICALLJava_com_example_demo_MainActivity_stringFromJNI ( JNIEnv* env, jobject ) { std ::string hello = "Hello from C++" ; __android_log_print(ANDROID_LOG_DEBUG, "demo" , "xxxxxx jni native %d %d" , 100 , 200 ); pthread_t thread; pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr); return env->NewStringUTF(hello.c_str()); }
==pthread_t thread==
线程ID,其实就是long
pthread_create(&thread, nullptr, reinterpret_cast<void *(*)(void *)>(myThread), nullptr);
// 线程ID(传刚刚定义的long类型的指针)、线程属性、函数(传递函数指针)、传给函数的参数
// 等待线程执行完毕
// 默认的线程属性是 joinable 随着主线程结束而结束
pthread_join(thread, nullpyr)
// 线程属性是 dettach 可以分离执行
pthread_exit(0)
// 子线程中使用它来退出线程
JNI_OnLoad so中各种函数的执行时机
init、init_array、JNI_OnLoad
JNI_OnLoad 的执行是比定义的普通函数还要早,init 比 init_array 早, init_array 比 JNI_OnLoad 早
1 2 3 4 5 6 7 8 9 10 11 JNIEXPORT jint JNI_OnLoad (JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { LOGD("getenv failed" ); return -1 ; } return JNI_VERSION_1_6; }
一个so中可以不定义JNI_OnLoad
一旦定义了JNI_OnLoad,在so被加载的时候会自动执行
必须返回JNI版本 JNI_VERSION_1_6,否则报错
JNIEnv
里面定义了很多的API,创建对象数组什么的,所以需要先获取这个JNIEnv对象,而这个对象的获取就需要用到 JavaVM 这个结构体中的 GetEnv 方法,所以,上面的代码还是很常用的
状态信息,判断是否支持JNI版本
JavaVM 结构体,注意这个是C++版本的
东西也不多,一个属性,五个函数
比较常用的只有两个方法,GetEnv 和 AttachCurrentThread 一个是在主线程获得 JNIEnv ,一个是在子线程获得 JNIEnv
点 DestroyJavaVM 查看C语言版本,其实就在上面翻一下就到了
C++版本只是对C版本的封装,其实还是调用的C中的方法,真正反编译看到的是C语言版本的
注意一下,参数个数是不一样的
JavaVM的获取
JNI_OnLoad 的第一个参数
JNI_OnUnload的第一个参数
env->GetJavaVM
对比各种方式获取的JavaVM指针是否一致
%p 打印地址,地址是一样的
因为 ==JavaVM每个进程中只有一份==(一个地址值)
JNIEnv 也是结构体,但是这个很常用
里面定义了很多的API,创建对象数组什么的,所以需要先获取这个JNIEnv对象,而这个对象的获取就需要用到 JavaVM 这个结构体中的 GetEnv 方法,具体的API后面再学,先简单知道有这个东西就好了
JNIEnv的获取方式
函数静态/动态注册,传的第一个参数(不光JNIEnv的函数)
vm->GetEnv
globalIVM->AttachCurrentThread(在子线程中使用的,和上面的同理)
主线程和子线程中取得的JNIEnv的地址是不一样的
与JavaVM不同的是,==JNIEnv是每个线程中都有一份==(每个线程一个地址值)
so的基本概念 导出表、导入表
反编译后这个Exports就是导出表,也可以在编写的时候使用hidden这样对应的函数就不会出现在导出表中
导入表在旁边的imports中,是程序依赖的函数
出现在导出表,导入表中的函数,一般可以通过frida相关的API获取函数地址,也可以自己计算
没有出现在导出表、导入表、符号表中的函数,都需要自己计算函数地址
所谓的符号表就是能看见名字的,都在符号表内,有的名字是IDA自己生成的,这个就需要自己计算,为什么一定要获取函数地址呢,因为So函数的hook需要函数地址
SO函数注册 有静态注册和动态注册两种方式
JNI函数的静态注册 静态注册的命名必须遵循一定的命名规则,一般是 Java_包名_类名_方法名
系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用静态注册的jni函数
静态注册的函数必然在导出表内
JNI动态注册
通过 env -> RegisterNatives 注册函数,通常在 JNI_OnLoad 中注册
RegisterNatives:意思是注册本地
JNINativeMethod:指定C函数和Java调用的对应关系
函数签名:指定调用哪一个C函数
可以给同一个Java函数注册多个native函数,以最后一次为准
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jclass MainActivityClazz = env->FindClass("com/example/demo/test" ); JNINativeMethod method[] = { {"stringFromJNI3" , "(Ljava/lang/String;I[B)Ljava/lang/String;" , (void *)test} }; env->RegisterNatives(MainActivityClazz, method, 1 );
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 jstring test (JNIEnv* env, jobject that, jstring a, jint b, jbyteArray c) { return env->NewStringUTF("Hello" ); } JNIEXPORT jint JNI_OnLoad (JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { LOGD("getenv failed" ); return -1 ; } jclass MainActivityClazz = env->FindClass("com/example/demo/test" ); JNINativeMethod method[] = { {"stringFromJNI3" , "(Ljava/lang/String;I[B)Ljava/lang/String;" , (void *)test} }; env->RegisterNatives(MainActivityClazz, method, sizeof (method)/sizeof (JNINativeMethod)); return JNI_VERSION_1_6; }
1 2 3 4 5 6 public class test { static { System.loadLibrary("native-lib" ); } public native String stringFromJNI3 (String a, int b, byte [] c) ; }
注意:如果有重名的调用的是后面的,以最后一个为准。如:
1 2 3 4 JNINativeMethod method[] = { {"stringFromJNI3" , "(Ljava/lang/String;IB)Ljava/lang/String;" , (void *)test}; {"stringFromJNI3" , "(Ljava/lang/String;IB)Ljava/lang/String;" , (void *)test0x1} };
这个时候在Java端调用 stringFromJNI3 时,执行的C函数是 test0x1
SO之间的相互调用 多个cpp编译成一个so文件 创建一个c的源文件,将要编译成一个so文件的cpp写在配置文件中
调用另一个cpp中的方法时,不需要再使用下面的方法调用,省去这个步骤,在前面声明一下方法名,然后在需要调用的时候直接函数名调用即可
测试成功,只有一个so,且可以正常使用cpp内的方法
编译多个SO 这个需要的操作用小脑想一下,创建cpp文件,修改CMakeLists文件。最重要的当然是CMakeLists文件。
新加上一个add_library和target_link_libraries
一个值得so的名称和so内的cpp文件,另一个进行链接
SO路径的动态获取 so在手机中的data/app/包名+随机名/lib/ 目录下
包名这个目录后面的数是随机的,每次安装都不一样
我们需要用代码来找到这个目录中的SO文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public String getPath (Context cxt) { PackageManager pm = cxt.getPackageManager(); List<PackageInfo> pkgList = pm.getInstalledPackages(0 ); if (pkgList == null || pkgList.size() == 0 ) return null ; for (PackageInfo pi : pkgList) { if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/" ) && pi.packageName.startsWith("com.example.demo" )) { return pi.applicationInfo.nativeLibraryDir; } } return null ; }
Context对象通过 getApplicationContext() 方法来获取,可以看到输出了so所在的路径,但是还差了一个so的名字,自行补充一下就好了
SO的动态调用 既然已经能够获取到SO的路径了,那么就可以实现so之间的相互调用了
在cpp文件中使用 dlopen 方法来获得
需要给两个参数,一个是SO的文件名,这个地方要给出全路径,一个是指定对SO的操着是立即解析还是懒加载等,常用的是 now和 lazy,RTLD_NOW是立即解析
注册的时候加上一个这玩意,调用的时候多加一个参数,jstring
人要的是 char * ,你给的是Java的字符串jstring,需要一个转换
使用 GetStringUTFChars 将 jstring 转换成 cstring,两个参数第一个是Java字符串,第二个是选择是否拷贝,一般用nullptr
然后使用 void* 来接收
1 2 const char * cPath = env->GetStringUTFChars(SoPath, nullptr);void * soInfo = dlopen(cPath, RTLD_NOW);
有了这个 void* 的指针就可以使用 dlsym 函数来获取so文件中的符号,不是cpp文件中的函数名,而是函数名经过符号修饰之后的结果,这个结果可以通过反编译查看,或者使用 extren “C” 修饰函数,如此函数名就不会被修饰。
返回值是一个函数指针
1 2 3 4 5 void (*ref)(); ref = reinterpret_cast<void (*)()> (dlsym(soInfo ,"_Z4testv" )); ref();
调用成功
这中方式调用so是不用在Java层加载so的,执行代码的时候就自己给加载上了,如果要卸载so使用dlclose 函数
SO的动态调用了解一下,逆向的时候可能会碰到。
这个的hook也和普通的不一样,因为这个地址是在一定时间内存在的
还有一种方式
给需要调用的SO方法 extern ”C“,然后在调用cpp的文件中使用 extern "C"
进行声明
通过JNI创建Java对象 在使用so层的方法时,难免需要Java对象,有两种创建Java对象的方式
NewObject 很像Java中的反射,找类,找方法,调方法
1 2 3 4 5 6 7 jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID methodId = env->GetMethodID(clazz, "<init>" , "()V" ); jobject ReflectDemoObj = env->NewObject(clazz, methodId); LOGD("ReflectDemoObj %p" , ReflectDemoObj);
ReflectDemoObj 是Java中的一个对象,输出的是这个对象在虚拟机中的一个唯一标识
这个地址是在虚拟机内,和外部的内存地址不在一个位置,因为在JNI中需要区分Java的东西和C的东西,本质就是因为存放的位置不一样。所以需要来回的转换
AllocObject 1 2 3 4 5 6 7 8 9 10 jclass jclass1 = env->FindClass("com/example/demo/test" ); jmethodID jmethodId = env->GetMethodID(clazz, "<init>" , "(Ljava/lang/String;)V" ); jobject ReflectDemoObj2 = env->AllocObject(jclass1); jstring jstr = env->NewStringUTF("from jni str" ); env->CallNonvirtualVoidMethod(ReflectDemoObj2, jclass1, jmethodId, jstr);
通过JNI访问Java属性 Java属性分为静态和非静态的,两种属性获取的方法不同
获取静态字段 使用 GetStatic****Field
来设置非静态字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 jclass clazz = env->FindClass("com/example/demo/test" ); jfieldID privateStaticStringField = env->GetStaticFieldID(clazz, "age" , "Ljava/lang/String;" ); jstring age = static_cast<jstring> (env->GetStaticObjectField(clazz, privateStaticStringField)); const char * privatecstr = env->GetStringUTFChars(age, nullptr); LOGD("age: %s" , age); env->ReleaseStringUTFChars(age, privatecstr);
获取对象字段 这个和获取静态字段大差不差,就几个函数不一样
使用 Get****Field
来获取非静态字段
1 2 3 4 5 6 7 8 9 10 11 jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID methodId = env->GetMethodID(clazz, "<init>" , "()V" ); jobject ReflectDemoObj = env->NewObject(clazz, methodId); jfieldID nameID = env->GetFieldID(clazz, "name" , "Ljava/lang/String;" ); jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID)); const char * nameContent = env->GetStringUTFChars(name, nullptr); LOGD("name: %s" , nameContent); env->ReleaseStringUTFChars(name, nameContent);
设置非静态字段 使用 Set****Field
来设置非静态字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID methodId = env->GetMethodID(clazz, "<init>" , "()V" ); jobject ReflectDemoObj = env->NewObject(clazz, methodId); jfieldID nameID = env->GetFieldID(clazz, "name" , "Ljava/lang/String;" ); jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID)); const char * nameContent = env->GetStringUTFChars(name, nullptr); LOGD("name: %s" , nameContent); env->ReleaseStringUTFChars(name, nameContent); env->SetObjectField(ReflectDemoObj, nameID, env->NewStringUTF("呓语" )); name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID)); nameContent = env->GetStringUTFChars(name, nullptr); LOGD("name new: %s" , nameContent);
通过JNI访问Java数组 获取数组 通过 Get****ArrayElements
获取数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID methodId = env->GetMethodID(clazz, "<init>" , "()V" ); jobject ReflectDemoObj = env->NewObject(clazz, methodId); jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray" , "[B" ); jbyteArray byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID)); int _byteArrayLength = env->GetArrayLength(byteArray1); jbyte* cByte = env->GetByteArrayElements(byteArray1, nullptr); for (int i = 0 ; i < _byteArrayLength; i++) { LOGD("byteArray: %d" , cByte[i]) } env->ReleaseByteArrayElements(byteArray1, cByte, 0 );
修改数组 通过 Get****ArrayRegion
修改数组内容,Region是一次性设置完成,即使是数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 char javaByte[10 ]; for (int i = 0 ; i < 10 ; i++) { javaByte[i] = static_cast<char >(100 -i); } const jbyte *java_array = reinterpret_cast<const jbyte*>(javaByte); env->SetByteArrayRegion(byteArray1, 0 , _byteArrayLength, java_array); byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID)); cByte = env->GetByteArrayElements(byteArray1, nullptr); for (int i = 0 ; i < _byteArrayLength; i++) { LOGD("byteArray: %d" , cByte[i]) }
通过JNI来访问Java方法 调用静态函数 调用静态函数的API和使用构造方法的API相似,但是不一样
1 2 3 4 5 6 7 jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID staticFunID = env->GetStaticMethodID(clazz, "StaticFunction" , "()V" ); env->CallStaticVoidMethod(clazz, staticFunID);
调用对象函数 1 2 3 4 5 6 7 8 9 10 11 jmethodID FunID = env->GetMethodID(clazz, "functional" , "(Ljava/lang/String;)Ljava/lang/String;" ); jstring str = env->NewStringUTF("this is from JNI" ); jstring str2 = reinterpret_cast<jstring>(static_cast<jstring> (env->CallObjectMethod(ReflectDemoObj, FunID, str))); const char * retval_cstr = env->GetStringUTFChars(str2, nullptr); LOGD("retval_cstr: %s" , retval_cstr); env->ReleaseStringUTFChars(str2,retval_cstr);
CallObjectMethodA CallObjectMethod这个API是用来调用执行函数的,同系列的还有 CallObjectMethodA 和 CallObjectMethodV
CallObjectMethod 执行的时候调用了 CallObjectMethodV,也就是说 CallObjectMethod 可以完美替代 CallObjectMethodV 在调用的时候的使用了,因为 CallObjectMethod 支持接收多个参数,如果不使用 CallObjectMethod 还得自行处理参数,传入 CallObjectMethodV
CallObjectMethodA 的使用需要传入一个 jvalue,这个jvalue是一个联合体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue; jvalue args[3 ]; args[0 ].i = 100 ; args[1 ].c = str2; args[2 ].z = true ;
传递数组作为参数 稍微升级一下,调用传入值为数组,返回值也是数组的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 jclass StringClazz = env->FindClass("java/lang/String" ); jobjectArray StringArr = env->NewObjectArray(3 , StringClazz, nullptr); for (int i = 0 ; i < 3 ; ++i) { env->SetObjectArrayElement(StringArr, i, env->NewStringUTF("NB666" )); } jmethodID StringArrayID = env->GetStaticMethodID(clazz, "functionTest" , "([Ljava/lang/String;)[I" ); jintArray jint = static_cast<jintArray> (env->CallStaticObjectMethod(clazz, StringArrayID, StringArr)); int *cinArr = env->GetIntArrayElements(jint, nullptr); LOGD("cintArr[0]=%d" , cinArr[0 ]); env->ReleaseIntArrayElements(jint, cinArr, JNI_ABORT);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 jstring demoHelloWorld (JNIEnv* env, jobject that, jstring a, jint b, jbyteArray c) { jclass clazz = env->FindClass("com/example/demo/test" ); jmethodID methodId = env->GetMethodID(clazz, "<init>" , "()V" ); jobject ReflectDemoObj = env->NewObject(clazz, methodId); LOGD("ReflectDemoObj %p" , ReflectDemoObj); jclass jclass1 = env->FindClass("com/example/demo/test" ); jmethodID jmethodId = env->GetMethodID(clazz, "<init>" , "(Ljava/lang/String;)V" ); jobject ReflectDemoObj2 = env->AllocObject(jclass1); jstring jstr = env->NewStringUTF("from jni str" ); env->CallNonvirtualVoidMethod(ReflectDemoObj2, jclass1, jmethodId, jstr); jfieldID privateStaticStringField = env->GetStaticFieldID(clazz, "age" , "Ljava/lang/String;" ); jstring age = static_cast<jstring> (env->GetStaticObjectField(clazz, privateStaticStringField)); const char * privatecstr = env->GetStringUTFChars(age, nullptr); LOGD("age: %s" , privatecstr); env->ReleaseStringUTFChars(age, privatecstr); jfieldID nameID = env->GetFieldID(clazz, "name" , "Ljava/lang/String;" ); jstring name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID)); const char * nameContent = env->GetStringUTFChars(name, nullptr); LOGD("name: %s" , nameContent); env->ReleaseStringUTFChars(name, nameContent); env->SetObjectField(ReflectDemoObj, nameID, env->NewStringUTF("呓语" )); name = static_cast<jstring> (env->GetObjectField(ReflectDemoObj, nameID)); nameContent = env->GetStringUTFChars(name, nullptr); LOGD("name new: %s" , nameContent); env->ReleaseStringUTFChars(name, nameContent); jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray" , "[B" ); jbyteArray byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID)); int _byteArrayLength = env->GetArrayLength(byteArray1); jbyte* cByte = env->GetByteArrayElements(byteArray1, nullptr); for (int i = 0 ; i < _byteArrayLength; i++) { LOGD("byteArray: %d" , cByte[i]) } env->ReleaseByteArrayElements(byteArray1, cByte, 0 ); char javaByte[10 ]; for (int i = 0 ; i < 10 ; i++) { javaByte[i] = static_cast<char >(100 -i); } const jbyte *java_array = reinterpret_cast<const jbyte*>(javaByte); env->SetByteArrayRegion(byteArray1, 0 , _byteArrayLength, java_array); byteArray1 = static_cast<jbyteArray> (env->GetObjectField(ReflectDemoObj, byteArrayID)); cByte = env->GetByteArrayElements(byteArray1, nullptr); for (int i = 0 ; i < _byteArrayLength; i++) { LOGD("byteArray: %d" , cByte[i]) } env->ReleaseByteArrayElements(byteArray1, cByte, 0 ); jmethodID staticFunID = env->GetStaticMethodID(clazz, "StaticFunction" , "()V" ); env->CallStaticVoidMethod(clazz, staticFunID); jmethodID FunID = env->GetMethodID(clazz, "functional" , "(Ljava/lang/String;)Ljava/lang/String;" ); jstring str = env->NewStringUTF("this is from JNI" ); jstring str2 = reinterpret_cast<jstring>(static_cast<jstring> (env->CallObjectMethod(ReflectDemoObj, FunID, str))); const char * retval_cstr = env->GetStringUTFChars(str2, nullptr); LOGD("retval_cstr: %s" , retval_cstr); env->ReleaseStringUTFChars(str2,retval_cstr); jclass StringClazz = env->FindClass("java/lang/String" ); jobjectArray StringArr = env->NewObjectArray(3 ,StringClazz, nullptr); for (int i = 0 ; i < 3 ; ++i) { env->SetObjectArrayElement(StringArr, i, env->NewStringUTF("NB666" )); } jmethodID StringArrayID = env->GetStaticMethodID(clazz, "functionTest" , "([Ljava/lang/String;)[I" ); jintArray jint = static_cast<jintArray> (env->CallStaticObjectMethod(clazz, StringArrayID, StringArr)); int *cinArr = env->GetIntArrayElements(jint, nullptr); LOGD("cintArr[0]=%d" , cinArr[0 ]); env->ReleaseIntArrayElements(jint, cinArr, JNI_ABORT); return env->NewStringUTF("Hello" ); }
通过JNI访问Java父类方法 在Java中使用 super 来访问父类中的方法,现在就需要在CPP文件中实现类似的操作,就需要使用CallNonvirtual 系列的API
先获取到调用的onCreate方法的父类,jclass,然后找到需要调用的父类方法的ID,再传入参数,参数是从Java层传过来的,直接放进去即可,这样就实现了一个简单的调用父类方法。
也可以看出要将Java代码使用JNI转换成c代码还是很费事的
1 2 3 4 5 6 7 8 9 extern "C" JNIEXPORT void JNICALL Java_com_example_demo_MainActivity_onCreates (JNIEnv *env, jobject thiz, jobject saved_instance_state) { jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity" ); jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate" , "(Landroid/os/Bundle;)V" ); env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state); }
内存管理 之前使用的 ReleaseIntArrayElements、ReleaseStringUTFChars 等方法也算是一种吧,这里主要介绍的是局部引用和全局引用的相关API
局部引用 之前创建的 jclass、jmethodID、jstring 什么的就是一个局部引用,在函数运行完毕后消失,看起来和局部变量是差不多的,但是不一样的。可以用全局变量来实验一下
IDE也没有报错,运行直接崩溃了
在函数中使用JNI函数,在函数执行完毕之后全部进行回收,不管是不是全局变量还是局部变量,也就是因为JNI_OnLoad函数执行完毕之后,clazz被回收了,所有在 demoHelloWorld 函数中没有获得正确的 clazz。这也就是局部引用和局部变量、全局变量的区别
大多数的jni函数,调用以后返回的结果都是局部引用
因此,env->NewLocalRef(创建一个局部引用) 基本不用
一个函数内的局部引用数量是有限制的,在早期的安卓系统中,十分的明显,现在基本够用,因此可能不太会见到这几个函数,早期的时候的so可能会有
当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用
可以使用env->DeleteLocalRef 来删除局部引用
局部引用还有一些函数
1 2 3 4 5 6 env->EnsureLocalCapacity(num) // 判断是否有足够的局部引用可以使用,足够则返回0 需要大量使用局部引用时,手动删除太过麻烦,可以使用下面两个函数来批量管理局部引用 env->PushLocalFrame(num) env->PopLocalFrame(nullptr)
全局引用 在jni开发中,需要跨函数使用变量时,直接定义全局变量是没啥用的,需要两个方法,来创建、删除全局引用
1 2 env->NewGlobalRef env->DeleteGlobalRef
env->NewGlobalRef 是创建全局引用,需要传入一个 jobject ,然后返回一个jobject
现在就可以正常运行了
全局引用使用完毕之后使用 env->DeleteGlobalRef 删除
还有两个函数,是弱全局引用,什么叫弱全局引用呢,就是内存充足的时候就是全局引用,内存紧张的时候,可能会被回收。
1 2 env->NewWeakGlobalRef env->DeleteWeakGlobalRef
子线程中获取Java类 常规的手段在子线程中是不能获取到Java类的,获取值为nullptr。获取方法会导致系统崩溃
但是可以使用FindClass来获取系统类,可以看到自己的Java类是没有获取到地址的
如果想在子线程中获取自己定义的类,也是可以的,可以用到全局引用,在主线程中获取类,使用全局引用来传递到子线程中去。
还有一种方式就是,在主线程中获取正确的ClassLoader,在子线程中去加载类
之前在Java学习过程中,出现了一些方法已经加载,但是无法hook的情况,那是因为不在同一个 ClassLoader 中,C可以先获取到对应方法所在的 ClassLoader,将ClassLoader给到子线程
在Java中想要获取ClassLoader,可以先获取类字节码,然后时候 getClassLoader() 来获取
Demo.class.getClassLoader();
new Demo().getClassLoader();
Class.forName(……).getClassLoader();
这三种方法,多少跟反射沾点边,其实FindClass就相当于Demo.class
这个时候只需要在主线程中调用一下getClassLoader() 然后在子线程中调用 loadClass() 就ojbk了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 在JNI_OnLoad中 jclass text = env->FindClass("com/example/demo/test" ); clazz = static_cast<jclass> (env->NewGlobalRef(text)); jclass javaClass = env->FindClass("java/lang/Class" ); jmethodID jmethodId = env->GetMethodID(javaClass, "getClassLoader" , "()Ljava/lang/ClassLoader;" ); jobject obj = env->CallObjectMethod(text, jmethodId); object1 = env->NewGlobalRef(obj); 在子线程中 jclass ClassLoaderClazz = env->FindClass("java/lang/ClassLoader" ); jmethodID loadClassID = env->GetMethodID(ClassLoaderClazz, "loadClass" , "(Ljava/lang/String;)Ljava/lang/Class;" ); jclass test = static_cast<jclass> (env->CallObjectMethod(object1, loadClassID, env->NewStringUTF("com.example.demo.test" ))); jmethodID jmethodId = env->GetStaticMethodID(test, "functionTest" , "([Ljava/lang/String;)[I" ); LOGD("myThread jmthodID: %p" , jmethodId);
这种方法挺麻烦的,不如 在主线程中获取类,使用全局引用来传递到子线程中来的方便,如果系统类在子线程中 findClass 更好。所以只做了解
init与initarray JNI_OnLoad 在其他函数执行之前执行,还有俩个执行的时机比他早,一个是init,一个是initarray
init和initarray的存在就是处理so的加固解密什么的
init定义 1 2 3 extern "C" void _init() { }
initarray定义 1 __attribute__ ((constructor)) void initArrayTest1 () {}
init函数定义只能是这一个,名字为 _init
,而initarray可以有多个,如果 constructor
后面不指定数字的话,就按照在cpp文件中的前后顺序执行,指定数字之后按照数字从小到大执行,不写数字的在带有数字的执行完毕之后执行,不建议使用 0-100,这些系统可能会用。这些执行完毕之后再执行 JNI_OnLoad
可以加上一个 hidden 属性,这样so反编译的时候,这个函数就不会出现
在 IDA View-A 中使用快捷键 ctrl +S,找到这个 .init_array
可以看到有这三个函数,按照给定的顺序排列执行,2被隐藏了,函数名无法解析,IDA就给自动生成了一个名字,sub_+函数地址
这个函数地址其实是这个函数在So中的偏移量,一般so中的hook都是hook地址,很少hook函数名,因为符号会被各种的修饰,去除,但是地址值是不变的
_init
编译之后,符号会变为 .init_proc
,但是定义的时候必须是 _init
onCreate函数native化 一般情况下app实现先执行MainActivity中的onCreate方法
1 2 3 4 5 6 7 8 9 10 11 ActivityMainBinding.inflate() 这个是和页面绑定的,具体的类是编译之后自己生成的,将鼠标放上面找打具体的类 // ActivityMainBinding com.example.demo.databinding public final class com.example.demo.databinding.ActivityMainBinding extends androidx.viewbinding.ViewBinding // inflate com.example.demo.databinding.ActivityMainBinding public static @NonNull com.example.demo.databinding.ActivityMainBinding inflate( @NonNull android.view.LayoutInflater inflater )
1 2 3 // getRoot com.example.demo.databinding.ActivityMainBinding public androidx.constraintlayout.widget.ConstraintLayout getRoot()
尝试实现native化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 extern "C" JNIEXPORT void JNICALL Java_com_example_demo_MainActivity_onCreates (JNIEnv *env, jobject thiz, jobject saved_instance_state) { jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity" ); jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate" , "(Landroid/os/Bundle;)V" ); env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state); jclass aClass = env->FindClass("android/app/Activity" ); jmethodID getLayoutInflaterID = env->GetMethodID(aClass, "getLayoutInflater" , "()Landroid/view/LayoutInflater;" ); jobject LayoutInflater = env->CallObjectMethod(thiz, getLayoutInflaterID); jclass ActivityMainBindingClazz = env->FindClass("com/example/demo/databinding/ActivityMainBinding" ); jmethodID inflateID = env->GetStaticMethodID(ActivityMainBindingClazz, "inflate" , "(Landroid/view/LayoutInflater;)Lcom/example/demo/databinding/ActivityMainBinding;" ); jobject Binding = env->CallStaticObjectMethod(ActivityMainBindingClazz, inflateID, LayoutInflater); jmethodID getRootID = env->GetMethodID(ActivityMainBindingClazz, "getRoot" , "()Landroidx/constraintlayout/widget/ConstraintLayout;" ); jobject ConstraintLayout = env->CallObjectMethod(ActivityMainBindingClazz, getRootID); jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity" ); jmethodID setContentViewID = env->GetMethodID(AppCompatActivityClazz, "setContentView" , "(Landroid/view/View;)V" ); env->CallVoidMethod(thiz, setContentViewID, ConstraintLayout); }
调了那么一串,其实才执行了三句话,后面的实现其实也大差不差,都是这个流程。
1 2 3 4 5 protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); }
补档:
将onCreate彻底native化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); TextView tv = binding.sampleText; String path = getPath(getApplicationContext()) + "/libdemo.so" ; tv.setText(stringFromJNI(path)); int a = 2 ; String c = stringFromJNIObject(new String ("a" ), a,new byte []{'h' ,'e' ,'l' ,'l' } ); Button button1 = binding.button1; button1.setOnClickListener(MainActivity.this ); text = binding.textView; editText = binding.editTextText; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 extern "C" JNIEXPORT void JNICALL Java_com_example_demo_MainActivity_onCreate (JNIEnv *env, jobject thiz, jobject saved_instance_state) { jclass fragmentClass = env->FindClass("androidx/fragment/app/FragmentActivity" ); jmethodID onCreateID = env->GetMethodID(fragmentClass, "onCreate" , "(Landroid/os/Bundle;)V" ); env->CallNonvirtualVoidMethod(thiz,fragmentClass ,onCreateID, saved_instance_state); jclass aClass = env->FindClass("android/app/Activity" ); jmethodID getLayoutInflaterID = env->GetMethodID(aClass, "getLayoutInflater" , "()Landroid/view/LayoutInflater;" ); jobject LayoutInflater = env->CallObjectMethod(thiz, getLayoutInflaterID); jclass ActivityMainBindingClazz = env->FindClass("com/example/demo/databinding/ActivityMainBinding" ); jmethodID inflateID = env->GetStaticMethodID(ActivityMainBindingClazz, "inflate" , "(Landroid/view/LayoutInflater;)Lcom/example/demo/databinding/ActivityMainBinding;" ); jobject Binding = env->CallStaticObjectMethod(ActivityMainBindingClazz, inflateID, LayoutInflater); jmethodID getRootID = env->GetMethodID(ActivityMainBindingClazz, "getRoot" , "()Landroidx/constraintlayout/widget/ConstraintLayout;" ); jobject ConstraintLayout = env->CallObjectMethod(Binding, getRootID); jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity" ); jmethodID setContentViewID = env->GetMethodID(AppCompatActivityClazz, "setContentView" , "(Landroid/view/View;)V" ); env->CallVoidMethod(thiz, setContentViewID, ConstraintLayout); jfieldID sampleTextID = env->GetFieldID(ActivityMainBindingClazz, "sampleText" , "Landroid/widget/TextView;" ); jobject tv = env->GetObjectField(Binding, sampleTextID); jclass ContextWrapperClazz = env->FindClass("android/content/ContextWrapper" ); jmethodID getApplicationContextID = env->GetMethodID(ContextWrapperClazz, "getApplicationContext" , "()Landroid/content/Context;" ); jobject Context = env->CallObjectMethod(thiz, getApplicationContextID); jclass MainActivityClazz = env->FindClass("com/example/demo/MainActivity" ); jmethodID getPathID = env->GetMethodID(MainActivityClazz , "getPath" , "(Landroid/content/Context;)Ljava/lang/String;" ); jstring path = static_cast<jstring> (env->CallObjectMethod(thiz, getPathID, Context)); const char *Path = env->GetStringUTFChars(path, nullptr); const char *So = "/libdemo.so" ; char *result = new char [256 ]; strcpy (result, Path); strcat (result, So); jstring PathSo = env->NewStringUTF(result); jclass TextViewID = env->FindClass("android/widget/TextView" ); jmethodID setTextID = env->GetMethodID(TextViewID, "setText" , "(Ljava/lang/CharSequence;)V" ); env->CallVoidMethod(tv, setTextID, Java_com_example_demo_MainActivity_stringFromJNI(env, thiz, PathSo)); LOGD("tv.setText(stringFromJNI(path));" ) jmethodID stringFromJNIObjectID = env->GetMethodID(MainActivityClazz, "stringFromJNIObject" , "(Ljava/lang/String;I[B)Ljava/lang/String;" ); jbyteArray a = env->NewByteArray(4 ); jbyte bytes[] = {'h' , 'e' , 'l' , 'l' }; jint myIntValue = 42 ; jstring c = demoHelloWorld(env, thiz, env->NewStringUTF("a" ), myIntValue, a); LOGD("String c = stringFromJNIobject(new String( original: \"a\"), a,new byte[]{'h','e','l','l});" ) jfieldID button1ID = env->GetFieldID(ActivityMainBindingClazz, "button1" , "Landroid/widget/Button;" ); jobject button1 = env->GetObjectField(Binding, button1ID); LOGD("Button button1 = binding.button1;" ) jclass ViewID = env->FindClass("android/view/View" ); jmethodID setOnClickListenerID = env->GetMethodID(ViewID, "setOnClickListener" , "(Landroid/view/View$OnClickListener;)V" ); env->CallVoidMethod(button1, setOnClickListenerID, thiz); LOGD("button1.setOnClickListener(MainActivity.this);" ) jfieldID textViewID = env->GetFieldID(ActivityMainBindingClazz, "textView" , "Landroid/widget/TextView;" ); jobject text = env->GetObjectField(Binding, textViewID); jfieldID editTextTextID = env->GetFieldID(ActivityMainBindingClazz, "editTextText" , "Landroid/widget/EditText;" ); jobject editText = env->GetObjectField(Binding, editTextTextID); LOGD("text = binding.textView;\n" "editText = binding.editTextText;" ) }