🗒️JNI

JNI案例开发介绍 以及 在安卓逆向中 JNI的位置 反编译找关联技巧
JNI
status
Published
type
Post
category
技术分享
date
Jan 18, 2024
slug
JNI
summary
JNI案例开发介绍 以及 在安卓逆向中 JNI的位置 反编译找关联技巧
tags
安卓
逆向
password
icon
😀
通过本文能够了解JNI开发流程 Java与C之间如何将进行交互 从逆向角度出发,了解JNI静态注册以及JNI_onload动态注册的特质 并且基于关联变量能够快速定位 交互函数 并且介绍C语言编译后的so文件 以及如何进行反编译技巧
 
 

1、JNI介绍与安装


1.1 JNI介绍

💡
JNI java native interface 简写,java本地开发接口 目的是实现安卓中java和c语言之间的相互访问与调用
notion image
 
在Android Studio进行JNI开发时,我们需要熟悉
  • Java如何调用Native方法?
  • Java方法的参数如何传递给Native层?
  • 而Native 层又如何反射调用 Java 方法?
 

1.2 NDK安装

💡
什么是NDK? NDK与JNI之间有什么关系? 1、NDK:(Native Develop Kits) 本地开发工具包是一套工具集,用于开发 Android 应用的一部分或全部本地代码(通常是 C 或 C++ 代码)。它使得开发者能够为特定处理器编译和运行本地代码,(只需要在 AndroidStudio下载) 2、NDK 和 JNI 都是 Android 平台上使用的技术,它们在 Java 与 C/C++ 代码之间提供了桥梁,但各自的角色和使用场景有所不同。
Android Studio → File → Settings → SDK
notion image
 
 

2、创建JNI项目

💡
之前创建普通项目: Empty View Activity ---》只做java开发 现在做JNI开发: Native C++---》java和C一起用
-如果选了这个,项目会多配置内容,带一个c++案例(JNI),在java中调用 -生成一些默认jni配置,不需要我们额外再配置了 -即便咱们创建的是:Empty View Activity 也可以改造成 Native项目

2.1 创建项目

notion image
notion image
notion image
notion image
 

2.2 快速开发

💡
目的: 使用java---》JNI调用---》C代码---》完成功能
·java调用c中某个方法,完成 a+b这个功能
  1. 在cpp目录下,创建一个c文件,注意文件名结尾是c--》utils.c
notion image
  1. 编写一个java类(Utils.java),在java类中编写静态方法
notion image
  1. 在java类中引入静态文件---》让这个java类跟某个c文件对应好
notion image
  1. 在CMakeLists.txt中注册
notion image
  1. 第五步:utils.c 写代码,具体实现,实现a+b
notion image
  1. 修补MainActivity.java中的代码
notion image
 

3、JNI案例


3.1、JNI类型

 
 Java数据类型
 JNI本地类型 (Native 类型)
 C/C++数据类型
数据类型描述
 boolean
 jboolean
 unsigned char 
C/C++无符号8为整数 
byte
 jbyte
 signed char 
C/C++有符号8位整数 
char
 jchar 
 unsigned short 
C/C++无符号16位整数 
 short
 jshort 
 signed short
C/C++有符号16位整数
 int
 jint 
 signed int 
C/C++有符号32位整数 
 long
 jlong
 signed long 
C/C++有符号64位整数 
float
 jfloat 
 float 
C/C++32位浮点数 
 double
 jdouble 
 double 
C/C++64位浮点数 
notion image
 

3.2、数据类型描述符

💡
在 JNI(Java Native Interface)中,数据类型描述符是一种符号表示法,用于表示 Java 数据类型在 JNI 代码中的对应类型。这些描述符在定义 JNI 方法签名时非常重要,因为它们帮助 JNI 正确识别和匹配 Java 方法的参数类型和返回值类型。
以下是一些常见的 JNI 数据类型描述符:

