JNI开发
在安卓程序中实现Java和C代码的相互调用。
JNI,java native interface,Java本地开发接口,实现Java和C语言之间的相互调用。
1.NDK环境配置
打开Android Studio中的设置,左上角FIle-》setting,然后如下图配置
2.创建项目(快速上手)
默认设置即可
3.目录结构图
在默认创建号的项目中,在MainActivity加入两行假设加密的算法值
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Example of a call to a native method
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
//调用核心算法,获取签名的值,发送到后端的API
String signString=EncryptUtils.v0(11,"alex");
//更NB的算法,让他以C语言的形式实现
int sign2=EncryptUtils.v1(11,22);
Log.e("---->",String.valueOf(sign2));
}
在MainActivity同级目录下创建加密类EncryptUtils
package com.example.jnistudy;
//定义一个加密类,假设我们调用这个加密
public class EncryptUtils {
static {
System.loadLibrary("enc");
}
public static String v0(int n1,String n2){
//写了很多代码,实现了一个很NB的算法。
return "alexdsb";
}
//C语言实现的功能调用入口也放这,加native作为标识
public static native int v1(int n1,int n2);
}
创建后在cpp目录下,创建enc.c文件
#include <jni.h>
//跟加密方法的返回值一样,int则位jint
JNIEXPORT jint
//Java_开头,后续追加包名以及具体的方法名
JNICALL
Java_com_example_jnistudy_EncryptUtils_v1(JNIEnv *env, jclass clazz, jint n1, jint n2){
//TODO:implement v1()
//具体实现逻辑
return n1+n2+100;
}
然后再去CMakeLists.txt文件中创建add_library
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html.
# For more examples on how to use CMake, see https://github.com/android/ndk-samples.
# Sets the minimum CMake version required for this project.
cmake_minimum_required(VERSION 3.22.1)
# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},
# Since this is the top level CMakeLists.txt, the project name is also accessible
# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level
# build script scope).
project("jnistudy")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
#
# In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define
# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}
# is preferred for the same purpose.
#
# In order to load a library into your app from Java/Kotlin, you must call
# System.loadLibrary() and pass the name of the library defined here;
# for GameActivity/NativeActivity derived applications, the same library name must be
# used in the AndroidManifest.xml file.
#额外加一个add_libray
add_library(
enc
SHARED
enc.c
)
add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt.
native-lib.cpp)
# Specifies libraries CMake should link to your target library. You
# can link libraries from various origins, such as libraries defined in this
# build script, prebuilt third-party libraries, or Android system libraries.
target_link_libraries(
#配置enc
enc
${CMAKE_PROJECT_NAME}
# List libraries link to the target library
android
log
)
然后启动虚拟机,看控制台是否成功打印加密值133。出现则代表成功创建了java接口调用C语言加密函数的整套流程
3.1总结一下
安卓开发角度
//java:
package com.example.jnistudy;
//定义一个加密类,假设我们调用这个加密
public class EncryptUtils {
static {
System.loadLibrary("enc");
}
//C语言实现的功能调用入口也放这,加native作为标识
public static native int v1(int n1,int n2);
}
//enc.c具体的代码
Java_com_example_jnistudy_EncryptUtils_v1
复杂的逻辑和算法来实现
//一些基本配置
逆向角度:
1.使用jadx反编译apk,得到Java,根据关键字在Java中寻找相关算法
- 直接找到
- 找到带有native方法,则应该去找System.loadLibrary("enc");
- 找到去apk文件中Lib目录下寻找libenc.so
2.IDA对so文件进行反编译,得到C语言,分析C语言+Hook机制
4.补充一点
实际中上诉代码我们可以做删减,例如去除创建时自带的C文件,以及初始化MainActivity类中关于这个的Java代码,并且清除配置文件CMakeLists中的相关配置
5.类型的关系
java中的数据类型与C语言中的数据类型对应关系(前提:JNI开发)
图中只是部分用做示例
5.1具体实现Demo
MainActivity中代码
package com.example.jnistudy;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.example.jnistudy.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//测试类型关系
String sign3=EncryptUtils.v2("root");
Log.e("--->",sign3);
}
}
EncryptUtils类
package com.example.jnistudy;
//定义一个加密类,假设我们调用这个加密
public class EncryptUtils {
static {
System.loadLibrary("enc");
}
public static native String v2(String origin);
}
enc.c文件
#include <jni.h>
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v2(JNIEnv *env, jclass clazz, jstring origin) {
// TODO: implement v2()
//jstring转换C语言中的字符串数组
//假设字符串info = "root" {r,o,o,t}
char *info=(*env)->GetStringUTFChars(env,origin,0);
//*info目前是以指针的形式存在
info+=1;
*info='w';
info+=1;
*info='x';
//因为指针偏移了,所以往前推两位则会返回完整的字符串
info-=2;
return (*env)->NewStringUTF(env,info);
}
重启项目运行结果如下:
5.2番外内容
假设逆向某个APP,发现X-sign参数是一个动态的值。例如:9841651sadsadsad
找遍了Java代码,你没找到。咋办?
//很有可能是下面这种情况
public static native String ss(int n1,int n2);
String data=ss(11,22);
x-sign=data
可以利用Hook的机制,Hook JNI语言中的newStringUTF方法。
hook.newStringUTF=function(arg){
console.log(arg);
return this.newStringUTF(arg);
}
//通过上诉hook出来的结果 ,我们可以拿9841651sadsadsad这个参数值在结果里面搜索。
//看看是否存在
类型关系对应如图
6.一波案例(Java调C)
6.1数字处理
public static native int v1(int n1,int n2);
#include <jni.h>
//跟加密方法的返回值一样,int则位jint
JNIEXPORT jint
//Java_开头,后续追加包名以及具体的方法名
JNICALL
Java_com_example_jnistudy_EncryptUtils_v1(JNIEnv *env, jclass clazz, jint n1, jint n2){
//TODO:implement v1()
//具体实现逻辑
return n1+n2+100;
}
6.2字符串修改-指针
public static native String v2(String origin);
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v2(JNIEnv *env, jclass clazz, jstring origin) {
// TODO: implement v2()
//jstring转换C语言中的字符串数组
//假设字符串info = "root" {r,o,o,t}
char *info=(*env)->GetStringUTFChars(env,origin,0);
//*info目前是以指针的形式存在
info+=1;
*info='w';
info+=1;
*info='x';
//因为指针偏移了,所以往前推两位则会返回完整的字符串
info-=2;
return (*env)->NewStringUTF(env,info);
}
6.3字符串修改-数组
public static native String v3(String old);
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v3(JNIEnv *env, jclass clazz, jstring old) {
// TODO: implement v3()
//字符串数组
char *info=(*env)->GetStringUTFChars(env,old,0);
info[0]='x';
info[5]='x';
return (*env)->NewStringUTF(env,info);
}
6.4字符串拼接
//字符串拼接
public static native String v4(String name,String role);
#include <jni.h>
#include <string.h>
#include <syslog.h>
#include<stdlib.h>
int GetStringLen(char *dataString){
int count=0;
// \0在C中是隐藏得字符串末尾,
for (int i = 0; i < dataString[i]!='\0'; ++i) {
count+=1;
}
return count;
}
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v4(JNIEnv *env, jclass clazz, jstring name, jstring role) {
// 字符数组=指针
char *nameString=(*env)->GetStringUTFChars(env,name,0);
char *roleString=(*env)->GetStringUTFChars(env,role,0);
//开辟块内存,长度以及算出来了。准备为后续得拼接做准备
char *result=malloc(GetStringLen(nameString)+ GetStringLen(roleString)+1);
//拷贝
strcpy(result,nameString);
//拼接
strcat(result,roleString);
//打印日志并返回结果
syslog(LOG_ERR,"%s",result);
return (*env)->NewStringUTF(env,result);
}
6.5字符处理(十六进制处理)
String n5=EncryptUtils.v5("name=ouo&age=18");
//字符处理
public static native String v5(String data);
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v5(JNIEnv *env, jclass clazz, jstring data) {
//"name=ouo&age=19"
char *urlParams=(*env)->GetStringUTFChars(env,data,0);
int size= GetStringLen(urlParams);
//v34={1,9,,,,,}
char v34[size * 2]; //扩大长度,开辟了一块内存
char *v28=v34;
for (int i = 0; i < urlParams[i]!='\0'; ++i) {
// syslog(LOG_ERR,"%02x",urlParams[i]);
//转换成十六进制存入到v34
sprintf(v28,"%02x",urlParams[i]);
v28+=2;
}
//最终返回
return (*env)->NewStringUTF(env,v34);
}
6.6字节处理
//字节处理
public static native String v6(byte[] data);
#include <jni.h>
#include <string.h>
#include <syslog.h>
#include<stdlib.h>
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v6(JNIEnv *env, jclass clazz, jbyteArray data) {
// TODO: implement v6()
char *byteArray=(*env)->GetByteArrayElements(env,data,0);
int size=(*env)->GetArrayLength(env,data);
char v34[size*2];
char *v28=v34;
for (int i = 0; i < byteArray[i]!='\\0'; ++i) {
sprintf(v28,"%02x",byteArray[i]);
v28+=2;
}
return (*env)->NewStringUTF(env,v34);
}
6.7字节处理-案例
//字节处理
public static native String v7(byte[] data);
#include <jni.h>
#include <string.h>
#include <syslog.h>
#include<stdlib.h>
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v7(JNIEnv *env, jclass clazz, jbyteArray data) {
char *byteArray=(*env)->GetByteArrayElements(env,data,0);
int size=(*env)->GetArrayLength(env,data);
char v34[size*2];
char *v28=v34;
int v29=0;
do {
sprintf(v28,"%02x",byteArray[v29++]);
v28+=2;
} while (v29!=size);
return (*env)->NewStringUTF(env,v34);
}
7.一波案例(C调用Java)
静态方法
//创建一个实体类
package com.example.jnistudy;
/**
* 准备让C中调用Java中得某个方法
*/
public class SignQuery {
public static String getPart1(){
return "ouo";
}
public static String getPart2(int n1,int n2){
return "ouo";
}
public static String getPart3(int n1,String n2){
return "ouo";
}
}
//字符串修改-指针--接口类
public static native String v2(String origin);
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v2(JNIEnv *env, jclass clazz, jstring origin) {
// TODO: implement v2()
//jstring转换C语言中的字符串数组
//假设字符串info = "root" {r,o,o,t}
char *info=(*env)->GetStringUTFChars(env,origin,0);
//*info目前是以指针的形式存在
info+=1;
*info='w';
info+=1;
*info='x';
//因为指针偏移了,所以往前推两位则会返回完整的字符串
info-=2;
//找到类,C调用java
jclass cls=(*env)->FindClass(env,"com/example/jnistudy/SignQuery");
//找到方法
jmethodID method1=(*env)->GetStaticMethodID(env,cls,"getPart1", "()Ljava/lang/String;");
jmethodID method2=(*env)->GetStaticMethodID(env,cls,"getPart2", "(II)Ljava/lang/String;");
jmethodID method3=(*env)->GetStaticMethodID(env,cls,"getPart3", "(ILjava/lang/String;)Ljava/lang/String;");
//执行方法1
jstring res1=(*env)->CallStaticObjectMethod(env,cls,method1);
//获得方法返回结果指针
char *data=(*env)->GetStringUTFChars(env,res1,0);
return (*env)->NewStringUTF(env,info);
}
实例方法
创建测试实体类SignQuery2
package com.example.jnistudy;
public class SignQuery2 {
String name;
String city;
int count;
public SignQuery2(String city, int count) {
this.name="ouo";
this.city = city;
this.count = count;
}
public String getPart1(){
return this.name;
}
public String getPart2(int len){
return "root".substring(2);
}
public String getPart3(String prev){
return "xxx-";
}
public int getPart4(String prev,int v1){
return 100;
}
}
在加密类中创建接口
public static native String v8();
C中代码
#include <jni.h>
#include <string.h>
#include <syslog.h>
#include<stdlib.h>
JNIEXPORT jstring JNICALL
Java_com_example_jnistudy_EncryptUtils_v8(JNIEnv *env, jclass clazz) {
//找到类
jclass cls=(*env)->FindClass(env,"com/example/jnistudy/SignQuery2");
//找到构造方法
jmethodID init=(*env)->GetMethodID(env,cls,"<init>", "(Ljava/lang/String;I)V");
//实例化对象new SignQuery2(...)
//(*env)->NewStringUTF(env,"gg")--如果参数为String需要转成jstring类型
jobject cls_obj=(*env)->NewObject(env,cls,init,(*env)->NewStringUTF(env,"gg"),22);
//找到方法
jmethodID jmethodId1=(*env)->GetMethodID(env,cls,"getPart1", "()Ljava/lang/String;");
jmethodID jmethodId2=(*env)->GetMethodID(env,cls,"getPart2", "(I)Ljava/lang/String;");
jmethodID jmethodId3=(*env)->GetMethodID(env,cls,"getPart3", "(Ljava/lang/String;)Ljava/lang/String;");
jmethodID jmethodId4=(*env)->GetMethodID(env,cls,"getPart4", "(Ljava/lang/String;I)I");
//执行方法
jstring res1=(*env)->CallObjectMethod(env,cls_obj,jmethodId1);
jstring res2=(*env)->CallObjectMethod(env,cls_obj,jmethodId2,100);
jstring res3=(*env)->CallObjectMethod(env,cls_obj,jmethodId3,(*env)->NewStringUTF(env,"ouo"));
jint res4=(*env)->CallIntMethod(env,cls_obj,jmethodId4,(*env)->NewStringUTF(env,"hhhhh"),18);
char *p1=(*env)->GetStringUTFChars(env,res1,0);
// char *p2=(*env)->GetStringUTFChars(env,res2,0);
// char *p3=(*env)->GetStringUTFChars(env,res3,0);
// char *p4=(*env)->GetStringUTFChars(env, (jstring) res4, 0);
return (*env)->NewStringUTF(env,p1);
}
额外补充场景:
创建实体类SignQuery3
package com.example.jnistudy;
public class SignQuery3 {
public String token;
public String params;
public SignQuery3(String token, String params) {
this.token = token;
this.params = params;
}
@Override
public String toString() {
return params+"&sign="+token;
}
}
在加密工具类EncryptUtils中新增接口
//返回实体类
public static native SignQuery3 ss(String data);
在C文件中加密
JNIEXPORT jobject JNICALL
Java_com_example_jnistudy_EncryptUtils_ss(JNIEnv *env, jclass clazz,jstring data) {
//找到类
jclass cls=(*env)->FindClass(env,"com/example/jnistudy/SignQuery3");
//找到构造方法
jmethodID init=(*env)->GetMethodID(env,cls,"<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
//C语言中的某个功能data数据进行加密
jstring sign=(*env)->NewStringUTF(env,"hahah");
jobject cls_obj=(*env)->NewObject(env,cls,init,sign,data);
return cls_obj;
}
在MainActivity文件中调用
//假设通过实体类加密
SignQuery3 ss = EncryptUtils.ss("aid=123&page=9&size=19");
String result=ss.toString();
//结果:aid=123&page=9&size=19&sign=hahah
Log.e("---->",result);
上述场景就是有的加密是通过上述流程完成,主打一个出其不意,通过返回一个实体类,并将加密放在实体类的某个函数上,再通过C中加密,最终返回实体类后在toString取出来。
8.静态注册与动态注册
静态注册
- java中得native方法
package com.example.jnistudy;
public class EncryptUtils {
static {
System.loadLibrary("enc");
}
//C语言实现的功能调用入口也放这,加native作为标识
public static native int v1(int n1,int n2);
}
- C语言函数名的对应关系
#include <jni.h>
//跟加密方法的返回值一样,int则位jint
JNIEXPORT jint JNICALL //Java_开头,后续追加包名以及具体的方法名
Java_com_example_jnistudy_EncryptUtils_v1(JNIEnv *env, jclass clazz, jint n1, jint n2){
//TODO:implement v1()
//具体实现逻辑
return n1+n2+100;
}
逆向时比较方便,定位算法在so文件的那个函数中定义
动态注册
Java的native方法
package com.example.jnistudy;
/**
* 测试动态注册
*/
public class DynamicUtils {
static {
System.loadLibrary("dym");
}
public static native String testOne(int v1,int v2);
}
- 注意RegisterNatives的第三个参数,那里面放了函数对应关系
#include <jni.h>
//对应着方法的参数
jstring plus(JNIEnv *env, jclass clazz, jint n1, jint n2){
jstring sign=(*env)->NewStringUTF(env,"ouohaha");
return sign;
}
//对应动态注册的那个加密类里面的方法数组
static JNINativeMethod methods[]={
{"testOne", "(II)Ljava/lang/String;",(void *)plus},
};
JNIEXPORT jint
JNICALL JNI_OnLoad(JavaVM *vm,void *reserved){
JNIEnv *env=NULL;
//在java虚拟机中获取env
if((*vm)->GetEnv(vm,(void **) &env,JNI_VERSION_1_6)!=JNI_OK){
return JNI_ERR;
}
//3.调用FindClass方法,获取目标java对象
jclass clazz=(*env)->FindClass(env,"com/example/jnistudy/DynamicUtils");
if (clazz == NULL){
return JNI_ERR;
}
//4.调用RegisterNatives方法,传入env,java对象、JNINativeMethod数组、注册数目,执行动态注册
jint result=(*env)->RegisterNatives(env,clazz, methods, 1);
if (result < 0){ // 注册失败会返回一个负值
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
总结
1.反编译APK用jadx,反编译so文件用IDA。
2.以后逆向时,如果遇到某个关键字寻找生成过程,native,一定是基于C实现
- 先找so文件 + java代码System.loadLibrary("xx");--->再去lib目录下找到对应的so文件
- 导出函数-》静态注册 Java_包名_类名_方法名
- 动态注册 -> RegisterNatives的第三个参数是关键
3.逆向某个参数时,怀疑是C实现
可以尝试去Hook->NewStringUTF,生成字符串,返回给java