1. 1. 生成Android能动态加载的Jar包
  2. 2. 动态加载Jar包
  3. 3. NDK编程
    1. 3.1. JNI 是什么
    2. 3.2. NDK 是什么
    3. 3.3. JNI 与 NDK 的关系
    4. 3.4. 第一个JNI程序
    5. 3.5. 数据类型
      1. 3.5.1. 基本数据类型
      2. 3.5.2. 引用数据类型
      3. 3.5.3. 数据类型描述符
      4. 3.5.4. Java 引用类型描述符
      5. 3.5.5. JNI 方法签名格式
    6. 3.6. JNI方法
    7. 3.7. JNI注册
      1. 3.7.1. 静态注册
      2. 3.7.2. 动态注册
        1. 3.7.2.1. RegisterNatives方法解析
    8. 3.8. Android.mk 和 CMake 语法
    9. 3.9. Android Studio 中使用 NDK
  4. 4. APK文件结构
    1. 4.1. assets文件夹
    2. 4.2. lib文件夹
    3. 4.3. META-INF文件夹
    4. 4.4. AndroidManifest.xml配置文件
    5. 4.5. resources.arsc文件
    6. 4.6. res文件夹
  5. 5. dex文件结构
    1. 5.1. string_ids
    2. 5.2. type_ids
    3. 5.3. proto_ids
    4. 5.4. field_ids
    5. 5.5. method_ids
    6. 5.6. class_def
      1. 5.6.1. DefCLassData
      2. 5.6.2. DexField
      3. 5.6.3. DexMethod
      4. 5.6.4. DexCode
  6. 6. ELF文件结构
    1. 6.1. ELF Header
    2. 6.2. Program Header
      1. 6.2.1. Base Address
      2. 6.2.2. p_type
    3. 6.3. Section Header
      1. 6.3.1. sh_type
      2. 6.3.2. sh_flags
    4. 6.4. Program Header 和 Section header区别
  7. 7. Smali汇编
    1. 7.1. 基本类型
    2. 7.2. 类的定义
    3. 7.3. 函数声明
    4. 7.4. 函数返回关键字
    5. 7.5. 构造函数声明
    6. 7.6. 变量声明
    7. 7.7. 常量声明
    8. 7.8. 静态代码块
    9. 7.9. 函数调用
    10. 7.10. 字段取值与赋值
    11. 7.11. Smali数组的取值与赋值
    12. 7.12. Smali对象创建
    13. 7.13. Smali常量数据定义
      1. 7.13.1. Smali字符串常量定义
      2. 7.13.2. Smali字节码常量定义
      3. 7.13.3. Smali数值常量定义
    14. 7.14. Smali条件跳转
    15. 7.15. Smali逻辑循环
    16. 7.16. 寄存器
  8. 8. ARM汇编
    1. 8.1. 寄存器详解
      1. 8.1.0.1. 1. 通用寄存器
      2. 8.1.0.2. 2. 专用寄存器
      3. 8.1.0.3. 3. 控制寄存器
    2. 8.1.1. 2.1 32位数据操作指令
    3. 8.1.2. 2.2 32位存储器数据传送指令
    4. 8.1.3. 2.3 32位转移指令
    5. 8.1.4. 2.4 其它32位指令
    6. 8.1.5. 2.5 立即数
    7. 8.1.6. 2.6 逻辑数
    8. 8.1.7. 2.7 逻辑运算和算术运算
  9. 8.2. 实例讲解
    1. 8.2.1. 3.1 MRS
    2. 8.2.2. 3.2 MSR
    3. 8.2.3. 3.3 PRIMASK
    4. 8.2.4. 3.4 FAULTMASK
    5. 8.2.5. 3.5 BX指令
    6. 8.2.6. 3.6 零寄存器 wzr、xzr
    7. 8.2.7. 3.7 立即寻址指令MOV
    8. 8.2.8. 3.8 寄存器间接寻址指令LDR
    9. 8.2.9. 3.9 寄存器移位寻址指令LSL
    10. 8.2.10. 3.10 基址寻址指令 STR
    11. 8.2.11. 3.11 多寄存器寻址指令
    12. 8.2.12. 3.12 无条件转移B,BAL
    13. 8.2.13. 3.13 条件转移B.cont
    14. 8.2.14. 3.14 WFE 和 WFI 对比
    15. 8.2.15. 3.15 MRC:协处理器寄存器到ARM寄存器的数据传输
    16. 8.2.16. 3.16 MCR:寄存器到协处理器寄存器的数据传输
    17. 8.2.17. 3.17 STM:将指令中寄存器列表中的各寄存器数值写入到连续的内存单元中
    18. 8.2.18. 3.18 LDM:将数据从连续内存单元中读取到指令的寄存器列表中的各寄存器中
    19. 8.2.19. 3.19 LDR:从内存中将一个32位的字读取到目标寄存器
    20. 8.2.20. 3.20 STR:将32位字数据写入到指定的内存单元
    21. 8.2.21. 3.21 SWI:软中断指令
    22. 8.2.22. 3.22 BIC清除位
    23. 8.2.23. 3.23 EOR逻辑异或指令
    24. 8.2.24. 3.24 CMN与负数对比
    25. 8.2.25. 3.25 MVN取反
    26. 8.2.26. 3.26 LSL(Logical Shift Left)左移运算
    27. 8.2.27. 3.27 STP
  10. 8.3. 实例解析
  • 9. Android系统源码基础
    1. 9.1. 系统架构
      1. 9.1.1. 应用层
      2. 9.1.2. 应用框架层
      3. 9.1.3. 系统运行库层
        1. 9.1.3.1. Android Runtime(ART)
        2. 9.1.3.2. C/C++ 原生库(Native Libraries)
      4. 9.1.4. 硬件抽象层
      5. 9.1.5. Linux内核层
    2. 9.2. 源码目录结构
  • 10. Frida 使用
    1. 10.1. 将一个脚本注入到Android目标进程
    2. 10.2. Hook Java
      1. 10.2.1. 带参数的构造函数
      2. 10.2.2. 无参数构造函数
      3. 10.2.3. 一般函数
      4. 10.2.4. 无参数的函数
      5. 10.2.5. 调用函数
      6. 10.2.6. 字段操作
      7. 10.2.7. 类型转换
      8. 10.2.8. Java.available字段
      9. 10.2.9. Java.perform方法
    3. 10.3. Hook Native
      1. 10.3.1. Hook Native层中调用的函数并且读取传入的参数
      2. 10.3.2. Hook修改native层程序返回值
      3. 10.3.3. 调用native层中未被调用的方法
      4. 10.3.4. 主动调用native中的函数
      5. 10.3.5. 打印堆栈
      6. 10.3.6. 使用frida 追踪jni函数动静态注册
  • Android基础

    生成Android能动态加载的Jar包

    这样可以直接build生成jar包,生成的jar包会在如下图片的路径下

    具体项目里没显示也不知道为什么。如果想要更改成release版本只需在structure里

    之后选择

    将其改变为release即可

    利用gradle打jar包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    task makeJar(type: Copy) {
    //删除存在的
    delete 'build/libs/myjar.jar'
    //设置拷贝的文件
    from('build/intermediates/aar_main_jar/release/')
    //打进jar包后的文件目录
    into('build/libs/')
    //将classes.jar放入build/libs/目录下
    //include ,exclude参数来设置过滤
    include('classes.jar')
    //重命名
    rename ('classes.jar', 'myjar.jar')
    }

    makeJar.dependsOn(build)

    然后在命令行输入

    1
    gradlew makeJar

    或者直接点击运行也行

    之后可以利用d8 将生成的jar包提取到桌面

    1
    d8 --dex --output=test.jar /Users/ocean/Cybersecurity/Android_Project/Study_Android/creatJar/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classs.jar

    动态加载Jar包

    参考:
    https://blog.csdn.net/fengyulinde/article/details/79623743
    https://blog.csdn.net/u012121105/article/details/129297666

    这里主要记录下重要的逻辑

    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
    package com.lgf.dynamicload.demo;

    import android.os.Bundle;
    import android.os.Environment;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    import android.widget.Toast;

    import com.lgf.plugin.IDynamic;

    import java.io.File;

    import dalvik.system.DexClassLoader;

    public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }

    // layout相应的View中使用onClick属性
    public void showMessage(View view) {
    // 此路径为插件存放路径
    File dexPathFile = new File(Environment.getExternalStorageDirectory() + File.separator + "plugin.jar");
    String dexPath = dexPathFile.getAbsolutePath();
    String dexDecompressPath = getDir("dex", MODE_PRIVATE).getAbsolutePath(); // dex解压后的路径
    // String dexDecompressPath = Environment.getExternalStorageDirectory().getAbsolutePath(); // 不能放在SD卡下,否则会报错
    /**
    * DexClassLoader参数说明
    * 参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限(<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> )
    * 参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。本文getDir("dex", MODE_PRIVATE)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成output.dex;如果不满足条件,Android会报错误
    * 参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null
    * 参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()获取到。
    */
    DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexDecompressPath, null, getClassLoader());
    Class libClazz = null;
    try {
    libClazz = dexClassLoader.loadClass("com.lgf.base.DynamicTest");
    IDynamic lib = (IDynamic) libClazz.newInstance();
    Toast.makeText(this, lib.show(), Toast.LENGTH_SHORT).show();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    记得添加下权限

    1
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

    NDK编程

    JNI 是什么

    1
    2
    3
    JNI 是 Java Native Interface 的缩写,即 Java 的本地接口。
    目的是使得 Java 与本地其他语言(如 C/C++)进行交互。
    JNI 是属于 Java 的,与 Android 无直接关系。

    NDK 是什么

    1
    2
    3
    NDK 是 Native Development Kit 的缩写,是 Android 的工具开发包。
    作用是快速开发 C/C++ 的动态库,并自动将动态库与应用一起打包到 apk。
    NDK 是属于 Android 的,与 Java 无直接关系。

    JNI 与 NDK 的关系

    1
    JNI 是实现的目的,NDK 是 Android 中实现 JNI 的手段。

    第一个JNI程序

    新建一个c++Native程序,Android Studio 会自动帮你生成一个可执行的 Hello World 程序,我们简单看一下这个工程

    其中 cpp 目录就是我们的 C/C++ 代码、预编译库的默认路径了,而 CMakeList.txt 就是编译的脚本文件了

    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
    package com.example.ndkstudy;  

    import androidx.appcompat.app.AppCompatActivity;

    import android.os.Bundle;
    import android.widget.TextView;

    import com.example.ndkstudy.databinding.ActivityMainBinding;

    public class MainActivity extends AppCompatActivity {

    // Used to load the 'ndkstudy' library on application startup.
    static {
    System.loadLibrary("ndkstudy");
    }

    private ActivityMainBinding binding;

    @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());
    }

    /**
    * A native method that is implemented by the 'ndkstudy' native library, * which is packaged with this application. */ public native String stringFromJNI();
    }

    Java 调用本地方法,是使用 native 关键字。而本例中,本地方法的实现是在一个叫做 “native-lib” 的动态库里(动态库的名称是在 CMakeList.txt 中指定的),要想使用这个动态库,就必须先加载这个库,即 System.loadLibrary(native-lib) 。这些都是 Java 的语法定义。

    看一下cpp的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <jni.h>  
    #include <string>

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_ndkstudy_MainActivity_stringFromJNI(
    JNIEnv* env,
    jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
    }

    上面提到的 public native String stringFromJNI() 方法的实现就是在这里,那怎么知道 Java 中的某个 native 方法是对应的 cpp 中的哪个方法呢?这就和 JNI 的注册有关了,本例中使用的是静态注册,即 “Java包名类名_方法名” 的形式,其中包名也是用下划线替代点号。

    总结下流程:

    1
    2
    3
    4
    1.Gradle 调用您的外部构建脚本 CMakeLists.txt。
    2.CMake 按照构建脚本中的命令将 C++ 源文件 native-lib.cpp 编译到共享的对象库中,并命名为 libnative-lib.so,Gradle 随后会将其打包到 APK 中。
    3.运行时,应用的 MainActivity 会使用 System.loadLibrary() 加载原生库。现在,应用可以使用库的原生函数 stringFromJNI()。
    4.MainActivity.onCreate() 调用 stringFromJNI(),这将返回“Hello from C++”并使用这些文字更新 TextView。

    在列一下CMakeLists.txt

    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
    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("ndkstudy")

    # 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_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(${CMAKE_PROJECT_NAME}
    # List libraries link to the target library
    android
    log)

    解释下含义用gpt

    cmake_minimum_required(VERSION 3.22.1)`

    • 含义:声明本项目使用的最低 CMake 版本是 3.22.1。这是为了确保 CMake 的语法和功能版本兼容。

    project("ndkstudy")

    • 含义:定义项目名称为 ndkstudy

    • 作用

      • 会设置变量 PROJECT_NAMEndkstudy

      • 在顶层 CMakeLists.txt 中,PROJECT_NAMECMAKE_PROJECT_NAME 是一样的;

      • 这个名称也通常被用作生成库文件的前缀,例如生成 libndkstudy.so

    add_library(${CMAKE_PROJECT_NAME} SHARED native-lib.cpp)

    • 含义

      • 创建一个共享库(SHARED),名字就是上面定义的项目名 ndkstudy

      • 包含一个源文件 native-lib.cpp,即这个库的源代码;

    • 结果

      • 会构建出一个名为 libndkstudy.so 的共享库。

    target_link_libraries(${CMAKE_PROJECT_NAME} android log)

    • 含义

      • 为这个原生库链接上其他的依赖库:

      • android: Android 原生 API;

      • log: 用于调用 Android 的日志系统(如 __android_log_print);

    • 效果

      • 可以在 native-lib.cpp 中使用 Android 系统的功能,例如打印 log 信息。

    数据类型

    基本数据类型

    Java 数据类型 JNI 本地类型 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 位浮点数

    上表显示了java对应的c的数据类型使用JNI进行中转

    引用数据类型

    以下是你Java 类类型 与 JNI 引用类型 的对应关系

    Java 类类型 JNI 引用类型 类型描述
    java.lang.Object jobject 表示任何 Java 对象,或没有特定 JNI 类型的对象(实例方法参数)
    java.lang.String jstring Java 的 String 字符串对象
    java.lang.Class jclass Java 的 Class 类型对象(用于静态方法的强制参数)
    Object[] jobjectArray Java 中任意对象数组的表示形式
    boolean[] jbooleanArray Java 基本类型 boolean 的数组表示形式
    byte[] jbyteArray Java 基本类型 byte 的数组表示形式
    char[] jcharArray Java 基本类型 char 的数组表示形式
    short[] jshortArray Java 基本类型 short 的数组表示形式
    int[] jintArray Java 基本类型 int 的数组表示形式
    long[] jlongArray Java 基本类型 long 的数组表示形式
    float[] jfloatArray Java 基本类型 float 的数组表示形式
    double[] jdoubleArray Java 基本类型 double 的数组表示形式
    java.lang.Throwable jthrowable Java 的异常类型,包括所有子类
    void void 无返回值(用于 JNI 方法返回类型)

    数据类型描述符

    Java 类型 JNI 类型描述符 说明
    int I 整型
    long J 长整型
    byte B 字节型
    short S 短整型
    char C 字符型(UTF-16)
    float F 单精度浮点型
    double D 双精度浮点型
    boolean Z 布尔型
    void V 无返回值

    Java 引用类型描述符

    Java 类型 JNI 类型描述符 示例
    引用类型(类) L<类的全限定名>; Ljava/lang/String; 表示 String
    数组类型(任意维度) [ + 元素类型描述符 [I 表示 int[][Ljava/lang/String; 表示 String[]

    JNI 方法签名格式

    1
    (参数列表)返回值 
    示例 Java 方法签名 对应 JNI 方法描述符
    void foo() ()V
    int sum(int a, int b) (II)I
    String concat(String a, String b) (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    int[] getData() ()[I
    void setValues(float[] values, boolean flag) ([FZ)V

    可以验证一下

    可以看到我们定义的一个方法,编译一下之后用jabap看一下他的签名

    1
    javap -s com.example.ndkstudy.MainActivity

    符合表格的规则。来写一个读取scard目录的文件功能练习

    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
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143


    public class MainActivity extends AppCompatActivity {

    private final static int MY_PERMISSIONS_REQUEST_WRITE_CODE = 11;



    // Used to load the 'ndk01' library on application startup.

    static {

    System.loadLibrary("ndk01");

    }



    public int testFun(String a, double b, long c){

    return 1;

    }



    private ActivityMainBinding binding;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);



    binding = ActivityMainBinding.inflate(getLayoutInflater());

    setContentView(binding.getRoot());



    testFun("aa", 4.5, 5);



    // Example of a call to a native method

    TextView tv = binding.sampleText;

    tv.setText(stringFromJNI());

    tv.setOnClickListener(new View.OnClickListener() {

    @Override

    public void onClick(View v) {

    int ret = ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (ret == PackageManager.PERMISSION_GRANTED){

    Log.i("tttttttt", "已经有写SDCard的权限了");

    String fp1 = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();

    String fc = readSDCardFile(fp1+"/b.txt");

    Log.i("tttttttt", "文件内容:" + fc);

    }else{

    Log.i("tttttttt", "还没有写SDCard的权限");

    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, MY_PERMISSIONS_REQUEST_WRITE_CODE);

    }

    }

    });

    }



    @Override

    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);



    switch (requestCode){

    case MY_PERMISSIONS_REQUEST_WRITE_CODE:{

    if (grantResults.length > 0 && grantResults[0] != -1){

    Log.i("tttttttt", "写SDCard权限申请成功");

    }else{

    Log.i("tttttttt", "写SDCard权限申请失败");

    }

    break;

    } case 33:{

    Log.i("tttttttt", "这里是其他权限申请的结果");

    break;

    }

    }



    }



    /**

    * A native method that is implemented by the 'ndk01' native library,

    * which is packaged with this application.

    */

    public native String stringFromJNI();



    public native String readSDCardFile(String filePath);

    }

    cpp实现读取文件的功能

    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
    #include <jni.h>

    #include <string>

    #include <android/log.h>



    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "tttttttt", __VA_ARGS__)

    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "tttttttt", __VA_ARGS__)

    #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, "tttttttt", __VA_ARGS__)

    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "tttttttt", __VA_ARGS__)




    extern "C" JNIEXPORT jstring JNICALL

    Java_a_b_c_ndk01_MainActivity_stringFromJNI(

    JNIEnv* env,

    jobject /* this */) {

    std::string hello = "Hello from C++";

    return env->NewStringUTF(hello.c_str());

    }

    extern "C"
    JNIEXPORT jstring JNICALL
    Java_a_b_c_ndk01_MainActivity_readSDCardFile(JNIEnv *env, jobject thiz, jstring file_path) {
    //先将java类型的字符串转换成c类型的字符
    const char* filePath = env->GetStringUTFChars(file_path, nullptr);
    FILE *fp = fopen(filePath, "r");

    if (fp == nullptr) {
    //类似于android中的log.i就是打印日志的功能
    LOGE("Failed to open file: %s", filePath);
    //释放内存
    env->ReleaseStringUTFChars(file_path, filePath);
    return env->NewStringUTF("open file failed");
    }

    char buffer[1024];
    std::string result; // 使用 std::string 拼接内容

    while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
    result += buffer;
    }

    fclose(fp);
    env->ReleaseStringUTFChars(file_path, filePath);

    return env->NewStringUTF(result.c_str());
    }

    JNI方法

    参考: https://blog.csdn.net/afei__/article/details/81016413

    写一个例子,反射获取类和方法

    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
    // 告诉编译器使用 C 语言的函数命名方式,避免函数名被 C++ 编译器修改(名称改编)
    extern "C"

    // 定义一个 JNI 函数,返回一个 jint(int 类型),对应 Java 中 native 方法:callJavaFunFromJNI
    JNIEXPORT jint JNICALL
    Java_a_b_c_ndk01_MainActivity_callJavaFunFromJNI(JNIEnv *env, jobject thiz, jobject param) {
    // 获取 param 对象的类对象(即 Java 层传入的 Student 对象)
    jclass jclass_student = env->GetObjectClass(param);

    // 通过类名查找 Student 类(可选,这里其实没用上)
    jclass jclass_student2 = env->FindClass("a/b/c/ndk01/Student");

    // 获取名为 study 的成员方法 ID,签名表示接收一个 int,返回一个 String
    jmethodID jmethodId_study = env->GetMethodID(jclass_student, "study", "(I)Ljava/lang/String;");

    // 定义参数传入 Java 的 study 方法
    int flag = 34;

    // 调用 param 对象的 study 方法,返回一个 jobject(其实是 jstring 类型)
    jobject jobject_ret = env->CallObjectMethod(param, jmethodId_study, flag);

    // 将返回的 jstring 转换为 C 字符串
    char* t = (char*)env->GetStringUTFChars((jstring)jobject_ret, 0);

    // 打印 JNI 调用返回的字符串
    LOGI("ndk call study ret: %s", t);

    // 返回 flag 值(这里只是演示,实际返回值可以按需求设置)
    return flag;
    }

    // 继续使用 C 语言命名规则
    extern "C"

    // 定义一个 JNI 函数,返回 jstring(Java 字符串),对应 Java 中 native 方法:callStaticJavaFunFromJNI
    JNIEXPORT jstring JNICALL
    Java_a_b_c_ndk01_MainActivity_callStaticJavaFunFromJNI(JNIEnv *env, jobject thiz) {
    // 找到 Student 类
    jclass jclass_student2 = env->FindClass("a/b/c/ndk01/Student");

    // 获取名为 calcLength 的静态方法 ID,签名表示接收一个 String,返回一个 int
    jmethodID jmethodId_calcLength = env->GetStaticMethodID(jclass_student2, "calcLength", "(Ljava/lang/String;)I");

    // 创建一个 Java 字符串作为参数传入
    jstring jstring_param = env->NewStringUTF("hahahaha");

    // 调用静态方法 calcLength,传入字符串参数,获取返回的 int 值
    jint jint_ret = env->CallStaticIntMethod(jclass_student2, jmethodId_calcLength, jstring_param);

    // 打印返回的 int 值
    LOGI("ndk call calcLength ret: %d", jint_ret);

    // 返回原始字符串参数(只是为了展示)
    return jstring_param;
    }

    JNI注册

    静态注册

    静态注册就是通过 JNIEXPORT 和 JNICALL 两个宏定义声明,在虚拟机加载 so 时发现上面两个宏定义的函数时就会链接到对应的 native 方法。

    注册的规则:

    Java + 包名 + 类名 + 方法名

    其中使用下划线将每部分隔开,包名也使用下划线隔开,如果名称中本来就包含下划线,将使用下划线加数字替换。

    示例 包名:com.afei.jnidemo,类名:MainActivity

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Java native method
    public native String stringFromJNI();
    // JNI method
    JNIEXPORT jstring JNICALL
    Java_com_afei_jnidemo_MainActivity_stringFromJNI( JNIEnv *env, jobject instance);

    // Java native method
    public native String stringFrom_JNI();
    // JNI method
    JNIEXPORT jstring JNICALL
    Java_com_afei_jnidemo_MainActivity_stringFrom_1JNI(JNIEnv *env, jobject instance);

    动态注册

    通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。

    来看一个例子

    1
    2
    public native String stringFromJNI(); 
    public static native int add(int a, int b);

    一般在JNI_OnLoad完成注册

    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
    #include <jni.h>
    #include <string>
    #include "log.hpp"

    extern "C" {

    jstring stringFromJNI(JNIEnv *env, jobject instance) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
    }

    jint add(JNIEnv *env, jclass clazz, jint a, jint b) {
    return a + b;
    }

    jint RegisterNatives(JNIEnv *env) {
    jclass clazz = env->FindClass("com/afei/jnidemo/MainActivity");
    if (clazz == NULL) {
    LOGE("con't find class: com/afei/jnidemo/MainActivity");
    return JNI_ERR;
    }
    JNINativeMethod methods_MainActivity[] = {
    {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
    {"add", "(II)I", (void *) add}
    };
    // int len = sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]);
    return env->RegisterNatives(clazz, methods_MainActivity,
    sizeof(methods_MainActivity) / sizeof(methods_MainActivity[0]));
    }

    jint JNI_OnLoad(JavaVM *vm, void *reserved) {//这个方法是一个override方法,在加载动态库时,会自动调用,一般用来做一些初始化操作,动态注册的代码就可以写在这
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {//首先需要获取JNIEnv *env指针,因为registerNativeMethod方法会用到
    return JNI_ERR;
    }
    // 注册本地方法(动态注册的关键一步)
    jint result = RegisterNatives(env);
    LOGD("RegisterNatives result: %d", result);
    return JNI_VERSION_1_6;
    }

    }
    RegisterNatives方法解析
    1
    2
    3
    4
    5
    定义: jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

    clazz:指定的类,即 native 方法所属的类
    methods:方法数组,这里需要了解一下 JNINativeMethod 结构体
    nMethods:方法数组的长度

    JNINativeMethod

    1
    2
    3
    4
    5
    typedef struct {
    const char* name; // native 的方法名
    const char* signature; // 方法签名,例如 ()Ljava/lang/String;
    void* fnPtr; // 函数指针
    } JNINativeMethod;

    Android.mk 和 CMake 语法

    参考:
     Android.mk 语法和变量介绍
    CMakeLists.txt 语法介绍与实例演练

    Android Studio 中使用 NDK

       gradle 配置 cmake,及各参数详解

       gardle 配置 ndk 指定 ABI: abiFilters 详解

    APK文件结构

    参考: https://bbs.kanxue.com/thread-278112.htm

    assets文件夹

    assets 这里存放的是静态资源文件(图片,视频等),这个文件夹下的资源文件不会被编译。不被编译的资源文件是指在编译过程中不会被转换成二进制代码的文件,而是直接被打包到最终的程序中。这些文件通常是一些静态资源,如图片、音频、文本文件等。

    lib文件夹

    lib:.so库(c或c++编译的动态链接库)。APK文件中的动态链接库(Dynamic Link Library,简称DLL)是一种可重用的代码库,它包含在应用程序中,以便在运行时被调用。这些库通常包含许多常见的函数和程序,可以在多个应用程序中共享,从而提高了代码的复用性和效率。

    lib文件夹下的每个目录都适用于不同的环境下,armeabi-v7a目录基本通用所有android设备,arm64-v8a目录只适用于64位的android设备,x86目录常见用于android模拟器,x86-64目录适用于支持x86_64架构的Android设备(适用于支持通常称为“x86-64”的指令集的 CPU)

    META-INF文件夹

    META-INF:在Android应用的APK文件中,META-INF文件夹是存放数字签名相关文件的文件夹,包含以下三个文件:

    1. MANIFEST.MF:MANIFEST.MF 是一个摘要清单文件,它包含了 APK 文件中除自身外所有文件的数字摘要。这些摘要通常是通过特定的哈希算法(如 SHA - 1、SHA - 256 等)对文件内容进行计算得到的,用于确保文件内容在传输或存储过程中未被篡改。
    2. CERT.SF:CERT.SF 文件存储了 MANIFEST.MF 文件的数字摘要以及 MANIFEST.MF 中每个文件条目的数字摘要的二次摘要。开发者使用自己的私钥对 CERT.SF 进行签名,以保证 CERT.SF 文件内容的完整性和真实性。
    3. CERT.RSA:CERT.RSA 文件包含了使用开发者私钥对 CERT.SF 文件进行签名得到的数字签名以及签名时所使用的数字证书。当验证 APK 的签名时,系统会使用数字证书中的公钥来验证 CERT.SF 文件的数字签名是否有效,从而确保 CERT.SF 文件未被篡改,进而验证 MANIFEST.MF 文件和整个 APK 的完整性。

    AndroidManifest.xml配置文件

    AndroidManifest.xml是Android应用程序中最重要的文件之一,它包含了应用程序的基本信息,如应用程序的名称、图标、版本号、权限、组件(Activity、Service、BroadcastReceiver、Content Provider)等等。在应用程序运行时,系统会根据这个文件来管理应用程序的生命周期,启动和关闭应用程序,管理应用程序的组件等等。

    我们来了解一下AndroidManifest.xml文件的主要组成部分:

    1. manifest标签

    manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。

    1. application标签

    application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。

    1. activity标签

    activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。

    1. service标签

    service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。

    1. receiver标签

    receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。

    1. provider标签

    provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。

    1. uses-permission标签

    uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。

    1. uses-feature标签

    uses-feature标签定义了应用程序需要的硬件或软件特性,如摄像头、GPS等等。在应用程序安装时,系统会检查设备是否支持这些特性。

    以上是AndroidManifest.xml文件的主要组成部分,它们共同定义了应用程序的基本信息和组件,是应用程序的重要配置文件。现在如果看起来有点懵,没关系,后面实战会使用到它的,以后也会对它进行详解,那时你或许会有一点对它的理解了。

    resources.arsc文件

    resources.arsc文件是Android应用程序的资源文件之一,它是一个二进制文件,包含了应用程序的所有资源信息,例如布局文件、字符串、图片等。这个文件在应用程序编译过程中由aapt工具生成,并被打包进APK文件中。

    resources.arsc文件的主要作用是提供资源的索引和映射关系。它将资源文件名、类型、值等信息映射到一个唯一的整数ID上。这个ID在R文件中定义,并且可以通过代码中的R类来引用。例如,R.layout.main表示布局文件main.xml对应的ID,R.string.app_name表示字符串资源app_name对应的ID。

    当应用程序运行时,系统会根据R类中的ID来查找对应的资源,并将其加载到内存中,供应用程序使用。这个过程是通过解析resources.arsc文件和R类实现的。通过这种方式,应用程序可以方便地访问和使用资源,而不需要手动处理资源文件的位置和命名等问题。

    需要注意的是,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。

    总之,resources.arsc文件是Android应用程序的资源文件之一,包含了资源的索引和映射关系。它和R类一起构成了应用程序访问和使用资源的基础。通过解析resources.arsc文件和使用R类,应用程序可以方便地加载和使用资源。

    参考其他的文章
    Android资源管理及资源的编译和打包过程分析 - 掘金 (juejin.cn)

    (32条消息) 手把手教你解析Resources.arsc_beyond702的博客-CSDN博客

    Android逆向:resource.arsc文件解析(Config List) - 掘金 (juejin.cn)

    (32条消息) resource.arsc二进制内容解析 之 Dynamic package reference_BennuCTech的博客-CSDN博客

    res文件夹

    res:资源文件目录,二进制格式。实际上,APK文件下的res文件夹并不是二进制格式,而是经过编译后的二进制资源文件。在Android应用程序开发中,资源文件通常是以XML格式存储的,如布局文件、字符串资源、颜色资源等。在编译时,Android编译器会将这些XML资源文件编译成二进制格式的资源文件,以提高应用程序的运行效率和安全性。虽然res文件夹下的二进制资源文件不能直接编辑和修改,但是开发者仍然可以通过Android提供的资源管理工具,如aapt、apktool等,来反编译和编辑这些资源文件的。

    在res文件夹中,主要包含以下子文件夹和文件:

    res子目录 存储的资源类型
    animator/ 用于定义属性动画的 XML 文件。
    anim/ 用于定义补间动画的 XML 文件。属性动画也可保存在此目录中,但为了区分这两种类型,属性动画首选 animator/ 目录。
    color/ 定义颜色状态列表的 XML 文件。如需了解详情,请参阅颜色状态列表资源
    drawable/ 位图文件(PNG、.9.png、JPG 或 GIF)或编译为以下可绘制资源子类型的 XML 文件:位图文件九宫图(可调整大小的位图)状态列表形状动画可绘制对象其他可绘制对象如需了解详情,请参阅可绘制资源
    mipmap/ 适用于不同启动器图标密度的可绘制对象文件。如需详细了解如何使用 mipmap/ 文件夹管理启动器图标,请参阅将应用图标放在 mipmap 目录中
    layout/ 用于定义界面布局的 XML 文件。如需了解详情,请参阅布局资源
    menu/ 用于定义应用菜单(例如选项菜单、上下文菜单或子菜单)的 XML 文件。如需了解详情,请参阅菜单资源
    raw/ 需以原始形式保存的任意文件。如要使用原始 InputStream 打开这些资源,请使用资源 ID(即 R.raw.*filename*)调用 Resources.openRawResource()。但是,如需访问原始文件名和文件层次结构,请考虑将资源保存在 assets/ 目录(而非 res/raw/)下。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。
    values/ 包含字符串、整数和颜色等简单值的 XML 文件。其他 res/ 子目录中的 XML 资源文件会根据 XML 文件名定义单个资源,而 values/ 目录中的文件可描述多个资源。对于此目录中的文件,<resources> 元素的每个子元素均会定义一个资源。例如,<string> 元素会创建 R.string 资源,<color> 元素会创建 R.color 资源。由于每个资源均使用自己的 XML 元素进行定义,因此您可以随意命名文件,并在某个文件中放入不同的资源类型。但是,您可能需要将独特的资源类型放在不同的文件中,使其一目了然。例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定:arrays.xml 用于资源数组(类型化数组colors.xml 用于颜色值dimens.xml 用于维度值strings.xml 用于字符串值styles.xml 用于样式如需了解详情,请参阅字符串资源样式资源更多资源类型
    xml/ 可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(例如搜索配置)都必须保存在此处。
    font/ 带有扩展名的字体文件(例如 TTF、OTF 或 TTC),或包含 <font-family> 元素的 XML 文件。如需详细了解以资源形式使用的字体,请参阅将字体添加为 XML 资源

    dex文件结构

    https://cloud.tencent.com/developer/article/1663852
    https://juejin.cn/post/6844903847647772686

    如何将编译dex文件

    1
    ./d8 --debug  --output dex输出路径 class文件

    再来一个简洁的图

    • header : DEX 文件头,记录了一些当前文件的信息以及其他数据结构在文件中的偏移量
    • string_ids : 字符串的偏移量
    • type_ids : 类型信息的偏移量
    • proto_ids : 方法声明的偏移量
    • field_ids : 字段信息的偏移量
    • method_ids : 方法信息(所在类,方法声明以及方法名)的偏移量
    • class_def : 类信息的偏移量
    • data : : 数据区
    • link_data : 静态链接数据区

    从 header 到 data 之间都是偏移量数组,并不存储真实数据,所有数据都存在 data 数据区,根据其偏移量区查找。对 DEX 文件有了一个大概的认识之后,我们就来详细分析一下各个部分。

    DEX 文件头部分的具体格式可以参考 DexFile.h 中的定义:

    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
    struct DexHeader {
    u1 magic[8]; // 魔数
    u4 checksum; // adler 校验值
    u1 signature[kSHA1DigestLen]; // sha1 校验值
    u4 fileSize; // DEX 文件大小
    u4 headerSize; // DEX 文件头大小
    u4 endianTag; // 字节序
    u4 linkSize; // 链接段大小
    u4 linkOff; // 链接段的偏移量
    u4 mapOff; // DexMapList 偏移量
    u4 stringIdsSize; // DexStringId 个数
    u4 stringIdsOff; // DexStringId 偏移量
    u4 typeIdsSize; // DexTypeId 个数
    u4 typeIdsOff; // DexTypeId 偏移量
    u4 protoIdsSize; // DexProtoId 个数
    u4 protoIdsOff; // DexProtoId 偏移量
    u4 fieldIdsSize; // DexFieldId 个数
    u4 fieldIdsOff; // DexFieldId 偏移量
    u4 methodIdsSize; // DexMethodId 个数
    u4 methodIdsOff; // DexMethodId 偏移量
    u4 classDefsSize; // DexCLassDef 个数
    u4 classDefsOff; // DexClassDef 偏移量
    u4 dataSize; // 数据段大小
    u4 dataOff; // 数据段偏移量
    };

    magic 一般是常量,用来标记 DEX 文件,它可以分解为:

    1
    文件标识 dex + 换行符 + DEX 版本 + 0

    字符串格式为 dex\n035\0,十六进制为 0x6465780A30333500

    checksum 是对去除 magicchecksum 以外的文件部分作 alder32 算法得到的校验值,用于判断 DEX 文件是否被篡改。

    signature 是对除去 magicchecksumsignature 以外的文件部分作 sha1 得到的文件哈希值。

    endianTag 用于标记 DEX 文件是大端表示还是小端表示。由于 DEX 文件是运行在 Android 系统中的,所以一般都是小端表示,这个值也是恒定值 0x12345678

    string_ids

    string_ids 是一个表,保存了 所有字符串的引用地址

    1
    2
    3
    struct DexStringId {
    u4 stringDataOff;
    };

    先来写一个工具类后面的也都用这个工具类

    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
    // 从字节数组中读取 4 字节 little-endian 整数
    public int readInt(byte[] data, int offset) {
    return ((data[offset] & 0xFF)) |
    ((data[offset + 1] & 0xFF) << 8) |
    ((data[offset + 2] & 0xFF) << 16) |
    ((data[offset + 3] & 0xFF) << 24);
    }

    // 从指定 offset 位置开始读取一个 ULEB128 编码的整数
    // 返回值:[整数值,占用字节数]
    public int[] readUleb128(byte[] data, int offset) {
    int result = 0; // 解析结果
    int count = 0; // ULEB128 占用的字节数
    int cur;
    int shift = 0;

    do {
    cur = data[offset + count] & 0xFF; // 当前字节
    result |= (cur & 0x7F) << shift; // 去掉最高位后累加到结果
    shift += 7;
    count++;
    } while ((cur & 0x80) != 0); // 如果最高位为1,继续读下一个字节

    return new int[]{result, count};
    }

    // 从 offset 开始读取 UTF-8 编码的字符串,直到遇到 \0 结束
    public String readString(byte[] data, int offset) {
    int end = offset;
    while (data[end] != 0) end++; // 找到 null terminator
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }

    解析

    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
    // 解析 DEX 文件中的 string_ids 表
    public void parseStringIds(byte[] dexData) {
    // 从 DEX 文件头偏移 0x38 读取 string_ids_size(字符串数量)
    int stringIdsSize = readInt(dexData, 0x38);

    // 从偏移 0x3C 读取 string_ids_off(string_id 表的起始位置)
    int stringIdsOff = readInt(dexData, 0x3C);

    System.out.println("Total strings: " + stringIdsSize);

    // 遍历所有 string_id_item
    for (int i = 0; i < stringIdsSize; i++) {
    // 每个 string_id 占 4 字节,表示 string_data_item 的偏移地址
    int stringDataOff = readInt(dexData, stringIdsOff + i * 4);

    // 读取 ULEB128 编码的字符串长度(字符数量,不是字节数),并获得该编码占用的字节数
    int[] result = readUleb128(dexData, stringDataOff);
    int utf16Size = result[0]; // 实际字符数(通常你可以忽略它)
    int stringOffset = stringDataOff + result[1]; // 字符串真实内容的起始位置

    // 从 offset 开始读取 UTF-8 编码的字符串内容(直到遇到 0x00 为止)
    String value = readString(dexData, stringOffset);

    // 输出解析到的字符串
    System.out.printf("string[%d] = %s\n", i, value);
    }
    }

    type_ids

    1
    2
    3
    struct DexTypeId {
    u4 descriptorIdx;
    };

    type_ids 表示的是类型信息,descriptorIdx 指向 string_ids 中元素。根据索引直接在上一步读取到的字符串池即可解析对应的类型信息,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    int typeIdsSize = Utils.readInt(dexData, 0x40);
    int typeIdsOff = Utils.readInt(dexData, 0x44);
    for (int i = 0; i < typeIdsSize; i++) {
    int stringIndex = Utils.readInt(dexData, typeIdsOff + i * 4); // 获取 type_id 对应的 string_ids 索引

    // 接下来就是去 string_ids 表中找到那个 string_data_off
    int stringIdsOff = Utils.readInt(dexData, 0x3C); // string_ids 的起始地址
    int stringDataOff = Utils.readInt(dexData, stringIdsOff + stringIndex * 4); // 找到该字符串的偏移

    // 读取实际字符串内容(记得是 ULEB128 编码)
    int[] result = Utils.readUleb128(dexData, stringDataOff);
    int size = result[0];
    int offset = stringDataOff + result[1];
    String typeString = Utils.readString(dexData, offset); // 实际读取字符串内容
    System.out.println("type[" + i + "] = " + typeString);
    }

    proto_ids

    1
    2
    3
    4
    5
    struct DexProtoId {
    u4 shortyIdx; /* index into stringIds for shorty descriptor */
    u4 returnTypeIdx; /* index into typeIds list for return type */
    u4 parametersOff; /* file offset to type_list for parameter types */
    };

    proto_ids 表示方法声明信息,它包含以下三个变量:

    • shortyIdx : 指向 string_ids ,表示方法声明的字符串
    • returnTypeIdx : 指向 type_ids ,表示方法的返回类型
    • parametersOff : 方法参数列表的偏移量
      方法参数列表的数据结构在 DexFile.h 中用 DexTypeList 来表示:
    1
    2
    3
    4
    5
    6
    7
    8
    struct DexTypeList {
    u4 size; /* #of entries in list */
    DexTypeItem list[1]; /* entries */
    };

    struct DexTypeItem {
    u2 typeIdx; /* index into typeIds */
    };

    size 表示方法参数的个数,参数用 DexTypeItem 表示,它只有一个属性 typeIdx,指向 type_ids 中对应项。

    解析

    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
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;

    public class DexProtoParser {

    public static void main(String[] args) throws IOException {
    // 读取整个 dex 文件内容到 byte[] 中
    FileInputStream fis = new FileInputStream("your.dex"); // 修改为实际路径
    byte[] dexData = fis.readAllBytes();
    fis.close();

    // 调用解析方法
    parseProtoIds(dexData);
    }

    // 解析 proto_ids 表
    public static void parseProtoIds(byte[] dexData) {
    // 从 header 中读取 proto_ids 的数量和偏移地址
    int protoIdsSize = readInt(dexData, 0x44); // proto_ids_size
    int protoIdsOff = readInt(dexData, 0x48); // proto_ids_off

    System.out.println("Total proto_ids: " + protoIdsSize);

    for (int i = 0; i < protoIdsSize; i++) {
    // 每个 proto_id 项固定占 12 字节
    int base = protoIdsOff + i * 12;

    int shortyIdx = readInt(dexData, base); // 指向 string_ids 表,用于描述方法签名(shorty)
    int returnTypeIdx = readInt(dexData, base + 4); // 指向 type_ids,表示返回类型
    int parametersOff = readInt(dexData, base + 8); // 偏移到参数类型表(type_list)

    // 获取字符串表示
    String shorty = getStringById(dexData, shortyIdx);
    String returnType = getTypeString(dexData, returnTypeIdx);
    String[] params = getParamTypeList(dexData, parametersOff);

    // 打印解析结果
    System.out.printf("proto[%d]: shorty=%s, return=%s, params=%s\n",
    i, shorty, returnType, String.join(", ", params));
    }
    }

    // 从 byte[] 中读取一个小端 4 字节整数
    public static int readInt(byte[] data, int offset) {
    return ((data[offset] & 0xFF)) |
    ((data[offset + 1] & 0xFF) << 8) |
    ((data[offset + 2] & 0xFF) << 16) |
    ((data[offset + 3] & 0xFF) << 24);
    }

    // 读取 ULEB128(可变长度整型编码),返回:[值,占用字节数]
    public static int[] readUleb128(byte[] data, int offset) {
    int result = 0;
    int count = 0;
    int shift = 0;
    int b;

    do {
    b = data[offset + count] & 0xFF;
    result |= (b & 0x7F) << shift;
    shift += 7;
    count++;
    } while ((b & 0x80) != 0);

    return new int[]{result, count};
    }

    // 读取字符串:string_ids → string_data_item → UTF-8字符串
    public static String getStringById(byte[] data, int stringId) {
    // string_ids_off 存储在 header 的 0x3C 处
    int stringIdsOff = readInt(data, 0x3C);

    // 每个 string_id 占 4 字节,值是 string_data_item 的偏移地址
    int stringDataOff = readInt(data, stringIdsOff + stringId * 4);

    // 跳过前面的 uleb128(表示 utf16 字符长度),读取 UTF-8 内容
    int[] result = readUleb128(data, stringDataOff);
    int contentOffset = stringDataOff + result[1];

    return readString(data, contentOffset);
    }

    // 从 type_ids 中获取类型描述字符串(例如 Ljava/lang/String;)
    public static String getTypeString(byte[] data, int typeIdx) {
    int typeIdsOff = readInt(data, 0x40); // header 中的 type_ids_off
    int descriptorIdx = readInt(data, typeIdsOff + typeIdx * 4); // 指向 string_ids
    return getStringById(data, descriptorIdx);
    }

    // 读取 type_list(参数类型列表)
    public static String[] getParamTypeList(byte[] data, int parametersOff) {
    if (parametersOff == 0) return new String[0]; // 没有参数

    // 读取参数数量(4字节)
    int size = readInt(data, parametersOff);
    String[] types = new String[size];

    // 每个参数类型索引占 2 字节(type_idx)
    for (int i = 0; i < size; i++) {
    int typeIdx = ((data[parametersOff + 4 + i * 2] & 0xFF)
    | ((data[parametersOff + 4 + i * 2 + 1] & 0xFF) << 8));
    types[i] = getTypeString(data, typeIdx);
    }

    return types;
    }

    // 读取 null 结尾的 UTF-8 字符串
    public static String readString(byte[] data, int offset) {
    int end = offset;
    while (end < data.length && data[end] != 0) {
    end++;
    }
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }
    }

    field_ids

    1
    2
    3
    4
    5
    struct DexFieldId {
    u2 classIdx; /* index into typeIds list for defining class */
    u2 typeIdx; /* index into typeIds for field type */
    u4 nameIdx; /* index into stringIds for field name */
    };

    field_ids 表示的是字段信息,指明了字段所在的类,字段的类型以及字段名称,在 DexFile.h 中定义为 DexFieldId , 其各个字段含义如下:

    • classIdx : 指向 type_ids ,表示字段所在类的信息
    • typeIdx : 指向 ype_ids ,表示字段的类型信息
    • nameIdx : 指向 string_ids ,表示字段名称
    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
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;

    public class DexFieldParser {

    public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("your.dex"); // 替换成实际 dex 路径
    byte[] dexData = fis.readAllBytes();
    fis.close();

    parseFieldIds(dexData);
    }

    public static void parseFieldIds(byte[] dexData) {
    // 读取 header 中 field_ids 的数量和偏移
    int fieldIdsSize = readInt(dexData, 0x58); // field_ids_size
    int fieldIdsOff = readInt(dexData, 0x5C); // field_ids_off

    System.out.println("Total field_ids: " + fieldIdsSize);

    for (int i = 0; i < fieldIdsSize; i++) {
    int base = fieldIdsOff + i * 8;

    int classIdx = readU2(dexData, base); // 指向 declaring class (type_ids 索引)
    int typeIdx = readU2(dexData, base + 2); // 指向 field type (type_ids 索引)
    int nameIdx = readInt(dexData, base + 4); // 指向 field name (string_ids 索引)

    String classType = getTypeString(dexData, classIdx);
    String fieldType = getTypeString(dexData, typeIdx);
    String fieldName = getStringById(dexData, nameIdx);

    System.out.printf("field[%d]: class=%s, type=%s, name=%s\n", i, classType, fieldType, fieldName);
    }
    }

    // 读取小端 4 字节整数
    public static int readInt(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8)
    | ((data[offset + 2] & 0xFF) << 16)
    | ((data[offset + 3] & 0xFF) << 24);
    }

    // 读取小端 2 字节整数(U2)
    public static int readU2(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8);
    }

    // 读取 string_id → string_data → UTF-8 字符串
    public static String getStringById(byte[] data, int stringId) {
    int stringIdsOff = readInt(data, 0x3C); // string_ids_off
    int stringDataOff = readInt(data, stringIdsOff + stringId * 4);

    int[] uleb = readUleb128(data, stringDataOff);
    int stringOffset = stringDataOff + uleb[1]; // 跳过 uleb128 的字符长度
    return readString(data, stringOffset);
    }

    // 读取 type_id → string_id → string_data → UTF-8 类型字符串
    public static String getTypeString(byte[] data, int typeId) {
    int typeIdsOff = readInt(data, 0x40);
    int descriptorIdx = readInt(data, typeIdsOff + typeId * 4);
    return getStringById(data, descriptorIdx);
    }

    // 读取 null 结尾 UTF-8 字符串
    public static String readString(byte[] data, int offset) {
    int end = offset;
    while (end < data.length && data[end] != 0) {
    end++;
    }
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }

    // 读取 ULEB128 编码,返回:值 + 占用字节数
    public static int[] readUleb128(byte[] data, int offset) {
    int result = 0;
    int count = 0;
    int shift = 0;
    int b;

    do {
    b = data[offset + count] & 0xFF;
    result |= (b & 0x7F) << shift;
    shift += 7;
    count++;
    } while ((b & 0x80) != 0);

    return new int[]{result, count};
    }
    }

    method_ids

    1
    2
    3
    4
    5
    struct DexMethodId {
    u2 classIdx; /* index into typeIds list for defining class */
    u2 protoIdx; /* index into protoIds for method prototype */
    u4 nameIdx; /* index into stringIds for method name */
    };

    method_ids 指明了方法所在的类、方法声明以及方法名。在 DexFile.h 中用 DexMethodId 表示该项,其属性含义如下:

    • classIdx : 指向 type_ids ,表示类的类型
    • protoIdx : 指向 type_ids ,表示方法声明
    • nameIdx : 指向 string_ids ,表示方法名
    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
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;

    public class DexMethodParser {

    public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("your.dex"); // 替换为实际 dex 文件路径
    byte[] dexData = fis.readAllBytes();
    fis.close();

    parseMethodIds(dexData);
    }

    public static void parseMethodIds(byte[] dexData) {
    int methodIdsSize = readInt(dexData, 0x60); // method_ids_size 偏移为 0x60
    int methodIdsOff = readInt(dexData, 0x64); // method_ids_off 偏移为 0x64

    System.out.println("Total method_ids: " + methodIdsSize);

    for (int i = 0; i < methodIdsSize; i++) {
    int base = methodIdsOff + i * 8;

    int classIdx = readU2(dexData, base); // method 所属类,type_ids 的索引
    int protoIdx = readU2(dexData, base + 2); // 方法的签名结构,proto_ids 的索引
    int nameIdx = readInt(dexData, base + 4); // 方法名字符串,string_ids 的索引

    String classType = getTypeString(dexData, classIdx);
    String methodName = getStringById(dexData, nameIdx);
    String protoDesc = getProtoString(dexData, protoIdx);

    System.out.printf("method[%d]: class=%s, proto=%s, name=%s\n", i, classType, protoDesc, methodName);
    }
    }

    // ----------- 以下是通用辅助方法 ------------

    public static int readInt(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8)
    | ((data[offset + 2] & 0xFF) << 16)
    | ((data[offset + 3] & 0xFF) << 24);
    }

    public static int readU2(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8);
    }

    public static String getStringById(byte[] data, int stringId) {
    int stringIdsOff = readInt(data, 0x3C); // string_ids_off
    int stringDataOff = readInt(data, stringIdsOff + stringId * 4);

    int[] uleb = readUleb128(data, stringDataOff);
    int stringOffset = stringDataOff + uleb[1];
    return readString(data, stringOffset);
    }

    public static String getTypeString(byte[] data, int typeId) {
    int typeIdsOff = readInt(data, 0x40);
    int descriptorIdx = readInt(data, typeIdsOff + typeId * 4);
    return getStringById(data, descriptorIdx);
    }

    public static String getProtoString(byte[] data, int protoId) {
    int protoIdsOff = readInt(data, 0x44);
    int base = protoIdsOff + protoId * 12;

    int shortyIdx = readInt(data, base);
    int returnTypeIdx = readInt(data, base + 4);
    int parametersOff = readInt(data, base + 8);

    String returnType = getTypeString(data, returnTypeIdx);
    StringBuilder params = new StringBuilder();

    if (parametersOff != 0) {
    int size = readInt(data, parametersOff);
    for (int i = 0; i < size; i++) {
    int typeIdx = readU2(data, parametersOff + 4 + i * 2);
    String typeStr = getTypeString(data, typeIdx);
    params.append(typeStr);
    if (i != size - 1) {
    params.append(", ");
    }
    }
    }

    return "(" + params + ") → " + returnType;
    }

    public static String readString(byte[] data, int offset) {
    int end = offset;
    while (end < data.length && data[end] != 0) {
    end++;
    }
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }

    public static int[] readUleb128(byte[] data, int offset) {
    int result = 0;
    int count = 0;
    int shift = 0;
    int b;
    do {
    b = data[offset + count] & 0xFF;
    result |= (b & 0x7F) << shift;
    shift += 7;
    count++;
    } while ((b & 0x80) != 0);
    return new int[]{result, count};
    }
    }

    class_def

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct DexClassDef {
    u4 classIdx; /* index into typeIds for this class */
    u4 accessFlags;
    u4 superclassIdx; /* index into typeIds for superclass */
    u4 interfacesOff; /* file offset to DexTypeList */
    u4 sourceFileIdx; /* index into stringIds for source file name */
    u4 annotationsOff; /* file offset to annotations_directory_item */
    u4 classDataOff; /* file offset to class_data_item */
    u4 staticValuesOff; /* file offset to DexEncodedArray */
    };

    class_def 是 DEX 文件结构中最复杂也是最核心的部分,它表示了类的所有信息,对应 DexFile.h 中的 DexClassDef :

    • classIdx : 指向 type_ids ,表示类信息
    • accessFlags : 访问标识符
    • superclassIdx : 指向 type_ids ,表示父类信息
    • interfacesOff : 指向 DexTypeList 的偏移量,表示接口信息
    • sourceFileIdx : 指向 string_ids ,表示源文件名称
    • annotationOff : 注解信息
    • classDataOff : 指向 DexClassData 的偏移量,表示类的数据部分
    • staticValueOff :指向 DexEncodedArray 的偏移量,表示类的静态数据
    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
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.nio.charset.StandardCharsets;

    public class DexClassDefsParser {

    public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream("your.dex"); // 将 "your.dex" 替换为你的 dex 文件路径
    byte[] dexData = fis.readAllBytes(); // 读取整个 dex 文件为字节数组
    fis.close();

    parseClassDefs(dexData);
    }

    public static void parseClassDefs(byte[] dexData) {
    // 读取 class_defs_size(类定义数量),偏移地址 0x70
    int classDefsSize = readInt(dexData, 0x70);
    // 读取 class_defs_off(类定义开始偏移),偏移地址 0x74
    int classDefsOff = readInt(dexData, 0x74);

    System.out.println("Total class_defs: " + classDefsSize);

    // 遍历每一个 class_def 项,每项固定 32 字节
    for (int i = 0; i < classDefsSize; i++) {
    int base = classDefsOff + i * 32;

    int classIdx = readInt(dexData, base); // 当前类在 type_ids 中的索引
    int accessFlags = readInt(dexData, base + 4); // 访问标志(public、final 等)
    int superClassIdx = readInt(dexData, base + 8); // 父类索引(type_ids)
    int interfacesOff = readInt(dexData, base + 12); // 实现的接口偏移(指向 type_list)
    int sourceFileIdx = readInt(dexData, base + 16); // 源文件名在 string_ids 中的索引
    int annotationsOff = readInt(dexData, base + 20);// 注解偏移
    int classDataOff = readInt(dexData, base + 24); // 类字段/方法数据偏移
    int staticValuesOff = readInt(dexData, base + 28);// 静态变量初始值偏移

    // 获取类名、父类名和源文件名字符串
    String className = getTypeString(dexData, classIdx);
    String superClassName = getTypeString(dexData, superClassIdx);
    String sourceFile = getStringById(dexData, sourceFileIdx);

    System.out.printf("class[%d]: %s extends %s from [%s], class_data_off=0x%x\n",
    i, className, superClassName, sourceFile, classDataOff);
    }
    }

    // ------------------ 工具方法部分 ------------------

    // 从指定偏移读取 4 字节,按小端格式转换为 int
    public static int readInt(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8)
    | ((data[offset + 2] & 0xFF) << 16)
    | ((data[offset + 3] & 0xFF) << 24);
    }

    // 根据 type_id 取出对应的类型字符串(如 Ljava/lang/String;)
    public static String getTypeString(byte[] data, int typeId) {
    if (typeId < 0) return "null";
    int typeIdsOff = readInt(data, 0x40); // type_ids 起始偏移,0x40 位置
    int descriptorIdx = readInt(data, typeIdsOff + typeId * 4); // 每个 type_id 对应一个 string_id
    return getStringById(data, descriptorIdx);
    }

    // 根据 string_id 取出对应的字符串内容
    public static String getStringById(byte[] data, int stringId) {
    if (stringId < 0) return "null";
    int stringIdsOff = readInt(data, 0x3C); // string_ids 起始偏移,0x3C 位置
    int stringDataOff = readInt(data, stringIdsOff + stringId * 4); // 获取该字符串的偏移
    int[] uleb = readUleb128(data, stringDataOff); // 前缀是 ULEB128 格式的字符串长度
    int offset = stringDataOff + uleb[1]; // 字符串实际数据起始位置
    return readString(data, offset);
    }

    // 从 offset 开始读取一个以 0 结尾的字符串(UTF-8 编码)
    public static String readString(byte[] data, int offset) {
    int end = offset;
    while (end < data.length && data[end] != 0) end++;
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }

    // 读取一个 ULEB128 编码整数,返回 [值, 所占字节数]
    public static int[] readUleb128(byte[] data, int offset) {
    int result = 0;
    int count = 0;
    int shift = 0;
    int b;
    do {
    b = data[offset + count] & 0xFF;
    result |= (b & 0x7F) << shift;
    shift += 7;
    count++;
    } while ((b & 0x80) != 0);
    return new int[]{result, count};
    }
    }

    DefCLassData

    重点是 classDataOff 这个字段,它包含了一个类的核心数据,在 Android 源码中定义为 DexClassData ,它不在 DexFile.h 中了,而是在 DexClass.h 中:

    1
    2
    3
    4
    5
    6
    7
    struct DexClassData {
    DexClassDataHeader header;
    DexField* staticFields;
    DexField* instanceFields;
    DexMethod* directMethods;
    DexMethod* virtualMethods;
    };

    DexClassDataHeader 定义了类中字段和方法的数目,它也定义在 DexClass.h 中:

    1
    2
    3
    4
    5
    6
    struct DexClassDataHeader {
    u4 staticFieldsSize;
    u4 instanceFieldsSize;
    u4 directMethodsSize;
    u4 virtualMethodsSize;
    };
    • staticFieldsSize : 静态字段个数
    • instanceFieldsSize : 实例字段个数
    • directMethodsSize : 直接方法个数
    • virtualMethodsSize : 虚方法个数
    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
    public class DexClassDataParser {

    public static void parseClassData(byte[] dexData, int classDataOff) {
    // 解析 class_data_item(32 字节结构)
    int staticFieldsSize = readUleb128(dexData, classDataOff)[0]; // 静态字段数量
    int instanceFieldsSize = readUleb128(dexData, classDataOff + 1)[0]; // 实例字段数量
    int directMethodsSize = readUleb128(dexData, classDataOff + 2)[0]; // 直接方法数量
    int virtualMethodsSize = readUleb128(dexData, classDataOff + 3)[0]; // 虚拟方法数量

    // 打印解析出来的字段数量信息
    System.out.println("Static Fields: " + staticFieldsSize);
    System.out.println("Instance Fields: " + instanceFieldsSize);
    System.out.println("Direct Methods: " + directMethodsSize);
    System.out.println("Virtual Methods: " + virtualMethodsSize);

    // 静态字段解析
    int currentOffset = classDataOff + 4;
    for (int i = 0; i < staticFieldsSize; i++) {
    int fieldId = readUleb128(dexData, currentOffset)[0]; // 获取字段 ID
    String fieldName = getFieldName(dexData, fieldId);
    System.out.println("Static Field [" + i + "]: " + fieldName);
    currentOffset += 1; // 偏移量调整
    }

    // 实例字段解析
    for (int i = 0; i < instanceFieldsSize; i++) {
    int fieldId = readUleb128(dexData, currentOffset)[0]; // 获取字段 ID
    String fieldName = getFieldName(dexData, fieldId);
    System.out.println("Instance Field [" + i + "]: " + fieldName);
    currentOffset += 1; // 偏移量调整
    }

    // 直接方法解析
    for (int i = 0; i < directMethodsSize; i++) {
    int methodId = readUleb128(dexData, currentOffset)[0]; // 获取方法 ID
    String methodName = getMethodName(dexData, methodId);
    System.out.println("Direct Method [" + i + "]: " + methodName);
    currentOffset += 1; // 偏移量调整
    }

    // 虚拟方法解析
    for (int i = 0; i < virtualMethodsSize; i++) {
    int methodId = readUleb128(dexData, currentOffset)[0]; // 获取方法 ID
    String methodName = getMethodName(dexData, methodId);
    System.out.println("Virtual Method [" + i + "]: " + methodName);
    currentOffset += 1; // 偏移量调整
    }
    }

    // ------------------ 工具方法部分 ------------------

    // 读取一个 ULEB128 编码整数,返回 [值, 所占字节数]
    public static int[] readUleb128(byte[] data, int offset) {
    int result = 0;
    int count = 0;
    int shift = 0;
    int b;
    do {
    b = data[offset + count] & 0xFF;
    result |= (b & 0x7F) << shift;
    shift += 7;
    count++;
    } while ((b & 0x80) != 0);
    return new int[]{result, count};
    }

    // 根据字段 ID 获取字段名称
    public static String getFieldName(byte[] dexData, int fieldId) {
    int fieldIdsOff = readInt(dexData, 0x80); // 获取 field_ids 的偏移
    int fieldIdOffset = readInt(dexData, fieldIdsOff + fieldId * 4);
    return getStringById(dexData, fieldIdOffset);
    }

    // 根据方法 ID 获取方法名称
    public static String getMethodName(byte[] dexData, int methodId) {
    int methodIdsOff = readInt(dexData, 0x84); // 获取 method_ids 的偏移
    int methodIdOffset = readInt(dexData, methodIdsOff + methodId * 4);
    return getStringById(dexData, methodIdOffset);
    }

    // 从指定偏移读取 4 字节,按小端格式转换为 int
    public static int readInt(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8)
    | ((data[offset + 2] & 0xFF) << 16)
    | ((data[offset + 3] & 0xFF) << 24);
    }

    // 根据 string_id 获取字符串内容
    public static String getStringById(byte[] data, int stringId) {
    if (stringId < 0) return "null";
    int stringIdsOff = readInt(data, 0x3C); // string_ids 起始偏移,0x3C 位置
    int stringDataOff = readInt(data, stringIdsOff + stringId * 4); // 获取该字符串的偏移
    return readString(data, stringDataOff);
    }

    // 从 offset 开始读取一个以 0 结尾的字符串(UTF-8 编码)
    public static String readString(byte[] data, int offset) {
    int end = offset;
    while (end < data.length && data[end] != 0) end++;
    return new String(data, offset, end - offset, StandardCharsets.UTF_8);
    }
    }

    继续回到 DexClassData 中来。header 部分定义了各种字段和方法的个数,后面跟着的分别就是 静态字段 、实例字段 、直接方法 、虚方法 的具体数据了。字段用 DexField 表示,方法用 DexMethod 表示。

    DexField

    1
    2
    3
    4
    struct DexField {
    u4 fieldIdx; /* index to a field_id_item */
    u4 accessFlags;
    };
    • fieldIdx : 指向 field_ids ,表示字段信息
    • accessFlags :访问标识符

    DexMethod

    1
    2
    3
    4
    5
    struct DexMethod {
    u4 methodIdx; /* index to a method_id_item */
    u4 accessFlags;
    u4 codeOff; /* file offset to a code_item */
    46};

    method_idx 是指向 method_ids 的索引,表示方法信息。
    accessFlags 是该方法的访问标识符。codeOff 是结构体 DexCode 的偏移量

    DexCode

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    struct DexCode {
    u2 registersSize; // 寄存器个数
    u2 insSize; // 参数的个数
    u2 outsSize; // 调用其他方法时使用的寄存器个数
    u2 triesSize; // try/catch 语句个数
    u4 debugInfoOff; // debug 信息的偏移量
    u4 insnsSize; // 指令集的个数
    u2 insns[1]; // 指令集
    /* followed by optional u2 padding */ // 2 字节,用于对齐
    /* followed by try_item[triesSize] */
    /* followed by uleb128 handlersSize */
    /* followed by catch_handler_item[handlersSize] */
    };
    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
    public class DexCodeParser {

    public static void parseDexCode(byte[] dexData, int codeOffset) {
    // 解析寄存器数量
    int registersSize = readShort(dexData, codeOffset);

    // 解析输入指令数量
    int insSize = readShort(dexData, codeOffset + 2);

    // 解析输出指令数量
    int outsSize = readShort(dexData, codeOffset + 4);

    // 解析异常处理块数量
    int triesSize = readShort(dexData, codeOffset + 6);

    // 解析调试信息的偏移
    int debugInfoOffset = readInt(dexData, codeOffset + 8);

    // 解析指令集大小
    int insnsSize = readInt(dexData, codeOffset + 12);

    // 解析字节码指令
    byte[] insns = new byte[insnsSize];
    System.arraycopy(dexData, codeOffset + 16, insns, 0, insnsSize);

    // 打印解析的字节码信息
    System.out.println("Registers Size: " + registersSize);
    System.out.println("Ins Size: " + insSize);
    System.out.println("Outs Size: " + outsSize);
    System.out.println("Tries Size: " + triesSize);
    System.out.println("Debug Info Offset: " + debugInfoOffset);
    System.out.println("Insns Size: " + insnsSize);

    // 解析字节码指令
    for (int i = 0; i < insnsSize; i++) {
    System.out.printf("0x%02X ", insns[i]);
    if ((i + 1) % 16 == 0) {
    System.out.println();
    }
    }
    System.out.println();
    }

    // 读取 2 字节的小端数据
    public static int readShort(byte[] data, int offset) {
    return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
    }

    // 读取 4 字节的小端数据
    public static int readInt(byte[] data, int offset) {
    return (data[offset] & 0xFF)
    | ((data[offset + 1] & 0xFF) << 8)
    | ((data[offset + 2] & 0xFF) << 16)
    | ((data[offset + 3] & 0xFF) << 24);
    }
    }

    ELF文件结构

    参考: https://ctf-wiki.org/executable/elf/structure/basic-info/
    https://mp.weixin.qq.com/s/aDbrS_PE_i8Xt7-zZe2v9A

    ELF Header

    ELF Header 描述了 ELF 文件的概要信息,利用这个数据结构可以索引到 ELF 文件的全部信息,数据结构如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    typedef struct {
    unsigned char e_ident[16]; // 魔数,标识为 ELF
    uint16_t e_type; // 文件类型(可执行文件、目标文件等)
    uint16_t e_machine; // 架构(如 x86)
    uint32_t e_version;
    uint64_t e_entry; // 程序入口地址
    uint64_t e_phoff; // 程序头表偏移
    uint64_t e_shoff; // 节区头表偏移
    uint32_t e_flags;
    uint16_t e_ehsize; // ELF Header 大小
    uint16_t e_phentsize; // 每个 Program Header 大小
    uint16_t e_phnum; // Program Header 数量
    uint16_t e_shentsize; // 每个 Section Header 大小
    uint16_t e_shnum; // Section Header 数量
    uint16_t e_shstrndx; // 字符串表在 Section Header 表中的索引
    } Elf64_Ehdr;

    Program Header

    描述进程运行时如何加载 ELF 文件,比如 .text.data 段的位置和权限等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef struct {
    uint32_t p_type; // 段类型
    uint32_t p_flags; // 段权限(rwx)
    uint64_t p_offset; // 在文件中的偏移
    uint64_t p_vaddr; // 在内存中的虚拟地址
    uint64_t p_paddr; // 物理地址
    uint64_t p_filesz; // 文件中该段的大小
    uint64_t p_memsz; // 内存中该段的大小
    uint64_t p_align; // 对齐方式
    } Elf64_Phdr;

    Base Address

    要计算基地址,首先要确定可加载段中 p_vaddr 最小的内存虚拟地址,之后把该内存虚拟地址缩小为与之最近的最大页面的整数倍即是基地址。根据要加载到内存中的文件的类型,内存地址可能与 p_vaddr 相同也可能不同。

    1.在 ELF 的 Program Header 表中查找 p_type == PT_LOAD 的段。

    2.确定 最小的 p_vaddr

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Elf64_Addr min_vaddr = -1;
    for (int i = 0; i < ehdr.e_phnum; ++i) {
    if (phdr[i].p_type == PT_LOAD) {
    if (phdr[i].p_vaddr < min_vaddr || min_vaddr == -1) {
    min_vaddr = phdr[i].p_vaddr;
    }
    }
    }

    3.向下对齐(align)这个最小地址到页边界 一般都是 0x1000(4096)字节

    1
    2
    #define PAGE_SIZE 0x1000
    Elf64_Addr base_vaddr = min_vaddr & ~(PAGE_SIZE - 1);

    4.在动态链接库(.so)或 PIE 文件(ET_DYN 类型)中:

    • 内核会将段加载到任意合适的内存地址上,比如:0x7fabc00000
    • 这个实际加载地址 - base_vaddr 就是我们要补偿的基地址(通常叫 base_address
    1
    real_address = base_address + p_vaddr;

    如果是非 PIE 的 ET_EXEC 类型,通常 base_address == 0,也就不需要做偏移调整。

    p_type

    宏定义 含义
    0 PT_NULL 忽略
    1 PT_LOAD 可加载段(程序运行所需)
    2 PT_DYNAMIC 动态链接信息段
    3 PT_INTERP 解释器路径(比如 /lib/ld-linux.so.2
    4 PT_NOTE 各类注解
    5 PT_SHLIB 保留
    6 PT_PHDR 程序头自身所在段
    0x6474e550 PT_GNU_EH_FRAME GCC 的异常处理帧表
    0x6474e551 PT_GNU_STACK 栈权限设定段(比如是否允许栈可执行)

    Section Header

    描述每个节的名字、类型、偏移、大小等,比如 .text.data.symtab.strtab

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct {
    uint32_t sh_name; // 节名称的字符串表索引
    uint32_t sh_type; // 节的类型
    uint64_t sh_flags; // 标志(如可执行、可写)
    uint64_t sh_addr; // 节的地址
    uint64_t sh_offset; // 节在文件中的偏移
    uint64_t sh_size; // 节大小
    uint32_t sh_link; // 与其他节的关联
    uint32_t sh_info; // 附加信息
    uint64_t sh_addralign; // 对齐
    uint64_t sh_entsize; // 如果是表格,单项大小
    } Elf64_Shdr;

    详细解释

    字段名 说明
    sh_name 指向节名称字符串表(.shstrtab)的偏移,表示节的名字
    sh_type 节的类型,常见类型见下方
    sh_flags 节的属性,比如可写、可执行、可加载(SHF_WRITE, SHF_EXECINSTR, SHF_ALLOC
    sh_addr 该节在内存中的地址(如果可加载,否则为 0)
    sh_offset 该节在文件中的偏移(用于读取节内容)
    sh_size 节的字节大小
    sh_link 依节类型而定,例如 .symtab 的 link 指向 .strtab(字符串表)
    sh_info 附加信息,如 .symtab 中表示本地符号数量
    sh_addralign 对齐值(2 的幂),决定节在内存/文件中对齐边界
    sh_entsize 如果节是一个“表”,这个字段表示每项的大小(如 .symtab 中一个符号的大小)

    sh_type

    名称 说明
    0 SHT_NULL 无效节头(第一个节保留)
    1 SHT_PROGBITS 普通数据,比如 .text, .data
    2 SHT_SYMTAB 符号表
    3 SHT_STRTAB 字符串表
    4 SHT_RELA 有符号重定位表
    5 SHT_HASH 哈希表
    6 SHT_DYNAMIC 动态链接信息表
    7 SHT_NOTE 标注节(比如编译器信息)
    8 SHT_NOBITS 不占文件空间的节(如 .bss
    9 SHT_REL 无偏移重定位表
    0x0b SHT_DYNSYM 动态符号表

    sh_flags

    标志名 说明
    SHF_WRITE 0x1 节可写
    SHF_ALLOC 0x2 节会被加载进内存
    SHF_EXECINSTR 0x4 节包含可执行指令
    SHF_MERGE 0x10 内容可以合并(字符串表等)
    SHF_STRINGS 0x20 节包含 null 结尾的字符串
    SHF_INFO_LINK 0x40 sh_info 字段与其他节有关
    SHF_TLS 0x400 节用于线程本地存储

    Program Header 和 Section header区别

    Program Header(段) Section Header(节)
    操作系统加载用 链接器/调试器用
    每段可以包含多个节 更细粒度的代码/数据划分
    用于加载运行时内容 用于描述文件内容结构
    段的对齐、权限等更重要 节名、调试、符号、重定位等更丰富

    Smali汇编

    基本类型

     Smali基本数据类型中包含两种类型,原始类型和引用类型。对象类型和数组类型是引用类型,其它都是原始类型。具体数据类型如下表所示。

    Smali Java 说明
    v void 只能用于返回值类型
    Z boolean 布尔类型
    B byte 字节类型
    S short 短整型
    C char 字符型
    I int 整数类型
    J long 长整型
    F float 浮点型数据类型
    D double 双精度浮点型
    Lpackage/name; 对象类型 L接完整的包名,使用“;”表示对象名称的结束
    [数据类型 数组 [Ljava/lang/String,表示一个String类型的数组

    类的定义

    1
    .class + 权限修饰符 + 类名;

    示例

    1
    2
    3
    public class Test implements CharSequence
    {
    }
    1
    2
    3
    4
    .class public LTest;    #声明类(不可省略)
    .super Ljava/lang/Object; #声明该类所继承的父类,同Java,若没有指定其他父类,则所有类的父类都是Object(不可省略)
    .implements Ljava/lang/CharSequence; #若该类实现了接口,则添加该代码(视情况可省略)
    .source "Test.java" #反编译的过程中自动生成的标识该smali类对应的java源码类的标识,无实际作用(可省略)

    函数声明

    1
    2
    3
    .method 权限修饰符+静态修饰符+方法名(参数类型)返回值类型
    #方法体
    .end method

    示例1

    1
    2
    3
    4
    public class Test
    {
    public static void getName(){}
    }
    1
    2
    3
    4
    5
    6
    7
    .class public LTest;
    .super Ljava/lang/Object;
    .source "Test.java"

    .method public static getName()V
    return-void
    .end method

    示例2(带返回值)

    1
    2
    3
    4
    5
    6
    public class Test
    {
    public static String getName(String a,int b){
    return "hello"
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    .class public LTest;
    .super Ljava/lang/Object;
    .source "Test.java"

    .method public static getName(Ljava/lang/String;I)Ljava/lang/String;
    const-string v0, "hello"
    return-object v0
    .end method

    函数返回关键字

    Smali方法返回关键字 J数据类型
    return byte
    return short
    return int
    return float
    return char
    return boolean
    return-wide double
    return-wide long
    return-void void
    return-object array
    return-object object
    smali中总共分为四种函数返回关键字,对于不同的数据类型,使用不同的函数返回关键字,long、double为64位数据类型,因此需要使用return-wide关键字,而其他基本数据类型为32位及以下,只需要使用return。对于空类型使用return-void。而对于大小未知的数组及object,则使用return-object。

    构造函数声明

    1
    2
    3
    .method+权限修饰符+constructor <init>(参数类型)返回值类型
    #方法体
    .end method

    示例

    1
    2
    3
    4
    5
    public class Test
    {
    public Test(String a){
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    .class public LTest;
    .super Ljava/lang/Object;
    .source "Test.java"

    .method public constructor <init>(Ljava/lang/String;)V
    invoke-direct {p0},Ljava/lang/Object;-><init>()V #调用父类(Object)的构造函数
    return-void
    .end method

    构造函数的声明与普通函数声明的区别在于:1.必须加constructor修饰符;2.函数名必须为<init>;3.函数中必须调用父类的构造函数(java的构造函数中默认会调用父类的构造函数,但在代码中可以省略,而在Smali中不能省略)。

    变量声明

    1
    .field 权限修饰符+静态修饰符+变量名:变量全类名路径;
    1
    2
    3
    4
    public class Test
    {
    private static String a;
    }
    1
    2
    3
    4
    5
    .class public LTest;
    .super Ljava/lang/Object;
    .source "Test.java"

    .field private static a:Ljava/lang/String; #声明一个String类的对象,命名为a

    常量声明

    1
    .field 权限修饰符+静态修饰符+final+常量名:常量全类名路径;=常量值
    1
    2
    3
    4
    public class Test
    {
    private static final String a="hello";
    }
    1
    2
    3
    4
    5
    .class public LTest;
    .super Ljava/lang/Object;
    .source "Test.java"

    .field private static final a:Ljava/lang/String;="hello" #声明一个String类的常量对象,命名为a,赋值为"hello"

    静态代码块

    1
    2
    3
    .method+static+constructor <clinit>()V
    #方法体
    .end method
    1
    2
    3
    4
    public class Test
    {
    static{}
    }
    1
    2
    3
    4
    5
    6
    .class public LTest;
    .super Ljava/lang/Object;

    .method public static constructor<clinit>()V
    return-void
    .end method

    静态代码块的声明同构造函数的声明的区别在于:1.增加了static修饰符;2.函数名改为了<clinit>;3.无参数;4.返回类型固定为void

    Smali静态字段声明位置

    1
    2
    3
    4
    5
    public class Test
    {
    public static String a="a";
    static{}
    }
    1
    2
    3
    4
    5
    6
    7
    .class public LTest;
    .super Ljava/lang/Object;

    .method public static constructor<clinit>()V
    .field public static a:Ljava/lang/String;="a"
    return-void
    .end method

    当类中声明了静态的字段时,该字段的声明必须于静态代码块中进行,因该字段属于类而非对象,且静态代码块在构造函数之前被执行。

    函数调用

    1
    2
    3
    4
    5
    invoke-virtual    #非私有实例函数的调用
    invoke-direct #构造函数以及私有函数的调用
    invoke-static #静态函数的调用
    invoke-super #父类函数的调用
    invoke-interface #接口函数的调用

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test
    {
    public Test(String a){
    getName();
    }
    public String getName(){
    return "hello";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .class public LTest;
    .super Ljava/lang/Object;

    .method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V #调用父类的构造函数
    invoke-virtual{p0},LTest;->getName()Ljava/lang/String; #调用普通成员getName函数
    return-void
    .end method

    .method public getName()Ljava/lang/String;
    const-string v0,"hello" #定义局部字符串常量
    return-object v0 #返回常量
    .end method

    以上代码段中定义的getName函数是没有参数的,但是在调用该函数时却传入了一个参数p0,该p0参数实质上为类的对象,即java中的this。因被调用的getName函数非静态函数,因此在使用该函数时必须传入一个this作为参数,而这一步在java中被默认执行,故书写代码时常被省略。

    示例:构造函数以及私有实例函数的调用

    1
    invoke-direct {参数},函数所属类名;->函数名(参数类型)返回值类型;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test
    {
    public Test(String a){
    getName();
    }
    private String getName(){
    return "hello";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    .class public LTest;
    .super Ljava/lang/Object;

    .method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V #调用父类的构造函数
    invoke-direct{p0},LTest;->getName()Ljava/lang/String; #调用私有成员getName函数
    return-void
    .end method

    .method private getName()Ljava/lang/String;
    const-string v0,"hello" #定义局部字符串常量
    return-object v0 #返回常量
    .end method

    示例:静态函数

    1
    invoke-static {参数},函数所属类名;->函数名(参数类型)返回值类型;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test
    {
    public Test(String a){
    String b=getName();
    }
    private static String getName(){
    return "hello";
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    .class public LTest;
    .super Ljava/lang/Object;

    .method public constructor<init>(Ljava/lang/String;)V
    invoke-direct{p0},Ljava/lang/Object;-><init>()V #调用父类的构造函数
    invoke-static{},LTest;->getName()Ljava/lang/String; #调用私有成员getName函数
    move-result-object v0 #将返回值赋给v0
    return-void
    .end method

    .method private getName()Ljava/lang/String;
    const-string v0,"hello" #定义局部字符串常量
    return-object v0 #返回常量
    .end method

    示例: 父类成员函数的调用

    1
    invoke-super {参数},函数所属类名;->函数名(参数类型)返回值类型;
    1
    2
    3
    4
    @Override
    protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    }
    1
    2
    3
    4
    5
    6
    .method protected onCreate(Landroid/os/Bundle;)V
    .registers 2

    invoke-super{p0,p1},Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
    return-void
    .end method

    示例: 接口函数调用

    1
    invoke-interface {参数},函数所属类名;->函数名(参数类型)返回值类型;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class Test
    {
    private InterTest a=new Test2();
    public Test(String a){}
    public void setAa(){
    InterTest aa=a;
    //调用接口方法
    aa.est2();
    }
    public class Test2 implements InterTest
    {
    public Test2(){}
    public void est2(){}
    }
    interface InterTest
    {
    public void est2();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    .method public setAa()V
    .registers 2
    iget-object v0,p0,LTest;->a:LTest$InterTest;
    #调用接口方法
    invoke-interface{v0},LTest$InterTest;->est2()V
    return-void
    .end method

    字段取值与赋值

    1
    2
    3
    4
    iput 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径    #静态字段
    iput 存储值的寄存器,存储字段的对象,对象全类名路径->字段名:字段类型全类名路径 #字段
    iget 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径 #静态字段
    iget 存储值的寄存器,对象全类名路径->字段名:字段类型全类名路径 #字段

    Smali基本数据类型取值赋值关键字表

    Smali实例变量取值赋值关键字表

    smali——java取值赋值对照

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Test
    {
    private String a="hello";
    public Test(String a){
    }
    public String getA(){
    String aa=a;
    }
    }
    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
    .class public LTest;#声明类 (必须)
    .super Ljava/lang/Object;#声明父类 默认继承Object (必须)
    .source "Test.java" # 源码文件 (非必须)

    # 声明静态字段
    .field private static a:Ljava/lang/String;

    #构造方法
    .method public constructor <init>(Ljava/lang/String;)V
    .registers 3

    .prologue

    invoke-direct {p0}, Ljava/lang/Object;-><init>()V

    const-string v0, "hello"
    # 初始化成员变量
    iput-object v0, LTest;->a:Ljava/lang/String;

    return-void
    .end method


    # 取值方法
    .method public getA()Ljava/lang/String;
    .registers 2

    # 类非静态字段取值
    iget-object v0, LTest;->a:Ljava/lang/String;
    return-object v0
    .end method

    Smali数组的取值与赋值

    1
    2
    aput 存储值的寄存器,存储数组的寄存器,存储数组下标的寄存器
    aget 存储值的寄存器,存储数组的寄存器,存储数组下标的寄存器

    示例

    1
    2
    3
    4
    5
    6
    invoke-virtual {v1, v0}, Ljava/security/MessageDigest;->digest([B)[B
    move-result-object v0
    const/16 v4,47
    const/16 v5,0
    aput-byte v4,v0,v5
    aget-byte v3,v0,v5

    这里解释下这段汇编

    invoke-virtual {v1, v0}, Ljava/security/MessageDigest;->digest([B)[B

    • 调用 Java 的 MessageDigest.digest(byte[] input) 方法。

    • v1MessageDigest 实例(如 SHA-256

    • v0:原始输入的 byte[]

    • 返回值是 byte[] 类型,存储计算后的哈希值。

    • move-result-object v0

    • 接收上一步调用返回的哈希结果,赋值给 v0

    • 此时,v0 成为了 digest 输出的 byte[]

    const/16 v4, 47

    • 将整数常量 47 加载到 v4 中。
    • 十进制 47 = 十六进制 0x2F,等于 ASCII 字符 '/'

    const/16 v5, 0

    • 将索引值 0 加载到 v5 中,用于访问 v0 数组的第一个元素

    aput-byte v4, v0, v5

    • v4(即 47)这个字节写入到 v0[0] 中。
    • 相当于强行篡改哈希结果的首字节:
    1
    v0[0] = 47;

    aget-byte v3, v0, v5

    • v0[0] 读取一个字节,赋值到 v3
      1
      v3 = v0[0]; // 此时 v3 = 47

    Smali对象创建

    定义

    1
    2
    new-instance+对象名,对象全包名路径;    #声明实例
    invoke-direct{变量名},对象全包名路径;-><init>(参数)返回类型 #调用构造函数(若构造函数内还定义了成员变量,则在调用之前需要提前声明该变量并在invoke时作为参数一并传入)

    示例

    1
    Test _test=new Test();
    1
    2
    new-instance v0,LTest;
    invoke-direct {v0},LTest;-><init>()V

    Smali常量数据定义

    定义: 在Smali语言中,函数返回或函数调用等处若需使用常量,应当如同使用变量一般先行将之定义并存入寄存器中,后方可使用。

    Smali字符串常量定义

    定义

    1
    const-string 常量名,"字符串内容"

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    .class public LTest;
    .super Ljava/lang/Object;

    .method public static getHello()Ljava/lang/String;
    .registers 1 #该函数总共使用了1个寄存器
    const-string v0,"hello" #定义字符串常量
    return-object v0 #将字符串常量作为返回值
    .end method

    如上代码段所示,定义一个”hello”字符串存入寄存器,命名为v0,并将之作为返回值返回(因字符串为Object类型的数据,因此使用return-object)

    Smali字节码常量定义

    定义

    1
    const-class 常量名,类全包名路径;

    示例

    1
    Class a=TestClass.class
    1
    const-class v0,LTestClass;

    Smali数值常量定义

    格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const 寄存器,数值 #占用一个寄存器(32位)
    const/4 #占用一个寄存器中的低4位(最高位为符号位)
    const/16 #占用一个寄存器中的低16位(最高位为符号位)
    const #占用一个寄存器中全部32位(最高位为符号位)
    const/high16 #占用一个寄存器中的16位,且只将数据的高16位存入(最高位为符号位)
    const-wide 寄存器,数值 #占用两个寄存器(64位)
    const-wide/16 #占用两个寄存器的同时只使用第一个寄存器的低16位
    const-wide/32 #占用两个寄存器的同时只使用第一个寄存器的32位
    const-wide #占用两个寄存器的同时只使用两个寄存器的全部64位
    const-wide/high16 #占用两个寄存器的同时只使用第一个寄存器的16位,且只将数据的高16位存入

    示例

    1
    2
    int i=100;
    long j=10000;
    1
    2
    const/16 v0,64   
    const-wide v1,2710

    Smali条件跳转

    定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    if-eq vA, vB, :cond_**  #如果vA等于vB则跳转到:cond_** equal

    if-ne vA, vB, :cond_** #如果vA不等于vB则跳转到:cond_** not equal

    if-lt vA, vB, :cond_** #如果vA小于vB则跳转到:cond_** less than

    if-ge vA, vB, :cond_** #如果vA大于等于vB则跳转到:cond_** greater equal

    if-gt vA, vB, :cond_** #如果vA大于vB则跳转到:cond_** greater than

    if-le vA, vB, :cond_** #如果vA小于等于vB则跳转到:cond_** less equal

    if-eqz vA, :cond_** #如果vA等于0则跳转到:cond_** zero
    if-nez vA, :cond_** #如果vA不等于0则跳转到:cond_**
    if-ltz vA, :cond_** #如果vA小于0则跳转到:cond_**
    if-gez vA, :cond_** #如果vA大于等于0则跳转到:cond_**
    if-gtz vA, :cond_** #如果vA大于0则跳转到:cond_**
    if-lez vA, :cond_** #如果vA小于等于0则跳转到:cond_**

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    public class Test {
    public static void main(String[] args) {
    int a=2;
    if(a>1){
    //do-something
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    .method public static main([Ljava/lang/String;)V

    const/16 v0,0x2
    const/4 v1, 0x1
    if-le v0,v1,:cond_0
    #do-something
    :cond_0
    return-void
    .end method

    Smali逻辑循环

    定义

    1
    goto :cond_**  #跳转到:cond_**

    示例

    1
    2
    3
    4
    5
    6
    7
    public class Test {
    public static void main(String[] args) {

    for(int i=0; i<3;i++){
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .method public static main([Ljava/lang/String;)V

    const/4 v0, 0x0

    :goto_1
    const/4 v1, 0x3

    if-ge v0, v1, :cond_7

    add-int/lit8 v0, v0, 0x1 # 加法运算符 v0=v0+0x1

    goto :goto_1

    :cond_7
    return-void
    .end method

    寄存器

    寄存器分为如下两类:
    1、本地寄存器
    用v开头数字结尾的符号来表示,v0, v1, v2,…
    2、参数寄存器
    用p开头数字结尾的符号来表示,p0,p1,p2,…
    注意:
    在非static方法中,p0代指this,p1为方法的第一个参数。
    在static方法中,p0为方法的第一个参数。

    ARM汇编

    具体参考: https://blog.csdn.net/Luckiers/article/details/128221506

    寄存器详解

    1. 通用寄存器

    通用寄存器是一组用于存储数据和地址的寄存器。在 ARM 架构的不同版本中,这些寄存器的数量和命名有所不同。
    R0-R15 (R0-R14 + PC):
    在 ARMv7 和之前的版本中,有 16 个通用寄存器,编号从 R0 到 R15。
    R0 到 R14 用于存储数据和地址。
    R15 通常被称为程序计数器(PC),用于存储下一条指令的地址。

    X0-X30 (X0-X28+ FR + LR):
    在 ARMv8 和之后的版本中,有 31 个通用寄存器,编号从 X0 到 X30。
    X0 到 X28 用于存储数据和地址。
    X29: Frame Pointer (FP) 寄存器,它的主要作用是指向当前函数的栈帧(Stack Frame)
    X30: 链接寄存器(LR),用于保存返回地址。

    在ARMv7架构中使用程序状态寄存器(Current Program Status Register,CPSR)来表示当前的处理器状态(processor stste),而在ARMv8里使用PSTATE寄存器来表示。

    ARMv8及以后版本:

    寄存器 位数 描述
    X0-X30 64bit 通用寄存器,如果有需要可以当作32bit使用:W0-W30
    FP(X29) 64bit 保存栈帧地址(栈底指针)
    LR(X30) 64bit 程序链接寄存器,保存子程序结束后需要执行的下一条指令
    SP 64bit 保存栈顶指针,使用SP/WSP来进行对SP寄存器的访问。
    PC 64bit 程序计数器,俗称PC指针,总是指向即将要执行的下一条指令,在arm64中,软件不能修改PC寄存器
    PSTATE 64bit 状态寄存器,用于保存处理器的当前状态信息。

    ARM 64包含31个64bit寄存器,记为X0X30。
    每一个通用寄存器,它的低32bit都可以被访问,记为W0
    W30。

    LDR和STR分别从地址中读入内容到寄存器和向地址中写入寄存器的内容,后缀B表示字节,H表示半字,W表示单字

    向量和浮点寄存器。32个寄存器,向量和浮点共用,每个128位,用V0到V31来表示。不同的记号可以表示不同的长度,B表示字节,H表示半字,S表示单字,D表示双字,Q表示四字

    X0 - X7: 这8个寄存器通常用作函数参数寄存器,在函数调用时用来传递前8个参数,若参数个数大于8,就采用栈来传递。64位的函数返回值通常存放在X0寄存器中,128位的返回结果存在X0和X1两个寄存器中。
    X8: 间接结果位置寄存器,用于保存子函数的返回地址。在一些情况下,X8可以用于普通的临时寄存器,用于存储中间计算结果。
    X9 - X15: 通常被用作临时变量和中间计算结果的存储。调用者有责任在函数调用前保存它们的值,以免被覆盖。函数返回后,调用者也需要恢复这些寄存器的值。
    X16、X17: X16 (IP0, Intra-Procedure-call scratch register 0)这个寄存器通常被用作临时寄存器,用于存储函数内部的中间计算结果。它在函数调用过程中可能会被修改,所以调用者需要自行保存和恢复。X17与X16类似。

    X18: X18 (Platform register)这个寄存器通常被用作平台相关的寄存器,其用途取决于具体的硬件平台和软件环境。在某些系统中,X18 可能被用作过程链接表(PLT)指针,用于动态链接。在其他系统中,X18 可能被用作线程局部存储(Thread Local Storage)的指针。
    X19 - X28: 通常用于存储函数的局部变量和中间计算结果。被调用的函数有责任在返回前保存和恢复这些寄存器的值,确保调用者可以继续使用。

    X29: Frame Pointer (FP) 寄存器,它的主要作用是指向当前函数的栈帧(Stack Frame),方便访问函数内部的局部变量和参数。当一个函数被调用时,x29 寄存器会被设置为指向该函数的栈帧起始地址。这样可以通过 x29 寄存器轻松访问函数内部的局部变量和参数,而不需要依赖 x30 寄存器(Link Register)中存储的返回地址。

    X30: Link Register (LR),它的主要作用是在函数调用时存储函数的返回地址,以便函数执行完毕后能够正确地返回到调用点。当一个函数被调用时,CPU 会将当前执行点的地址保存到 x30 寄存器中。这样在函数执行完毕后,只需要从 x30 寄存器中恢复返回地址,就可以正确地返回到调用点。注意:当一个函数被调用时,CPU 会自动将当前执行点的地址(也就是函数调用语句的下一条指令地址)保存到 x30 寄存器中。

    X9 - X15和X19 - X28这两组寄存器的区别:

    x9 到 x15 则主要用作临时变量和中间计算结果的存储。x19 到 x28 通常用作函数的局部变量和中间计算结果的存储。对于 x9 到 x15 这些”caller-saved”寄存器,调用者(caller)有责任在函数调用前保存它们的值,并在调用后恢复。对于 x19 到 x28 这些”callee-saved”寄存器,被调用的函数(callee)有责任在返回前保存和恢复它们的值。使用”callee-saved”寄存器通常可以减少对栈的访问,提高性能。但同时也增加了函数调用时保存和恢复寄存器的开销。

    ARMv8前版本:
    R0-R15寄存器 根据“ARM-thumb 过程调用标准”:
    R0-R3 其中,R0通常用于存储函数的返回值,R1-R3则常用于传递函数参数。在子程序调用之前,可以将R0-R3用于任何用途。被调用函数在返回之前不必恢复R0-R3。如果调用函数需要再次使用 r0-r3的内容,则它必须保留这些内容。
    R4-R11 被用来存放函数的局部变量。如果被调用函数使用了这些寄存器,它在返回之前必须恢复这些寄存器的值。
    R12是内部调用暂时寄存器IP。它在过程链接胶合代码(例如,交互操作胶合代码)中用于此角色。
    在过程调用之间,可以将它用于任何用途。被调用函数在返回之前不必恢复R12。
    R13是栈指针SP:
    在ARM指令集中,R13常被用作堆栈指针,用于存储程序中的局部变量和函数调用时的返回地址。它不能用于任何其它用途。SP中存放的值在退出被调用函数时必须与进入时的值相同。用户也可以使用其他寄存器作为堆栈指针,但在Thumb指令集中,某些指令强制要求使用R13作为堆栈指针。
    R14是链接寄存器LR:
    用于存储函数调用之前的返回地址,如果您保存了返回地址,则可以在调用之前将R14用于其它用途,程序返回时要恢复。当执行子程序调用指令(如BL或BLX)时,R14会被设置成该子程序的返回地址。在子程序返回时,将R14的值复制回程序计数器PC即可完成子程序的调用返回。
    R15是程序计数器PC:
    用于存储当前正在执行的指令的地址。程序计数器是处理器控制指令执行的关键寄存器之一。它不能用于任何其它用途。由于ARM采用了流水线机制,当正确读取了PC的值后,该值为当前指令地址加8个字节,即PC指向当前指令的下两条指令地址。
    注意:在中断程序中,所有的寄存器都必须保护,编译器会自动保护R4~R11。

    2. 专用寄存器

    ARM处理器中的专用寄存器主要包括程序状态寄存器(CPSR)和备份的程序状态寄存器(SPSRs)。
    程序状态寄存器(CPSR)

    CPSR是一个32位的特殊寄存器,用于存储当前程序的状态信息。它包含以下内容:

    ALU状态标志 :如条件码(如零标志Z、负标志N、进位标志C等),用于反映ALU的运算结果。
    中断使能位 :用于控制中断的使能状态。
    执行模式位 :用于标识当前处理器的执行模式(如用户模式、系统模式、中断模式等)。

    CPSR和SPSR都是程序状态寄存器,其中SPSR是用来保存中断前的CPSR中的值,以便在中断返回之后恢复处理器程序状态。
    CPSR在任何处理器模式下都可被访问和修改(但某些位可能需要特权级代码才能修改)。通过读取和修改CPSR寄存器的各个标志位和控制位,可以控制程序的执行流程和处理器的行为。
    备份的程序状态寄存器(SPSRs)

    ARM处理器还包含5个备份的程序状态寄存器(SPSR_fiq、SPSR_irq、SPSR_svc、SPSR_abt、SPSR_und),用于在异常处理期间保存CPSR的值。当处理器进入异常模式时,会将CPSR的内容复制到对应的SPSR中;当从异常模式返回时,则可以将SPSR的内容复制回CPSR以恢复处理器的状态。

    3. 控制寄存器

    虽然控制寄存器不直接归类为通用或专用寄存器,但它们在ARM处理器的控制中发挥着重要作用。这些寄存器通常包含处理器的控制位和配置位,用于控制处理器的行为和工作模式。由于控制寄存器的访问和修改通常需要特权级代码,因此它们在普通的应用程序中很少被直接访问。如控制CPU的行为,如 CTRL 和 ACTLR。

    2.1 32位数据操作指令

    名字 功能
    ADC 带进位加法
    ADD 加法
    ADDW 宽加法(可以加 12 位立即数)
    AND 按位与
    ASR 算术右移
    BIC 位清零(把一个数按位取反后,与另一个数逻辑与)
    BFC 位段清零
    BFI 位段插入
    CMN 负向比较(把一个数和另一个数的二进制补码比较,并更新标志位)
    CMP 比较两个数并更新标志位
    CLZ 计算前导零的数目
    EOR 按位异或
    LSL 逻辑左移
    LSR 逻辑右移
    MLA 乘加
    MLS 乘减
    MOVW 把 16 位立即数放到寄存器的底16位,高16位清0
    MOV 加载16位立即数到寄存器(其实汇编器会产生MOVW——译注)
    MOVT 把 16 位立即数放到寄存器的高16位,低 16位不影响
    MVN 移动一个数的补码
    MUL 乘法
    ORR 按位或
    ORN 把源操作数按位取反后,再执行按位或(
    RBIT 位反转(把一个 32 位整数先用2 进制表达,再旋转180度——译注)
    REV 对一个32 位整数做按字节反转
    REVH/REV16 对一个32 位整数的高低半字都执行字节反转
    REVSH 对一个32 位整数的低半字执行字节反转,再带符号扩展成32位数
    ROR 圆圈右移
    RRX 带进位的逻辑右移一格(最高位用C 填充,且不影响C的值——译注)
    SFBX 从一个32 位整数中提取任意的位段,并且带符号扩展成 32 位整数
    SDIV 带符号除法
    SMLAL 带符号长乘加(两个带符号的 32 位整数相乘得到 64 位的带符号积,再把积加到另一个带符号 64位整数中)
    SMULL 带符号长乘法(两个带符号的 32 位整数相乘得到 64位的带符号积)
    SSAT 带符号的饱和运算
    SBC 带借位的减法
    SUB 减法
    SUBW 宽减法,可以减 12 位立即数
    SXTB 字节带符号扩展到32位数
    TEQ 测试是否相等(对两个数执行异或,更新标志但不存储结果)
    TST 测试(对两个数执行按位与,更新标志但不存储结果)
    UBFX 无符号位段提取
    UDIV 无符号除法
    UMLAL 无符号长乘加(两个无符号的 32 位整数相乘得到 64 位的无符号积,再把积加到另一个无符号 64位整数中)
    UMULL 无符号长乘法(两个无符号的 32 位整数相乘得到 64位的无符号积)
    USAT 无符号饱和操作(但是源操作数是带符号的——译注)
    UXTB 字节被无符号扩展到32 位(高24位清0——译注)
    UXTH 半字被无符号扩展到32 位(高16位清0——译注)

    2.2 32位存储器数据传送指令

    名字 功能
    LDR 加载字到寄存器
    LDRB 加载字节到寄存器
    LDRH 加载半字到寄存器
    LDRSH 加载半字到寄存器,再带符号扩展到 32位
    LDM 从一片连续的地址空间中加载多个字到若干寄存器
    LDRD 从连续的地址空间加载双字(64 位整数)到2 个寄存器
    STR 存储寄存器中的字
    STRB 存储寄存器中的低字节
    STRH 存储寄存器中的低半字
    STM 存储若干寄存器中的字到一片连续的地址空间中
    STRD 存储2 个寄存器组成的双字到连续的地址空间中
    PUSH 把若干寄存器的值压入堆栈中
    POP 从堆栈中弹出若干的寄存器的值

    2.3 32位转移指令

    名字 功能
    B 无条件转移
    BL 转移并连接(呼叫子程序)
    TBB 以字节为单位的查表转移。从一个字节数组中选一个8位前向跳转地址并转移
    TBH 以半字为单位的查表转移。从一个半字数组中选一个16 位前向跳转的地址并转移

    2.4 其它32位指令

    名字 功能
    LDREX 加载字到寄存器,并且在内核中标明一段地址进入了互斥访问状态
    LDREXH 加载半字到寄存器,并且在内核中标明一段地址进入了互斥访问状态
    LDREXB 加载字节到寄存器,并且在内核中标明一段地址进入了互斥访问状态
    STREX 检查将要写入的地址是否已进入了互斥访问状态,如果是则存储寄存器的字
    STREXH 检查将要写入的地址是否已进入了互斥访问状态,如果是则存储寄存器的半字
    STREXB 检查将要写入的地址是否已进入了互斥访问状态,如果是则存储寄存器的字节
    CLREX 在本地的处理上清除互斥访问状态的标记(先前由 LDREX/LDREXH/LDREXB做的标记)
    MRS 加载特殊功能寄存器的值到通用寄存器
    MSR 存储通用寄存器的值到特殊功能寄存器
    NOP 无操作
    SEV 发送事件
    WFE 休眠并且在发生事件时被唤醒
    WFI 休眠并且在发生中断时被唤醒
    ISB 指令同步隔离(与流水线和 MPU等有关——译注)
    DSB 数据同步隔离(与流水线、MPU 和cache等有关——译注)
    DMB 数据存储隔离(与流水线、MPU 和cache等有关——译注)
    DMB 数据存储器隔离。DMB 指令保证: 仅当所有在它前面的存储器访问操作都执行完毕后,才提交(commit)在它后面的存储器访问操作。
    DSB 数据同步隔离。比 DMB 严格: 仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访 问操作——译者注)
    ISB 指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。

    2.5 立即数

    1、立即数:一个立即数是一块数据存储作为指令本身,而不是在一个中的一部分内容存储器位置或寄存器。立即值通常用于加载值或对常量执行算术或逻辑运算的指令。
    2、比如一个数 10,把他存入内存中,高级语言表示法是 int i=10,这个数放入内存之前叫立即数,放入之后就不是了,再比如一个数 10,把他存入寄存器中,这个数放入寄存器之前叫立即数,放入之后就不是了。

    2.6 逻辑数

    逻辑数是用来表示二值逻辑中的”是”与”否”、或称”真”与”假”两个状态的数据。在计算机中,可以用一位基2码表示逻辑数据,即8个逻辑数据可以存放在1个字节中,可用其中的每个bit(位)表示一个逻辑数据。逻辑数可以用计算机中的基2码的两个状态”1”和”0”来表示,其中”1”表示真,”0”表示假。

    2.7 逻辑运算和算术运算

    逻辑运算是一种只存在于二进制中的运算。在计组中逻辑运算经常出现的是 或、与、非和异或,这几种运算方式。
    算数运算我们平常十进制的 加减乘除,但因为在计算机中是二进制所以就只能是加法运算。在计算机中也可以算数运算也可以区分成进位的算数运算和不进位的算数运算。带进位的算数运算

    实例讲解

    3.1 MRS

    将状态寄存器CPSR或SPSR的内容移动到一个通用寄存器

    3.2 MSR

    将立即数或通用寄存器的内容加载到CPSR或SPSR的指定字段中

    1
    2
    3
    MSR CPSR,R0        
    MSR SPSR,R0
    MSR CPSR_c,R0

    3.3 PRIMASK

    用于disable NMI和硬 fault之外的所有异常,它有效地把当前优先级改为 0(可编程 优先级中的最高优先级)。
    CPS指令会更改CPSR中的一个或多个模式以及A、I和F位,但不更改其他CPSR位。CPSID就是中断禁止,CPSIE中断允许,

    A:表示启用或禁止不精确的中止;I:表示启用或禁止IRQ中断;F:表示启用或禁止FIQ中断

    3.4 FAULTMASK

    1
    2
    3
    CPSIE f; / CPSID f;

    MSR FAULTMASK,R0

    FAULTMASK更绝,它把当前优先级改为-1。这么一来,连硬fault都被掩蔽了。使用方案与
    PRIMASK的相似。但要注意的是,FAULTMASK会在异常退出时自动清零。

    3.5 BX指令

    BX{条件} 目标地址
    BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM 指令,也可以是Thumb指令。

    3.6 零寄存器 wzr、xzr

    因为我们在使用 str 的是没法使用立即数 0 给寄存器赋值,所以 wzr xzr就是干这个事情的。是一个比较特殊又常常见到的寄存器。

    3.7 立即寻址指令MOV

    1
    2
    3
    4
    SUBS R0,R0,#1   
    MOV R0,#0xFF000
    MOV R1,R2
    SUB R0,R1,R2

    3.8 寄存器间接寻址指令LDR

    1
    2
    LDR R1,[R2]    
    SWP R1,R1,[R2]
    1
    2
    3
    4
    5
    6
    按照从简单到复杂的分类方法,可以通过以下方式来指定访存指令的地址:从寄存器中获取地址;通过寄存器内容再加上偏移来获取地址;对偏移进行扩展、移位等运算之后,再与寄存器内容相加,获得地址。
    LDR X0, [X1] ; 直接从寄存器X1的内容中获取地址。
    LDR X0, [X1,
    LDR X0, [X1, X2] ; X!的内容和X2的内容相加得到地址。
    LDR X0, [X1, W2, SXTW] ; 对W2的内容做符号扩展,再与X1的内容相加,作为地址。
    LDR X0, [X1, X2, LSL

    3.9 寄存器移位寻址指令LSL

    1
    2
    MOV R0,R2,LSL #3     
    ANDS R1,R1,R2,LSL R3
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    寻址模式

    简单模式:X1的内容不会被改变,例如。
    LDR X0, [X1]
    LDR X0, [X1, #4]

    前变址模式,X1的内容在load之前变化,例如。
    LDR X0, [X1, #4]!
    等价于
    ADD X1, X1, #4
    LDR X0, [X1]

    后变址模式,X1的内容在load之后变化,例如。
    LDR X0, [X1], #4
    等价于
    LDR X0, [X1]
    ADD X1, X1, #4

    支持对整型、浮点标量和向量,要求源寄存器和目的寄存器必须具有相同的宽度,例如。
    LDP W3, W7, [X0] ; [X0] => W3, [X0 + 4 bytes] =>W7
    STP Q0, Q1, [X4] ; Q0 => [X4], Q1=>[X4 + 16 bytes]

    3.10 基址寻址指令 STR

    1
    2
    LDR R2,[R3,#0x0C]  
    STR R1,[R0,#-4]!

    3.11 多寄存器寻址指令

    1
    2
    LDMIA R1!,{R2-R7,R12}   
    STMIA R0!,{R2-R7,R12}

    3.12 无条件转移B,BAL

    举例: B LABEL ; LABEL为某个位置

    1
    2
    CMP      x3,x4
    B.CS {pc}+0x10 ; 0xc000800094

    BCC是指CPSR寄存器条件标志位为0时的跳转。结合CMP R3, R1,意思是比较R3 R1寄存器,当相等时跳转到环测试。因为CMP指令减去两个值并在CPSR中设置条件标志位。

    3.13 条件转移B.cont

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    BEQ 相等
    BNE 不等
    BPL 非负
    BMI 负
    BCC 无进位
    BCS 有进位
    BLO 小于(无符号数)
    BHS 大于等于(无符号数)
    BHI 大于(无符号数)
    BLS 小于等于(无符号数)
    BVC 无溢出(有符号数)
    BVS 有溢出(有符号数)
    BGT 大于(有符号数)
    BGE 大于等于(有符号数)
    BLT 小于(有符号数)
    BLE 小于等于(有符号数)
    1
    2
    3
    blr Xm:跳转到由Xm目标寄存器指定的地址处,同时将下一条指令存放到X30寄存器中。例如:blr x20.
    br Xm:跳转到由Xm目标寄存器指定的地址处。不是子程序返回
    ret {Xm}:跳转到由Xm目标寄存器指定的地址处。是子程序返回。Xm可以不写,默认是X30.

    3.14 WFE 和 WFI 对比

    wfi 和 wfe 指令都是让ARM核进入standby睡眠模式。wfi是直到有wfi唤醒事件发生才会唤醒CPU,wfe是直到wfe唤醒事件发生,这两类事件大部分相同。唯一不同之处在于wfe可以被其他CPU上的sev指令唤醒,sec指令用于修改event寄存器的指令。

    WFE
    Wait For Event,是否实现此指令是可选的。如果此指令未实现,它将作为NOP指令来执行。如果指令作为NOP在目标处理器上执行,汇编程序将生成诊断消息。

    SEV
    Set Event,其是否实现是可选的。如果未实现,它将作为NOP执行。如果指令作为NOP在目标上执行,汇编程序将生成诊断消息。
    SEV在ARMv6T2中作为NOP指令执行。

    3.15 MRC:协处理器寄存器到ARM寄存器的数据传输

    MRC指令将协处理器的寄存器中数值传送到ARM处理器的寄存器中。如果协处理器不能成功地执行该操作,将产生未定义的指令异常中断。

    1
    2
    MRC p2,5,r3,c5,c6协处理器p2把c5和c6经过5操作的结果赋给r3
    MRC p3,9,r3,c5,c6,2协处理器p3把c5和c6经过9操作(类型2)的结果赋给r3

    3.16 MCR:寄存器到协处理器寄存器的数据传输

    MCR指令将ARM处理器的寄存器中的数据传送到协处理器的寄存器中。如果协处理器不能成功地执行该操作,将产生未定义的指令异常中断。

    1
    MCR p6,0,r4,c5,c6协处理器p6把r4执行0操作后将结果存放进c5

    3.17 STM:将指令中寄存器列表中的各寄存器数值写入到连续的内存单元中

    STM指令是Store Multiple的缩写,它的作用是将多个寄存器的值保存到栈中。在ARM汇编中,栈是一种后进先出 (LIFO)的数据结构,用来存储临时数据和函数调用过程中的返回地址

    STM指令的语法如下:

    其中,条件码是可选项,用来指定条件执行STM指令的条件;模式用来指定存储模式,

    1、寻址模式(mode)
    mode决定了基址寄存器是在执行指令前地址增减还是指令执行后增减.
    I为Increment(递增)
    D为Decrement (递减)
    B为Before
    A为After

    常用的模式有IA (递增后存储) 、IB (递增前存储) 、DA (减后存储)和DB(递减前存储);SP是栈指针寄存器,用来指定栈的起始地址;寄存器列表指定要保存的寄存器。
    另外四种也是寻址模式
    FD 慢递减堆栈
    FA 满递增堆栈
    ED 空递减堆栈
    EA 空递增堆栈

    在上述代码中,STMFD指令存储了RO、R1和R2的值到栈中。SP!表示栈指针寄存器递增,即存储完后栈指针自动增加,以便下一次保存操作。

    2、“!”
    在传输数据完成后,更新基址寄存器中的值

    3、“^”

    在数据传输完成后,将SPSR的值复制到CPSR中,常用于异常模式下的返回.

    3.18 LDM:将数据从连续内存单元中读取到指令的寄存器列表中的各寄存器中

    LDMIA

    3.19 LDR:从内存中将一个32位的字读取到目标寄存器

    1
    ldr 加载指令: LDR{条件} 目的寄存器,<存储器地址>

    LDR指令用亍从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

    3.20 STR:将32位字数据写入到指定的内存单元

    STR指令的格式为:

    STR指令用亍从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用,寻址方式灵活多样,使用方式可参考指令LDR。

    1
    2
    3
    STR R0,[R1],#8  ;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
    STR R0,[R1,#8] ;将R0中的字数据写入以R1+8为地址的存储器中。
    str r1, [r0] ;将r1寄存器的值,传送到地址值为r0的(存储器)内存中

    3.21 SWI:软中断指令

    SWI指令格式如下:

    MOV R0,#34 ;设置功能号为34
    SWI 12 ;产生软中断,中断号为12

    3.22 BIC清除位

    1
    BIC指令的格式为: BIC{条件}{S} 目的寄存器,操作数1,操作数2

    BIC指令用于清除操作数1的某些位,并把结果放置到目的寄存器中。操作数1应是一个寄存器, 操作数2可以是一个寄存器、被移位的寄存器、或一个立即数。操作数2为32位的掩码,如果在 掩码中置了某一位1,则清除这一位。未设置的掩码位保持不变。

    1
    2
    3
    4
    BIC R0,R0,#0X1F
    0X1F=11111B

    BIC R4, R4, #0xFF000000 指令将E4高8位清除为0

    3.23 EOR逻辑异或指令

    1
    EOR{<cond>}{S} <Rd>,<Rn>,<shifter_operand>

    逻辑异或EOR(Exclusive OR)指令将寄存器中的值和的值执行按位“异或”操作,并将执行结果存储到目的寄存器中,同时根据指令的执行结果更新CPSR中相应的条件标志位。

    3.24 CMN与负数对比

    CMN 同于 CMP,但它允许你与负值进行比较,比如难于用其他方法实现的用于结束列表的 -1。这样与 -1 比较将使用:
    CMN R0, #1 ; 把 R0 与 -1 进行比较

    3.25 MVN取反

    将每一位操作数都取反,若为有符号的数据则进行补码保存

    其中上图中的0x4用二进制数(00000100)表示, 然后对其取反得到(11111011),可见取反后为负数,因此针对负数求其补码则为储存在R0中的值,先将负数最高位转换为正数(01111011)取反,得到(10000100),加1得到其补码,最后结果为(10000101),即结果为-5;

    3.26 LSL(Logical Shift Left)左移运算

    用于将寄存器的值向左移位,末尾填充0。在ARM处理器中,每个寄存器都有32位,当LSL被使用时,指令将寄存器中的二进制数值向左移动指定的位数,并用0填充未使用的右侧位数。

    3.27 STP

    STP是一条用于将General-Purpose Registers(通用寄存器)的值存储到内存地址的指令。STP是Store Pair的缩写,用于同时将两个寄存器的值存储到连续的内存地址中。

    1
    STP <Rt1>, <Rt2>, [<Xn|SP>, #<imm>]

    Rt1和Rt2是要存储的寄存器,可以是X0-X30中的任何一个。
    Xn是基础地址寄存器,可以是X0-X30中的任何一个,但不能是Rt1或Rt2。
    #是一个常数值,用于指定一个偏移量,范围是-2048到2047。
    例如,以下是使用STP指令将X2和X3寄存器的值存储到基础地址寄存器X18指定的内存地址偏移24字节处的代码:

    注意:结尾的!表示同时更新基础寄存器的值,即存储操作后,X18将指向下一个地址。如果不需要更新基础寄存器,可以省略!

    实例解析

    1
    2
    3
    4
    5
    6
    7
    8
    IMPORT |Image$RW_IRAM1$Base|            
    IMPORT |Image$RW_IRAM1$Length|
    IMPORT |Load$RW_IRAM1$Base|
    IMPORT |Image$RW_IRAM1$ZI$Base|
    IMPORT |Image$RW_IRAM1$ZI$Length|
    Load$$region_name$$Base
    Load$$region_name$$Length
    Load$$region_name$$Limit
    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
    LDR R0, = |Load$RW_IRAM1$Base|           
    LDR R1, = |Image$RW_IRAM1$Base|
    LDR R2, = |Image$RW_IRAM1$Length|
    CopyData
    SUB R2, R2, #4
    LDR R3, [R0, R2]
    STR R3, [R1, R2]
    CMP R2, #0
    BNE CopyData


    LDR R0, = |Image$RW_IRAM1$ZI$Base|
    LDR R1, = |Image$RW_IRAM1$ZI$Length|
    CleanBss
    SUB R1, R1, #4
    MOV R3, #0
    STR R3, [R0, R1]
    CMP R1, #0
    BNE CleanBss

    IMPORT mymain
    BL mymain
    B .
    ENDP
    ALIGN
    END

    Android系统源码基础

    系统架构

    应用层

    • 用户可见的所有应用(系统自带或用户安装)
      • 比如:电话、短信、浏览器、相机、设置
    • 应用通过 API 与系统框架层交互

    应用框架层

    • 为开发者提供的核心 Java API
    • 提供各种系统服务的访问接口,如:
    模块 功能
    ActivityManager 管理应用生命周期和任务栈
    WindowManager 管理窗口显示与焦点
    PackageManager 管理应用安装、权限等
    ContentProvider 跨应用数据共享
    NotificationManager 通知控制
    LocationManager 定位服务

    系统运行库层

    Android Runtime(ART)
    • 每个应用进程有独立的 ART 实例
    • 负责:
      • 执行 .dex 字节码(通过 JIT 或 AOT 编译)
      • 管理内存、垃圾回收
    C/C++ 原生库(Native Libraries)
    • 使用 C/C++ 实现的系统功能库,如:
    功能
    libc 标准 C 库
    libm 数学库
    OpenGL ES 图形渲染
    SQLite 数据库存储
    libmedia 音视频编解码

    硬件抽象层

    • 定义标准接口,让上层服务无需关心具体硬件
    • OEM 厂商通过 HAL 实现对硬件的控制,比如:
    模块 功能
    camera.default.so 摄像头驱动
    audio.primary.so 音频驱动
    lights.default.so 灯光控制
    sensors.default.so 传感器读取

    Linux内核层

    • Android 运行在修改版的 Linux 内核上
    • 提供:
      • 线程管理
      • 内存管理
      • 电源管理
      • 安全机制(SEAndroid)
      • 驱动接口(用于与硬件直接通信)

    源码目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    AOSP_ROOT/
    ├── art/ → Android Runtime(ART)
    ├── bionic/ → C 标准库实现(libc、libm、libdl)
    ├── bootable/ → 启动引导(bootloader、recovery)
    ├── build/ → 构建系统脚本(makefile, Soong)
    ├── compatibility/ → CTS(兼容性测试套件)
    ├── dalvik/ → Dalvik 虚拟机(已被 ART 替代,保留部分兼容性)
    ├── development/ → 开发工具、调试工具
    ├── device/ → 各品牌/型号的设备配置(BoardConfig.mk 等)
    ├── external/ → 第三方开源库(openssl、zlib、protobuf 等)
    ├── frameworks/ → Java 框架层源码(核心 API、系统服务)
    ├── hardware/ → 硬件抽象层(HAL)接口与实现
    ├── kernel/ → Linux 内核源码(有时是 symbolic link)
    ├── libcore/ → Java 基础类库实现(java.lang, java.util等)
    ├── libnativehelper/ → JNI 帮助函数(native <-> Java 桥接)
    ├── packages/ → 系统应用(设置、桌面、输入法、浏览器等)
    ├── platform_testing/ → 平台测试框架
    ├── prebuilts/ → 预编译的工具链(GCC、Clang、Java 等)
    ├── sdk/ → SDK 构建工具相关源码
    ├── system/ → 核心系统服务(init、vold、core、server)
    ├── test/ → 测试相关内容
    ├── tools/ → 构建工具集(adb、aapt 等)
    ├── vendor/ → 厂商特定代码(闭源驱动或 HAL 模块实现)
    └── out/ → 编译输出目录(构建后生成)

    参考: https://wx.comake.online/doc/doc/SigmaStarDocs-SSD238X-Android-20240712/platform/Android/arch.html

    Frida 使用

    frida hook有两种模式,如下
            1. attach模式
            attach到已经存在的进程,核心原理是ptrace修改进程内存。如果此时进程已经处于调试状态(比如做了反调试),则会attach失败。
            2. spawn模式
          启动一个新的进程并挂起,在启动的同时注入frida代码,适用于在进程启动前的一些hook,比如hook RegisterNative函数,注入完成后再调用resume恢复进程。

    将一个脚本注入到Android目标进程

    使用usb进行hook
    frida -U -l myhook.js com.xxx.xxxx
    参数解释:

    • -U 指定对USB设备操作
    • -l 指定加载一个Javascript脚本
    • 最后指定一个进程名,如果想指定进程pid,用-p选项。正在运行的进程可以用frida-ps -U命令查看

    使用host进行hook
    需要现在手机启动 frida服务frida -l 0.0.0.0 5555相当于在手机启动一个监听,然后通过adb转发下端口,adb forward tcp:5555 tcp:5555就可以转发到本地,之后在 使用 frida -H 127.0.0.1:5555 -l test.js命令去加载脚本进行hook

    **加 -f 包名**:
    → 是让 Frida 启动 一个新的进程(就是指定的包名/应用),
    然后 在进程一启动起来(通常非常早期,甚至在 main 函数之前)就把你的脚本 注入 进去。
    → 这种方式叫 spawn attach模式(或者说spawn模式),
    → 比 attach 普通运行时更早,可以抓到早期行为,比如 Native 层动态注册、加固解密什么的。
    → 但是,需要手动 resume() 继续进程。

    • 不加 -f,直接指定一个正在运行的进程名/包名
      → Frida 是直接 attach到已经在运行的进程上,
      → 这种就是普通的 attach模式
      → 只能看到 attach 时刻之后的行为,前面的(比如早期解密)可能已经错过了。

    Hook Java

    一个hook的固定模版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Java.perform(function() {

    var <class_reference> = Java.use("<package_name>.<class>");
    <class_reference>.<method_to_hook>.implementation = function(<args>) {

    /*
    我们自己的方法实现
    */

    }

    })

    Java.perform 是 Frida 中用于创建一个特殊上下文的函数,让你的脚本能够与 Android 应用程序中的 Java 代码进行交互。它就像是打开了一扇门,让你能够访问并操纵应用程序内部运行的 Java 代码。一旦进入这个上下文,你就可以执行诸如钩取方法或访问 Java 类等操作来控制或观察应用程序的行为。

    Java.use方法用于加载一个Java类,相当于Java中的Class.forName()。比如要加载一个String类:
    var StringClass = Java.use("java.lang.String");
    加载内部类:
    var MyClass_InnerClass = Java.use("com.luoyesiqiu.MyClass$InnerClass");
    其中InnerClass是MyClass的内部类

    带参数的构造函数

    修改参数为byte[]类型的构造函数的实现

    1
    2
    3
    ClassName.$init.overload('[B').implementation=function(param){    
    //do something
    }

    注:ClassName是使用Java.use定义的类;param是可以在函数体中访问的参数

    修改多参数的构造函数的实现

    1
    2
    ClassName.$init.overload('[B','int','int').implementation=function(param1,param2,param3){    //do something
    }

    无参数构造函数

    1
    ClassName.$init.overload().implementation=function(){    //do something}

    调用原构造函数

    1
    ClassName.$init.overload().implementation=function(){    //do something    this.$init();    //do something}

    注意:当构造函数(函数)有多种重载形式,比如一个类中有两个形式的func:void func()void func(int),要加上overload来对函数进行重载,否则可以省略overload

    一般函数

    修改函数名为func,参数为byte[]类型的函数的实现

    1
    ClassName.func.overload('[B').implementation=function(param){    //do something    //return ...}

    无参数的函数

    1
    ClassName.func.overload().implementation=function(){    //do something}

    注: 在修改函数实现时,如果原函数有返回值,那么我们在实现时也要返回合适的值

    1
    ClassName.func.overload().implementation=function(){    //do something    return this.func();}

    调用函数

    和Java一样,创建类实例就是调用构造函数,而在这里用$new表示一个构造函数。

    1
    2
    var ClassName=Java.use("com.luoye.test.ClassName");
    var instance = ClassName.$new();

    实例化以后调用其他函数

    1
    2
    3
    var ClassName=Java.use("com.luoye.test.ClassName");
    var instance = ClassName.$new();
    instance.func();

    字段操作

    字段赋值和读取要在字段名后加.value,假设有这样的一个类:

    1
    2
    3
    4
    5
    package com.luoyesiqiu.app;
    public class Person{
    private String name;
    private int age;
    }

    写个脚本操作Person类的name字段和age字段:

    1
    2
    3
    4
    5
    6
    7
    8
    var person_class = Java.use("com.luoyesiqiu.app.Person");//实例化Person类
    var person_class_instance = person_class.$new();
    //给name字段赋值
    person_class_instance.name.value = "luoyesiqiu";
    //给age字段赋值
    person_class_instance.age.value = 18;
    //输出name字段和age字段的值
    console.log("name = ",person_class_instance.name.value, "," ,"age = " ,person_class_instance.age.value);

    输出:

    1
    name = luoyesiqiu , age = 18

    类型转换

    Java.cast方法来对一个对象进行类型转换,如将variable转换成java.lang.String

    1
    2
    var StringClass=Java.use("java.lang.String");
    var NewTypeClass=Java.cast(variable,StringClass);

    Java.available字段

    这个字段标记Java虚拟机(例如: Dalvik 或者 ART)是否已加载, 操作Java任何东西之前,要确认这个值是否为true

    Java.perform方法

    Java.perform(fn)在Javascript代码成功被附加到目标进程时调用,我们核心的代码要在里面写。格式:

    1
    Java.perform(function(){//do something...});

    参考:
    https://www.daowuya.love/frida%E7%94%A8%E6%B3%95%E4%B9%8Bhook-java%E5%B1%82%E4%BB%A3%E7%A0%81/

    来看一个例子

    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
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    package a.b.k0801;





    import androidx.appcompat.app.AppCompatActivity;



    import android.os.Bundle;

    import android.view.LayoutInflater;

    import android.view.View;

    import android.widget.Button;

    import android.widget.EditText;

    import android.widget.TextView;



    import java.security.MessageDigest;

    import java.security.NoSuchAlgorithmException;



    import a.b.k0801.databinding.ActivityMainBinding;



    public class MainActivity extends AppCompatActivity {



    // Used to load the 'k0801' library on application startup.

    static {

    System.loadLibrary("k0801");

    }



    private ActivityMainBinding binding;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);



    binding = ActivityMainBinding.inflate(getLayoutInflater());

    setContentView(binding.getRoot());



    // Example of a call to a native method

    EditText et_userName = binding.etUserName;

    EditText et_key = binding.etKey;

    TextView tv_result = binding.tvResult;

    Button btn_reg = binding.btnOk;

    btn_reg.setOnClickListener(new View.OnClickListener() {

    @Override

    public void onClick(View view) {

    if(checkSN(et_userName.getText().toString().trim(), et_key.getText().toString().trim())){

    tv_result.setText("注册成功!");

    }else {

    tv_result.setText("注册失败!");

    }

    }

    });

    }



    private boolean checkSN(String userName, String sn) {

    try {

    if ((userName == null) || (userName.length() == 0))

    return false;

    if ((sn == null) || (sn.length() != 16))

    return false;

    MessageDigest digest = MessageDigest.getInstance("MD5");

    digest.reset();

    digest.update(userName.getBytes());

    byte[] bytes = digest.digest(); //采用MD5对用户名进行Hash

    String hexstr = bytes.toString(); //将计算结果转化成字符串

    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < hexstr.length(); i += 2) {

    sb.append(hexstr.charAt(i));

    }

    String userSN = sb.toString(); //计算出的SN

    if (!userSN.equalsIgnoreCase(sn)) //比较注册码是否正确

    return false;

    } catch (NoSuchAlgorithmException e) {

    e.printStackTrace();

    return false;

    }

    return true;

    }



    /**

    * A native method that is implemented by the 'k0801' native library,

    * which is packaged with this application.

    */

    public native String stringFromJNI();

    }

    hook脚本

    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
    // 定义 hook 主函数
    function hook_java_main(){
    // Java.perform 确保代码在 Java 虚拟机运行后执行
    Java.perform(() => {

    // 获取 Java 类 a.b.k0801.MainActivity 的句柄
    var cls_MainActivity = Java.use("a.b.k0801.MainActivity");

    // 获取 checkSN(String, String) 方法的特定重载版本
    var fun_checkSN = cls_MainActivity.checkSN.overload('java.lang.String', 'java.lang.String');

    // 重写该方法的实现逻辑
    fun_checkSN.implementation = function(p0, p1){
    // 打印原始输入参数
    console.log("checkSN in:", p0, p1);

    // 替换参数为固定值
    p0 = "cccc";
    p1 = "dddd";

    // 调用原始方法逻辑(此处已经替换了参数)
    var ret = this.checkSN(p0, p1);

    // 修改返回值为 true,无论原始逻辑返回什么
    ret = true;

    // 打印输出结果
    console.log("checkSN out:", ret);

    // 返回修改后的值
    return ret;
    };

    // 打印 hook 完成的提示信息
    console.log("hook_java_main run over");
    });
    }

    // 延迟执行 hook 函数,确保目标应用已加载完成
    setTimeout(hook_java_main, 0);

    Hook Native

    Hook Native层中调用的函数并且读取传入的参数

    先来看看模版

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Interceptor.attach(targetAddress, {
    onEnter: function (args) {
    console.log('Entering ' + functionName);
    // Modify or log arguments if needed
    },
    onLeave: function (retval) {
    console.log('Leaving ' + functionName);
    // Modify or log return value if needed
    }
    });

    Interceptor.attach:将回调函数附加到指定的函数地址。targetAddress 应该是我们想要挂钩的本地函数的地址。
    onEnter:当挂钩的函数被调用时,调用此回调。它提供对函数参数 (args) 的访问。
    onLeave:当挂钩的函数即将退出时,调用此回调。它提供对返回值 (retval) 的访问。

    需要获取targetAddress我们可以方便的使用如下API:

    Module.enumerateExports()
    通过调用 Module.enumerateExports(),我们可以获取到导出函数的名称、地址以及其他相关信息。这些信息对于进行函数挂钩、函数跟踪或者调用其他函数都非常有用。

    Module.getExportByName()
    当我们知道要查找的导出项的名称但不知道其地址时,可以使用 Module.getExportByName()。通过提供导出项的名称作为参数,这个函数会返回与该名称对应的导出项的地址。

    Module.findExportByName()
    这与 Module.getExportByName() 是一样的。唯一的区别在于,如果未找到导出项,Module.getExportByName() 会引发异常,而 Module.findExportByName() 如果未找到导出项则返回 null。让我们看一个示例。

    Module.getBaseAddress()
    通过调用 Module.getBaseAddress() 函数,我们可以获取指定模块的基址地址,然后可以基于这个基址地址进行偏移计算,以定位模块内部的特定函数、变量或者数据结构

    Module.enumerateImports()
    通过调用 Module.enumerateImports() 函数,我们可以获取到指定模块导入的外部函数或变量的名称、地址以及其他相关信息。

    参考: https://blog.csdn.net/qq_24481913/article/details/136546413

    对于没有导出的函数如何进行hook呢,就要利用ida找出该函数在内存中的便宜加上基址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 比如你知道 libtarget.so 中某函数偏移是 0x1234
    var base = Module.findBaseAddress("libtarget.so");
    var target = base.add(0x1234);

    Interceptor.attach(target, {
    onEnter: function (args) {
    console.log("hooked non-exported function");
    },
    onLeave: function (retval) {
    console.log("return value:", retval);
    }
    });

    Hook修改native层程序返回值

    hook的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function hook(){
    var check_flag = Module.enumerateExports("liba0x9.so")[0]["address"];
    console.log("Func address = ",check_flag);
    Interceptor.attach(check_flag,{
    onEnter:function (args){

    },onLeave:function (retval){
    console.log("Origin retval : ",retval);
    retval.replace(1337);
    }
    })
    }
    function main(){
    Java.perform(function (){
    hook();
    })
    }
    setImmediate(hook);

    调用native层中未被调用的方法

    1
    2
    3
    var native_adr = new NativePointer(<address_of_the_native_function>);
    const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
    native_function(<arguments>);
    1
    var native_adr = new NativePointer(<address_of_the_native_function>);

    要在 Frida 中调用一个本地函数,我们需要一个 NativePointer 对象。我们应该将要调用的本地函数的地址传递给 NativePointer 构造函数。接下来,我们将创建 NativeFunction 对象,它表示我们想要调用的实际本地函数。它在本地函数周围创建一个 JavaScript 包装器,允许我们从 Frida 调用该本地函数。

    1
    const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);

    第一个参数应该是 NativePointer 对象,第二个参数是本地函数的返回类型,第三个参数是要传递给本地函数的参数的数据类型列表。现在我们可以像在 Java 空间中那样调用该方法了。

    1
    native_function(<arguments>);

    看看例子

    发现就是在主函数中加载了stringFromJNI,没有关于flag的信息,但是有未被调用的flag函数,我们直接使用hook调用它输出log

    hook代码

    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
    function hook(){
    var a = Module.findBaseAddress("libfrida0xa.so");
    var b = Module.enumerateExports("libfrida0xa.so");
    var get_flagaddress = null;
    var mvaddress = null;
    for(var i = 0 ; b[i]!= null ; i ++ ){
    // console.log(b[i]["name"])
    if(b[i]["name"] == "_Z8get_flagii"){
    console.log("function get_flag : ",b[i]["address"]);
    console.log((b[i]["address"] - a).toString(16));
    // mvaddress = b[i]["address"] - a;
    get_flagaddress = b[i]["address"];
    }
    }
    console.log(ptr.toString(16));

    var get_flag_ptr = new NativePointer(get_flagaddress);
    const get_flag = new NativeFunction(get_flag_ptr,'char',['int','int']);
    var flag = get_flag(1,2);
    console.log(flag)
    //console.log(b);
    }

    function main(){
    Java.perform(function (){
    hook();
    })
    }
    setImmediate(main)

    主动调用native中的函数

    1、new NativeFunction(要调用的函数地址,函数返回值,[参数]);
    2、参数需要保证在内存里,所以字符串要申请内存 Memory.allocUtf8String()

    如下演示了一个写入字符到文件中的示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var fopen_addr = Module.findExportByName("libc.so", "fopen");
    var fopen = new NativeFunction(fopen_addr, "pointer", ["pointer", "pointer"]);
    var fputs_addr = Module.findExportByName("libc.so", "fputs");
    var fputs = new NativeFunction(fputs_addr, "int", ["pointer", "pointer"]);
    var fclose_addr = Module.findExportByName("libc.so", "fclose");
    var fclose = new NativeFunction(fclose_addr, "int", ["pointer"]);
    var filename = Memory.allocUtf8String("/sdcard/a.txt");
    var mod = Memory.allocUtf8String("a");
    var neirong = Memory.allocUtf8String("aaaaaaaa\r\nbbbb\r\b");
    var fp = fopen(filename, mod);
    fputs(neirong, fp);
    fclose(fp);

    打印堆栈

    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
    // 打印 Java 层调用堆栈
    function printJavaStack() {
    Java.perform(function () {
    var Exception = Java.use("java.lang.Exception");
    var instance = Exception.$new("print_stack"); // 创建一个 Exception 对象,构造参数用于标记用途
    var stack = instance.getStackTrace(); // 获取当前 Java 调用栈(返回 StackTraceElement[])
    console.log(stack); // 打印堆栈信息(可能为数组对象)
    instance.$dispose(); // 释放 Java 对象,防止内存泄漏
    });
    }

    // 打印 native 层调用堆栈
    function printNativeStack(context, str_tag)
    {
    console.log("\r\n=============================" + str_tag + " Stack in =======================\r\n");
    // 使用 Thread.backtrace 获取 native 层调用堆栈
    // Backtracer.ACCURATE:使用更准确的堆栈回溯(可能性能略低)
    // DebugSymbol.fromAddress:将地址转换为可读的函数名等符号信息
    console.log(Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'));
    console.log("\r\n=============================" + str_tag + " Stack out =======================\r\n");
    }

    // Hook Java 方法 MainActivity.z()
    function hook_java_main(){
    Java.perform(function () {
    var cls_act = Java.use("a.b.k0805.MainActivity"); // 获取 MainActivity 类
    var fun_z = cls_act.z.overload(); // 获取无参 z() 方法的重载
    fun_z.implementation = function(){
    console.log("hook java z() in"); // 进入 hook

    var ret = this.z(); // 调用原始方法

    printJavaStack(); // 打印 Java 层堆栈
    console.log("hook java z() out"); // 离开 hook
    return ret; // 返回原始方法的结果
    }

    console.log("hook_java_main out"); // 表示 Java hook 安装完成
    });
    }

    // Hook native 层函数(未导出,但知道其偏移地址)
    function hook_native_main(){
    var base_so = Module.findBaseAddress("libk0805.so"); // 查找模块基址
    if (base_so){
    console.log("base_so:", base_so); // 输出基址

    var real_addr_z = base_so.add(0x1DFD0); // 假设 z() 函数偏移为 0x1DFD0,计算实际地址

    Interceptor.attach(real_addr_z, {
    onEnter() {
    console.log("hook onEnter z()"); // 进入 native 函数
    printNativeStack(this.context, "current native z()"); // 打印 native 层堆栈
    },
    onLeave(retval) {
    console.log("hook onLeave z()"); // 离开 native 函数
    // 可选:console.log("Return value:", retval);
    }
    });
    }

    console.log("hook_native_main out"); // 表示 native hook 安装完成
    }

    // 延迟 2 秒后执行 native hook(避免模块未加载)
    setTimeout(hook_native_main, 2000);

    // (可选)你可以根据需要启用 Java hook,例如在 onCreate 中调用:
    hook_java_main();

    使用frida 追踪jni函数动静态注册

    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
    // Hook 一个 native 函数,并在其被调用时输出日志
    function hook_one(funName, funAddr) {
    Interceptor.attach(funAddr, {
    onEnter: function(args) {
    console.log(funName, " call in"); // 打印函数进入日志
    },
    onLeave: function(retVal) {
    // 函数返回时未做任何操作,可扩展记录返回值等
    }
    });
    }

    // 查找所有模块中符合条件的导出函数,并对其 Hook
    function hook_explorts() {
    var modules = Process.enumerateModules(); // 获取所有已加载模块信息

    for (var j = 0; j < modules.length; j++) {
    var oneModule = modules[j];

    // 只处理模块名包含 "0808" 的模块(可以是 so 文件名、进程名等)
    if (oneModule.name.indexOf("0808") == -1)
    continue;

    console.log("oneModule:", oneModule.name); // 打印被匹配模块名称

    var funList = oneModule.enumerateExports(); // 获取模块导出的函数列表

    for (var k = 0; k < funList.length; k++) {
    var oneFun = funList[k];

    // 只 Hook 函数名包含 "Java_" 或 "getAAA" 的导出函数
    if (oneFun.name.indexOf("Java_") == -1 && oneFun.name.indexOf("getAAA") == -1)
    continue;

    console.log("oneFun:", oneFun.name); // 打印被匹配的函数名

    // 对该函数进行 Hook
    hook_one(oneFun.name, oneFun.address);
    }
    }
    }

    // Java 层执行入口
    function hook_java_main() {
    Java.perform(function () {
    console.log("hook_java_main in");

    // 在 Java VM 环境中执行 Native Hook 操作
    hook_explorts();

    console.log("hook_java_main out");
    });
    }

    // 等待 Java 虚拟机启动后再执行 Hook
    setTimeout(hook_java_main, 2000);