基本类型描述符

  • V:表示 void 类型。只用于方法返回类型。
  • Z:表示 boolean 类型。
  • B:表示 byte 类型。
  • C:表示 char 类型。
  • S:表示 short 类型。
  • I:表示 int 类型。
  • J:表示 long 类型。
  • F:表示 float 类型。
  • D:表示 double 类型。

引用类型描述符

  • L完全限定类名;:表示类实例,例如 Ljava/lang/String; 表示 String 类型。完全限定名是使用 / 而不是 . 的类名(例如 java/lang/String)。
  • [:表示数组。数组的类型由紧随其后的元素类型描述符指定,例如 [I 表示 int[](整数数组),[Ljava/lang/String; 表示 String[](字符串数组)。

方法签名

在 JNI 中,方法签名由参数类型描述符序列和返回值类型描述符组成,格式为 (参数类型描述符)返回值类型描述符。例如:
  • ()V:表示无参数,无返回值的方法。
  • (I)Z:表示有一个 int 类型参数,返回 boolean 类型的方法。
  • (Ljava/lang/String;I)Ljava/lang/String;:表示有一个 String 类型参数和一个 int 类型参数,返回 String 类型的方法。

使用 JNI 类型描述符的示例

它的 JNI 签名将是 (II)I,表示这个方法接收两个 int 类型的参数,并返回一个 int 类型的值。
 

3.3、Java调用C

编写 JNI 接口的 Java 代码

  1. 在 MainActivity.java 或其他适当的 Java 类中,添加 JNI 方法的声明:

    步骤 4: 编写 C/C++ 代码

    1. 在 app/src/main/cpp 目录中创建一个名为 native-lib.cpp 的 C++ 源文件,用于实现 JNI 方法:
      1. 在同一目录下创建 CMakeLists.txt 文件(用于注册):
         

        3.4、C调用Java

        notion image

        示例 1: 调用 Java 静态方法

        步骤 1: 创建 Java 类及静态方法
         
        步骤 2: 编写 JNI 接口的 C/C++ 代码
        在 app/src/main/cpp 目录下创建 C/C++ 源文件,例如 native-lib.cpp:
        这段代码查找 Java 类 MyJavaClass 并调用其静态方法 staticMethod。
        注意:com/example/yourapp 是Java的包名MyJavaClass 是包名下的类名
         
        步骤 3: 从 Java 调用 JNI 方法
        在 MainActivity.java 或其他 Java 类中,加载本地库并调用 JNI 方法:
         
        示例 2: 调用 Java 成员方法
        java中,成员方法,绑定给对象,需要类实例化得到对象---》对象.成员方法
        步骤 1: 创建 Java 类及成员方法
         
        步骤 2: 编写 JNI 接口的 C/C++ 代码
        同样在 app/src/main/cpp 下创建或修改 C/C++ 源文件:
        这段代码首先创建了 MyJavaClass 的一个实例,然后调用其成员方法 instanceMethod。
        步骤 3: 从 Java 调用 JNI 方法
        在 MainActivity.java 中调用新的 JNI 方法:

        3.5、静态注册与动态注册

        在 JNI(Java Native Interface)中,静态注册和动态注册是两种将 Java 方法和本地(C/C++)方法关联起来的机制。逆向工程的角度来看,区分这两种注册方式对于理解应用的工作原理和安全性分析非常重要。

        静态注册

        静态注册是一种传统的 JNI 方法注册方式,它依赖于 Java 方法的名称和签名。在这种方式下,本地方法的名称需要遵循一定的命名规则:Java_包名_类名_方法名。

        如何进行静态注册

        1. Java 方法声明:在 Java 类中声明 native 方法。
          1. C/C++ 方法实现:在 C/C++ 文件中实现该方法,遵循特定的命名规则。

            动态注册

            动态注册是一种更灵活的 JNI 方法注册方式,允许在运行时将任意的本地方法名称映射到 Java 方法上。这是通过在本地代码中显式调用 RegisterNatives 函数实现的。

            如何进行动态注册

            1. Java 方法声明:与静态注册相同,声明 native 方法。
              1. C/C++ 方法实现:实现方法,但不需要遵循特定的命名规则。
                1. 显式注册:在 JNI_OnLoad 函数中调用 RegisterNatives。

                  从逆向工程角度区分静态和动态注册

                  1. 通过命名识别静态注册:在逆向工程时,静态注册的本地方法可以通过它们的名称被识别。如果你看到遵循 Java_包名_类名_方法名 规则的方法名,很可能是静态注册的 JNI 方法。
                  1. 查找 RegisterNatives 调用识别动态注册:动态注册的方法通常不遵循特定的命名规则。要识别这些方法,需要在二进制文件中查找 RegisterNatives 函数的调用。这通常在 JNI_OnLoad 函数中完成。
                  1. 反编译工具:使用诸如 Ghidra、IDA Pro、Radare2 等逆向工程工具可以帮助识别 JNI 方法的注册方式。静态注册的方法会直接出现在反编译代码中,而动态注册的方法可能需要更深入的分析。
                  1. 字符串和符号分析:在 APK 或其反编译的代码中搜索特定的字符串(如类名、方法名)和 JNI 相关符号(如 RegisterNatives),这对于识别 JNI 方法的注册方式非常有用。
                   
                   

                  4、如何反编译打包好的so文件→查看C代码

                  这里推荐一个工具叫做IDA pro 这是一个用于反编译打包apk后 得到二进制so文件用于反编译C的反编译工具
                  -反编译java---》jadx -反编译so---》IDA
                  • 把apk后缀名改成zip后使用压缩工具把 apk解压
                  • 进入lib的arm64-v8a目录,看到so文件
                  • 把so文件拖动到IDA中
                  • 把so文件拖动到IDA中
                  • 双击函数名,看到汇编
                  • 按F5,把混编进行反编译
                   

                  4.1、反编译静态注册

                  1. 反编译app
                  1. 找到了jni调用位置
                  1. 通过System.loadLibrary("utils") 确定是哪个so文件
                  1. 去so文件中,通过 静态注册方案找 v6 对应的c中的函数
                  notion image
                  notion image
                  notion image
                  notion image
                  notion image
                   
                   

                  4.2、反编译动态注册

                  1. 把so拖到 IDA后,看JNI_OnLoad
                  1. 找到gMethods【java和c的对应关系】
                  notion image
                  notion image
                   
                   
                   

                  4.3、反编译后引入JNI头文件与代码优化

                  反编译C文件后 代码很混乱, 我们引入JNI的头文件
                  ·在IDA中--》点击file---》Load file---》Parse C Header file--》选择jni.h或者jni_md.h
                  notion image
                  引入头文件之后 ,我们在第一个参数位置点击右键再找到Convert to struct *…
                  notion image
                  我们就能找到JNI的ENV ,因为我们知道他这个第一个参数就是JNI的env
                  notion image
                  所以我们引入jni.h这个头文件 才会有这个属性变量
                  所以我们引入jni.h这个头文件 才会有这个属性变量
                  开发环境图
                  开发环境图
                  💡
                  引入后,把__int64 a1 做类型转换,在上面点击右键--》convert to struct *---》选择JNIenv_
                  于是我们目前的代码就清晰一些了
                  但是还能不能更加清晰明了呢
                   
                  在代码上 右键:hide casts 隐藏投射,发现代码的类型不见了,代码清晰一些了
                  notion image
                  在代码上 右键:hide casts 隐藏投射,发现代码的类型不见了,代码清晰一些了
                  隐藏了强制类型转换:
                  notion image
                  注意关键字 → NewStringUTF
                  开发环境图,之前JNI开发 返回的字符串就是这个东西
                  开发环境图,之前JNI开发 返回的字符串就是这个东西
                   
                  所以再上图 v6就是NewStringUTF
                  也就是说
                  v7就是返回的字符串, 持续双击跟进该字符变量的内容 就能知道返回的内容
                  notion image
                   
                   

                  📎 参考文章

                  • 一些引用
                  • 引用文章
                  上一篇
                  Frida反调试
                  下一篇
                  App逆向-X货app
                  Loading...