逆向工程 --- Android JNI,从一个简单示例开始
前置知识
ARM是嵌入式设备的主流品牌,A系列针对的是高性能的设备比如手机
架构名称 | 支持指令集 | 位数 | 性能 | 适用设备 | 当前状态 |
---|---|---|---|---|---|
arm64-v8a | ARMv8-A | 64位 | 高性能和高能效 | 现代设备(主流架构) | 主流,强烈推荐 |
armeabi-v7a | ARMv7-A | 32位 | 中等性能 | 较旧设备或低端设备 | 次主流,逐渐被替代 |
armeabi | ARMv5TE | 32位 | 性能较低 | 非常旧的设备 | 已废弃,不再使用 |
走进JNI
https://blog.csdn.net/shulianghan/article/details/18964835
https://github.com/jp1017/HelloJni?tab=readme-ov-file
java层代码
// 定义包名,用于组织和管理代码 package github.jp1017.hellojni; // 导入需要使用的Android类 import android.os.Bundle; // 用于传递数据 import android.support.design.widget.FloatingActionButton; // Material Design浮动按钮 import android.support.design.widget.Snackbar; // Material Design提示条 import android.support.v7.app.AppCompatActivity; // 应用程序活动基类 import android.support.v7.widget.Toolbar; // 工具栏组件 import android.util.Log; // 日志工具 import android.view.Menu; // 菜单 import android.view.MenuItem; // 菜单项 import android.view.View; // 视图基类 import android.widget.Toast; // 提示框组件 // MainActivity类,继承自AppCompatActivity public class MainActivity extends AppCompatActivity { /********************************** JNI 开始 *********************************/ /** * 静态代码块加载库 * 在类加载时就会执行,加载名为"hello_jni"的native库 */ static { System.loadLibrary("hello_jni"); } // 声明native方法,使用静态注册方式 public native String staticRegFromJni(); //静态方法注册,实现在C代码中 // 声明native方法,使用动态注册方式 public native String dynamicRegFromJni(); //动态方法注册,实现在C代码中 /********************************** JNI 结束 *********************************/ // Activity创建时调用的方法 @Override protected void onCreate(Bundle savedInstanceState) { // 调用父类的onCreate方法 super.onCreate(savedInstanceState); // 设置界面布局 setContentView(R.layout.activity_main); // 初始化工具栏 Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); // 初始化浮动按钮 FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); // 设置按钮点击监听器 fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { // 创建并显示一个Snackbar提示条 Snackbar.make( view, // 指定Snackbar的显示位置(通常是当前视图) "替换为你的操作", // 显示的提示文本 Snackbar.LENGTH_LONG // 显示时长:LENGTH_LONG约2.5秒 ) .setAction( "操作", // 操作按钮的文本 null // 按钮点击监听器,null表示不执行任何操作 ) .show(); // 显示Snackbar } }); } // 创建选项菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { // 加载菜单布局文件,将菜单项添加到操作栏 getMenuInflater().inflate(R.menu.menu_main, menu); return true; } // 处理菜单项选择事件 @Override public boolean onOptionsItemSelected(MenuItem item) { // 获取被点击的菜单项ID int id = item.getItemId(); // 处理设置菜单项的点击 if (id == R.id.action_settings) { return true; } // 如果不是设置菜单,交由父类处理 return super.onOptionsItemSelected(item); } /** * 按钮点击事件处理方法 * @param view 被点击的视图对象 */ public void onClick(View view) { // 根据被点击的按钮ID执行相应操作 switch (view.getId()) { case R.id.bt_static: // 调用静态注册的native方法并记录日志 Log.d("日志", staticRegFromJni()); // 显示Toast提示 Toast.makeText(this, staticRegFromJni(), Toast.LENGTH_SHORT).show(); break; case R.id.bt_dynamic: // 调用动态注册的native方法并记录日志 Log.d("日志", dynamicRegFromJni()); // 显示Toast提示 Toast.makeText(this, dynamicRegFromJni(), Toast.LENGTH_SHORT).show(); } } }
c层代码
#include <jni.h> /********* 静态注册方法 staticRegFromJni ***********/ //方法名:Java_完整包名类名_方法名(); /** * env : 代表Java环境, 通过这个环境可以调用Java中的方法。类似 * obj : 代表调用JNI方法的对象, 即MainActivity对象。类似this指针 */ JNIEXPORT jstring JNICALL Java_github_jp1017_hellojni_MainActivity_staticRegFromJni(JNIEnv * env, jobject obj) { return (*env) -> NewStringUTF(env, "静态注册调用成功"); //C 的字符串转成 Java 的字符串返回。JNI本质上也是一种c和java的中间库,它可以调用C方法。JNI函数表(JNINativeInterface)是一个包含了所有 JNI 函数指针的结构体,它就像是一个"功能目录表"。将字符串关联到对应多线程env } /********* 动态注册方法 dynamicRegFromJni ***********/ static jstring nativeDynamicRegFromJni(JNIEnv *env, jobject obj) { return (*env) -> NewStringUTF(env, "动态注册调用成功"); } //方法数组,正是这个,可以动态调用任意 native 方法 ,dynamicRegFromJni是java的方法,()Ljava/lang/String;是java的方法签名 JNINativeMethod nativeMethod[] = {{"dynamicRegFromJni", "()Ljava/lang/String;", (void*)nativeDynamicRegFromJni}}; // JNIEXPORT // JNI导出标记,让这个函数可以被Java层调用 // jint // 返回一个整数(Java的int) // JNICALL // JNI调用约定,定义如何参数压栈 // JNI_OnLoad // 函数名,这是JNI库加载时的钩子函数 // ( // JavaVM *jvm, // Java虚拟机实例的指针 // void *reserved // 保留参数,通常不使用 // ) JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { JNIEnv *env; if ((*jvm) -> GetEnv(jvm, (void**) &env, JNI_VERSION_1_4) != JNI_OK)//获取jvm的env到env中并判断版本相等。 jvm Java虚拟机实例,输出参数,env 用于存储获取到的JNIEnv指针,JNI_VERSION_1_4 期望的JNI版本号 { return -1; } jclass clz = (*env) -> FindClass(env, "github/jp1017/hellojni/MainActivity"); //获取MainActivity类,clz 用于存储找到的MainActivity类 (*env) -> RegisterNatives(env, clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0])); //注册native方法,将nativeMethod数组中的方法注册到MainActivity类中 return JNI_VERSION_1_4; }
反编译c
在反编译的角度。通过比较两个反编译器,还是ghidra厉害,ida错误多且可读性差,但是ghidra有一个重大错误,返回值是空。还是建议使用ghidra的反编译功能。而且ghidra支持多个指令集的反编译,ida只支持x86
我们分析一下错误如何产生的。
通过分析指令集ret,发现没有正确读取w0的值,属于软件缺陷
之后的分析在不说明的情况下默认是ghidra
1二进制和伪代码比对。
arm64反汇编
高地址 ┌────────────┐ sp+24├─local_8───┤ 栈保护值 sp+16├─local_10──┤ JNIEnv指针 sp+8 ├─saved_x30─┤ 保存的返回地址 sp+0 ├─saved_x19─┤ 保存的x19值 └────────────┘ 低地址 void JNI_OnLoad(long *param_1) 001006ec ff 83 00 d1 sub sp, sp, #0x20 ; sp=sp-32 在栈上分配4个变量,64位系统中一个变量占8位 001006f0 82 00 80 52 mov w2, #0x4 ; w2=4 long *local_10; 001006f4 e1 43 00 91 add x1, sp, #0x10 ; 参数2 x1=sp+16 001006f8 22 00 a0 72 movk w2, #0x1, LSL #16 ; 参数3 w2=1左移动十六位+w2=0x10004 001006fc f3 7b 00 a9 stp x19, x30, [sp] ; 保存x19和x30(链接寄存器)到栈上,查看ARM64调用约定:x19-x28 是被调用者保存寄存器(callee-saved),如果函数要使用这些寄存器,必须保存。要确定是否需要保存x19的值,我们需要看两点: x19是否在调用前有重要数据。x19在当前函数中的使用 00100700 93 00 00 90 adrp x19, 0x110000 ; 加载栈保护值的地址页 00100704 03 00 40 f9 ldr x3, [x0] ; x3=JNIEnv, x0 寄存器中存放的就是 param_1(jvm)的值,[x0] 表示取 jvm 指向的 JNI 函数表 00100708 64 fe 47 f9 ldr x4, [x19, #offset] ; 加载栈保护值的地址 0010070c 63 18 40 f9 ldr x3, [x3, #0x30] ; x3=JNIEnv->GetEnv函数指针,这里的#0x30表示jni文件中定义 00100710 84 00 40 f9 ldr x4, [x4] ; 加载实际的栈保护值 local_8 = ___stack_chk_guard; 00100714 e4 0f 00 f9 str x4, [sp, #local_8] ; 将栈保护值存储到栈上 iVar1 = (**(code **)(*param_1 + 0x30))(param_1,&local_10,0x10004); 00100718 60 00 3f d6 blr x3 ; GetEnv(x0, x1, w2) if (iVar1 == 0) { 0010071c 60 03 00 35 cbnz w0, LAB_00100788 ; 如果返回值不为0,跳转到错误处理 uVar2 = (**(code **)(*local_10 + 0x30))(local_10,"github/jp1017/hellojni/MainActivity"); 00100720 e2 0b 40 f9 ldr x2, [sp, #local_10] ; x2=local_10,这里的local_10是从jvm中获取JNIEnv并保存在这里的 00100724 01 00 00 90 adrp x1, 0x100000 ; 参数2 x1 = 包含字符串的页面起始地址 00100728 21 60 1f 91 add x1, "github/jp1017/hellojni/MainActivity" ; x1=x1+"github/jp1017/hellojni/MainActivity" 0010072c e0 03 02 aa mov x0, x2 ; 参数1 x0=x2 00100730 42 00 40 f9 ldr x2, [x2] ; x2 = JNIEnv->functions (加载函数表指针) 00100734 42 18 40 f9 ldr x2, [x2, #0x30] ; x2 = functions->FindClass (加载FindClass函数指针) 00100738 40 00 3f d6 blr x2 ; 调用x2 (**(code **)(*local_10 + 0x6b8))(local_10,uVar2,nativeMethod,1); 0010073c e1 03 00 aa mov x1, x0 ; 参数2 x1=x0=上一个调用x2的返回值 00100740 e2 0b 40 f9 ldr x2, [sp, #local_10]; x2=local_10 ,重新加载JNIEnv指针 00100744 23 00 80 52 mov w3, #0x1 ; 参数4 w3=1,设置要注册的方法数量为1 00100748 e0 03 02 aa mov x0, x2 ; 参数1 x0=x2, 0010074c 44 00 40 f9 ldr x4, [x2] ; x4 = JNIEnv->functions (加载函数表指针) 00100750 82 00 00 90 adrp x2, 0x110000 ; 加载本地方法结构体的地址页 00100754 84 5c 43 f9 ldr x4, [x4, #0x6b8] ; x4 = functions->RegisterNatives 00100758 42 f8 47 f9 ldr x2, nativeMethod ; 参数3 x2 =nativeMethod ,加载本地方法结构体 0010075c 80 00 3f d6 blr x4 ; 调用RegisterNatives uVar2 = 0x10004; 00100760 80 00 80 52 mov w0, #0x4 ; 设置返回值为JNI_VERSION_1_4 (0x10004) 00100764 20 00 a0 72 movk w0, #0x1, LSL #16 ; 设置版本号高16位 LAB_00100768: if (local_8 == ___stack_chk_guard) { 00100768 73 fe 47 f9 ldr x19, [x19, #offset]; 重新加载栈保护值地址 0010076c e2 0f 40 f9 ldr x2, [sp, #local_8] ; 加载保存的栈保护值 00100770 61 02 40 f9 ldr x1, [x19] ; 加载当前栈保护值 00100774 5f 00 01 eb cmp x2, x1 ; 比较栈保护值 00100778 c1 00 00 54 b.ne LAB_00100790 ; 如果不相等,检测到栈损坏 # 函数清理和返回 0010077c f3 7b 40 a9 ldp x19, x30, [sp] ; 恢复保存的寄存器 00100780 ff 83 00 91 add sp, sp, #0x20 ; 释放栈空间 00100784 c0 03 5f d6 ret ; 返回w0 LAB_00100788: 对应反编译代码: else { uVar2 = 0xffffffff; } 00100788 00 00 80 12 mov w0, #0xffffffff ; w0=-1 0010078c f7 ff ff 17 b LAB_00100768 ; 跳转到清理代码 LAB_00100790: __stack_chk_fail(uVar2); 00100790 ac ff ff 97 bl __stack_chk_fail ; 调用栈检查失败处理程序
armeabi和armeabi-v7a
; void JNI_OnLoad(JavaVM *vm, void *reserved) ; 函数参数:R0 = JavaVM指针,R1 = 保留参数(未使用) PUSH {R0-R2,R4,R5,LR} ; 保存寄存器到栈上,包括参数和返回地址 ; 栈保护机制初始化 LDR R4, =(__stack_chk_guard_ptr - 0xE4A) ; 加载栈保护值的地址 MOV R1, SP ; 保存栈指针 MOV R1, SP 的作用是: 提供一个栈上的内存位置,用于存储GetEnv函数返回的JNIEnv指针 这个位置正好是之前PUSH指令保存的原始R0值的位置 GetEnv调用完成后,JNIEnv指针会被写入到这个栈位置 这就是为什么后面可以通过 LDR R0, [SP,#0x18+var_18] 来获取JNIEnv指针 这是一个巧妙的设计: 复用了保存原始参数的栈空间 避免了额外分配栈空间来存储JNIEnv指针 保证了后续操作可以方便地访问到JNIEnv指针 ADD R4, PC ; 计算栈保护值的实际地址 LDR R4, [R4] ; 加载栈保护值 LDR R2, =0x10004 ; 加载JNI版本号(1.4) LDR R3, [R4] ; 获取栈保护值 STR R3, [SP,#0x18+var_14] ; 将栈保护值存储到局部变量 ; 获取JNIEnv指针 LDR R3, [R0] ; R3 = JavaVM->函数表 LDR R3, [R3,#0x18] ; R3 = GetEnv函数指针 BLX R3 ; 调用GetEnv CMP R0, #0 ; 检查返回值 BNE loc_E80 ; 如果获取失败,跳转到错误处理 ; 查找Java类 LDR R0, [SP,#0x18+var_18] ; 加载JNIEnv指针 LDR R1, =(aGithubJp1017He - 0xE64) ; 加载类名字符串地址 LDR R3, [R0] ; 获取JNIEnv函数表 ADD R1, PC ; 计算类名字符串的实际地址 LDR R3, [R3,#0x18] ; 获取FindClass函数指针 BLX R3 ; 调用FindClass ; 注册本地方法 MOVS R3, #0xD7 ; 设置偏移量 MOVS R1, R0 ; R1 = 类引用 LDR R0, [SP,#0x18+var_18] ; 重新加载JNIEnv指针 LDR R2, =(nativeMethod_ptr - 0xE76) ; 加载本地方法结构体地址 LSLS R3, R3, #2 ; R3 = 0xD7 << 2 (计算RegisterNatives函数表偏移) LDR R5, [R0] ; 获取JNIEnv函数表 ADD R2, PC ; 计算本地方法结构体实际地址 LDR R2, [R2] ; 加载本地方法结构体 LDR R5, [R5,R3] ; 获取RegisterNatives函数指针 MOVS R3, #1 ; 设置要注册的方法数量为1 BLX R5 ; 调用RegisterNatives ; 返回成功 LDR R0, =0x10004 ; 返回JNI版本号(1.4) B loc_E84 ; 跳转到函数结束处理 loc_E80: ; 错误处理 MOVS R0, #1 ; NEGS R0, R0 ; 返回-1表示错误 loc_E84: ; 函数结束处理 ; 检查栈保护值 LDR R2, [SP,#0x18+var_14] ; 加载保存的栈保护值 LDR R3, [R4] ; 加载当前栈保护值 CMP R2, R3 ; 比较栈保护值 BEQ locret_E90 ; 如果相等,正常返回 BL j___stack_chk_fail ; 否则调用栈保护失败处理 locret_E90: ; 正常返回 POP {R1-R5,PC} ; 恢复寄存器并返回 .text:00000E90 ; --------------------------------------------------------------------------- .text:00000E92 ALIGN 4 .text:00000E94 off_E94 DCD __stack_chk_guard_ptr - 0xE4A .text:00000E94 ; DATA XREF: JNI_OnLoad+2↑r .text:00000E98 dword_E98 DCD 0x10004 ; DATA XREF: JNI_OnLoad+A↑r .text:00000E98 ; JNI_OnLoad+3C↑r .text:00000E9C off_E9C DCD aGithubJp1017He - 0xE64 .text:00000E9C ; DATA XREF: JNI_OnLoad+1C↑r .text:00000E9C ; "github/jp1017/hellojni/MainActivity" .text:00000EA0 off_EA0 DCD nativeMethod_ptr - 0xE76 .text:00000EA0 ; DATA XREF: JNI_OnLoad+2C↑r .text:00000EA0 ; } // starts at E40
让我详细比较armeabiv7a版本与之前版本的所有差异:
- 函数入口和栈操作差异:
; armeabi版本 PUSH {R0-R2,R4,R5,LR} MOV R1, SP ADD SP, SP, #0xC ; 函数退出时调整栈指针 POP {R4,R5,PC} ; 只恢复必要寄存器 ; 之前版本 PUSH {R0-R2,R4,R5,LR} MOV R1, SP POP {R1-R5,PC} ; 直接恢复所有寄存器
- 条件分支指令差异:
; armeabi版本 CBNZ R0, loc_E40 ; 使用更高效的条件分支指令 ; 之前版本 CMP R0, #0 BNE loc_E80 ; 使用比较+分支两条指令
- RegisterNatives函数偏移差异:
; armeabi版本 LDR.W R5, [R3,#0x35C] ; 使用更大的偏移量0x35C ; 之前版本 MOVS R3, #0xD7 LSLS R3, R3, #2 ; 通过移位计算偏移量 LDR R5, [R5,R3]
- 错误处理实现:
; armeabi版本 MOV.W R0, #0xFFFFFFFF ; 直接加载-1,使用32位指令 ; 之前版本 MOVS R0, #1 NEGS R0, R0 ; 通过取反得到-1
- 地址计算和偏移量:
; armeabi版本 LDR R4, =(__stack_chk_guard_ptr - 0xE10) LDR R1, =(aGithubJp1017He - 0xE26) LDR R2, =(nativeMethod_ptr - 0xE2E) ; 之前版本 LDR R4, =(__stack_chk_guard_ptr - 0xE4A) LDR R1, =(aGithubJp1017He - 0xE64) LDR R2, =(nativeMethod_ptr - 0xE76)
- 指令编码和优化:
; armeabi版本
使用.W后缀表示32位指令
代码更紧凑,减少了对齐填充
; 之前版本
使用基本指令集
有更多的对齐填充
- 数据段布局:
; armeabi版本
数据段更紧凑,常量池位置不同
偏移量计算更优化
; 之前版本
数据段较分散,常量池位置靠后
mips
# void JNI_OnLoad(JavaVM *vm, void *reserved) # 参数: $a0 = JavaVM指针, $a1 = reserved(未使用) # 初始化全局指针和栈帧 li $gp, (off_11020+0x7FF0 - .) ; 设置全局指针基址 addu $gp, $t9 ; 调整全局指针位置 addiu $sp, -0x30 ; 分配48字节栈空间 # 保存寄存器到栈 sw $gp, 0x24+var_14($sp) ; 保存全局指针 sw $s1, 0x24+var_s4($sp) ; 保存s1寄存器 sw $s0, 0x24+var_s0($sp) ; 保存s0寄存器 sw $ra, 0x24+var_s8($sp) ; 保存返回地址 # 设置栈保护 la $s1, __stack_chk_guard ; 加载栈保护值地址 lw $v0, 0($a0) ; 加载JavaVM函数表 lw $v1, (__stack_chk_guard - 0x11078)($s1) ; 加载栈保护值 lw $t9, 0x18($v0) ; 获取GetEnv函数指针 # 准备调用GetEnv lui $s0, 1 ; 设置JNI版本高16位 addiu $a1, $sp, 0x24+var_C ; 设置GetEnv第二个参数(JNIEnv**) addiu $a2, $s0, 4 ; 设置版本号0x10004 sw $v1, 0x24+var_8($sp) ; 保存栈保护值 jalr $t9 ; 调用GetEnv nop ; 延迟槽 # 检查GetEnv返回值 bnez $v0, loc_6A0 ; 如果返回非0,跳转到错误处理 lw $gp, 0x24+var_14($sp) ; 恢复全局指针 # 准备调用FindClass lw $a0, 0x24+var_C($sp) ; 加载JNIEnv指针 li $a2, 0 ; 清零 lw $a1, 0($a0) ; 加载JNIEnv函数表 lw $t9, 0x18($a1) ; 获取FindClass函数指针 jalr $t9 ; 调用FindClass addiu $a1, $a2, aGithubJp1017He ; 设置类名参数 # 准备调用RegisterNatives lw $a0, 0x24+var_C($sp) ; 重新加载JNIEnv指针 lw $gp, 0x24+var_14($sp) ; 恢复全局指针 lw $a3, 0($a0) ; 加载JNIEnv函数表 la $a2, nativeMethod ; 加载本地方法结构体地址 lw $t9, 0x35C($a3) ; 获取RegisterNatives函数指针 move $a1, $v0 ; 移动FindClass返回值作为参数 jalr $t9 ; 调用RegisterNatives li $a3, 1 ; 设置方法数量为1 # 设置返回值 lw $gp, 0x24+var_14($sp) ; 恢复全局指针 ori $v0, $s0, 4 ; 返回JNI版本号0x10004 # 检查栈保护值 loc_680: lw $a0, 0x24+var_8($sp) ; 加载保存的栈保护值 lw $t0, (__stack_chk_guard - 0x11078)($s1) ; 加载当前栈保护值 bne $a0, $t0, loc_6A8 ; 如果不相等,跳转到失败处理 # 函数返回清理 lw $ra, 0x24+var_s8($sp) ; 恢复返回地址 lw $s1, 0x24+var_s4($sp) ; 恢复s1 lw $s0, 0x24+var_s0($sp) ; 恢复s0 jr $ra ; 返回 addiu $sp, 0x30 ; 恢复栈指针 # 错误处理路径 loc_6A0: b loc_680 ; 跳转到结束处理 li $v0, 0xFFFFFFFF ; 返回-1表示错误 # 栈检查失败处理 loc_6A8: la $t9, __stack_chk_fail ; 加载栈检查失败函数 jalr $t9 ; 调用栈检查失败处理 nop ; 延迟槽
这是MIPS64架构的JNI_OnLoad实现,让我们比较与之前MIPS32和ARM版本的主要区别:
- 64位指令和寄存器操作:
; MIPS64版本(使用64位操作) daddiu $sp, -0x30 ; 64位立即数加法 sd $gp, 0x10+var_s10($sp) ; 64位存储 ld $v1, 0($a0) ; 64位加载 ; MIPS32版本 addiu $sp, -0x30 ; 32位立即数加法 sw $gp, 0x24+var_14($sp) ; 32位存储 lw $v0, 0($a0) ; 32位加载
- 栈帧布局差异:
; MIPS64版本(8字节对齐) var_10 = -0x10 var_8 = -8 var_s0 = 0 var_s8 = 8 var_s10 = 0x10 var_s18 = 0x18 ; MIPS32版本(4字节对齐) var_18 = -0x18 var_14 = -0x14 var_C = -0xC var_8 = -8
- 函数调用约定:
; MIPS64版本 ld $t9, 0x30($v1) ; 64位函数指针 jalr $t9 ; 调用函数 ; MIPS32版本 lw $t9, 0x18($v0) ; 32位函数指针 jalr $t9 ; 调用函数
- 立即数加载:
; MIPS64版本 dli $v0, 0xFFFFFFFFFFFFFFFF ; 64位立即数 dla $t9, __stack_chk_fail ; 64位地址加载 ; MIPS32版本 li $v0, 0xFFFFFFFF ; 32位立即数 la $t9, __stack_chk_fail ; 32位地址加载
- JNI函数表偏移:
; MIPS64版本 ld $t9, 0x6B8($a4) ; RegisterNatives偏移 ld $t9, 0x30($v1) ; GetEnv偏移 ; MIPS32版本 lw $t9, 0x35C($a3) ; RegisterNatives偏移 lw $t9, 0x18($v0) ; GetEnv偏移
- 栈保护机制:
; MIPS64版本 sdc2 $25, ___stack_chk_guard($zero) ; 使用协处理器2存储 ; MIPS32版本 sw $v1, 0x24+var_8($sp) ; 直接存储
x86
; int __cdecl JNI_OnLoad(JavaVM *vm) ; 局部变量定义 var_2C = dword ptr -2Ch ; JavaVM指针 var_28 = dword ptr -28h ; 参数2指针 var_24 = dword ptr -24h ; JNI版本号 var_20 = dword ptr -20h ; 方法数量 var_14 = dword ptr -14h ; JNIEnv指针 var_10 = dword ptr -10h ; 栈保护值 arg_0 = dword ptr 4 ; 第一个参数(JavaVM*) ; 函数序言 push esi ; 保存esi寄存器 push ebx ; 保存ebx寄存器 call sub_4A0 ; 调用子函数(可能是获取模块基址) add ebx, (offset off_1FE8 - $) ; 计算GOT表偏移 ; 栈帧设置 lea esp, [esp-24h] ; 分配36字节栈空间 mov eax, [esp+2Ch+arg_0] ; 获取JavaVM参数 lea ecx, [esp+2Ch+var_14] ; 获取JNIEnv指针存储位置 ; 设置栈保护 mov esi, large gs:14h ; 获取栈保护值 mov [esp+2Ch+var_10], esi ; 保存栈保护值 xor esi, esi ; 清零esi ; 准备调用GetEnv mov edx, [eax] ; 获取JavaVM函数表 mov [esp+2Ch+var_24], 10004h ; 设置JNI版本号(1.4) mov [esp+2Ch+var_28], ecx ; 设置GetEnv第二个参数 mov [esp+2Ch+var_2C], eax ; 设置GetEnv第一个参数 call dword ptr [edx+18h] ; 调用GetEnv ; 检查GetEnv返回值 test eax, eax ; 检查返回值 jnz short loc_5A8 ; 如果非零跳转到错误处理 ; 准备调用FindClass mov eax, [esp+2Ch+var_14] ; 获取JNIEnv指针 lea ecx, (aGithubJp1017He - 1FE8h)[ebx] ; 加载类名字符串 mov edx, [eax] ; 获取JNIEnv函数表 mov [esp+2Ch+var_28], ecx ; 设置类名参数 mov [esp+2Ch+var_2C], eax ; 设置JNIEnv指针 call dword ptr [edx+18h] ; 调用FindClass ; 准备调用RegisterNatives mov edx, [esp+2Ch+var_14] ; 重新获取JNIEnv指针 lea esi, (nativeMethod - 1FE8h)[ebx] ; 加载本地方法结构体 mov ecx, [edx] ; 获取JNIEnv函数表 mov [esp+2Ch+var_20], 1 ; 设置方法数量为1 mov [esp+2Ch+var_24], esi ; 设置本地方法结构体 mov [esp+2Ch+var_28], eax ; 设置类引用 mov [esp+2Ch+var_2C], edx ; 设置JNIEnv指针 call dword ptr [ecx+35Ch] ; 调用RegisterNatives ; 设置返回值 mov eax, 10004h ; 返回JNI版本号 loc_58E: ; 函数返回处理 mov esi, [esp+2Ch+var_10] ; 获取保存的栈保护值 xor esi, large gs:14h ; 检查栈保护值 jnz short loc_5AF ; 如果不匹配跳转到失败处理 ; 函数清理和返回 lea esp, [esp+24h] ; 恢复栈指针 pop ebx ; 恢复ebx pop esi ; 恢复esi retn ; 返回 ; 错误处理路径 loc_5A8: mov eax, 0FFFFFFFFh ; 返回-1 jmp short loc_58E ; 跳转到返回处理 ; 栈保护失败处理 loc_5AF: call sub_480 ; 调用栈检查失败处理
x86 64
让我比较这段x86_64架构的JNI_OnLoad与之前x86(32位)版本的主要区别:
- 寄存器使用差异:
; x86_64版本 (使用64位寄存器) mov rdi, [rsp+18h+var_18] ; 使用rdi作为第一个参数 mov rsi, rsp ; 使用rsi作为第二个参数 ; x86版本 (使用32位寄存器) mov eax, [esp+2Ch+arg_0] ; 使用eax mov ecx, [esp+2Ch+var_14] ; 使用ecx
- 调用约定差异:
; x86_64版本 (System V AMD64 ABI) rdi - 第一个参数 rsi - 第二个参数 rdx - 第三个参数 rcx - 第四个参数 ; x86版本 (cdecl) 通过栈传递参数 [esp+2Ch+arg_0] - 第一个参数
- 栈帧管理:
; x86_64版本 lea rsp, [rsp-18h] ; 更简洁的栈空间分配 lea rsp, [rsp+18h] ; 释放栈空间 ; x86版本 lea esp, [esp-24h] ; 32位栈操作 lea esp, [esp+24h] ; 释放栈空间
- 栈保护实现:
; x86_64版本 mov rax, fs:28h ; 64位段寄存器偏移 mov [rsp+18h+var_10], rax ; 保存64位栈保护值 ; x86版本 mov esi, large gs:14h ; 32位段寄存器偏移 mov [esp+2Ch+var_10], esi ; 保存32位栈保护值
- 函数调用方式:
; x86_64版本 call qword ptr [rax+30h] ; 64位函数指针调用 call qword ptr [r8+6B8h] ; 使用r8扩展寄存器 ; x86版本 call dword ptr [edx+18h] ; 32位函数指针调用 call dword ptr [ecx+35Ch] ; 使用基本寄存器
- 代码布局:
; x86_64版本
更紧凑的代码布局
不需要显式的GOT表访问
; x86版本
需要通过ebx访问GOT表
需要更多的对齐和填充
主要改进:
- 更大的寄存器空间(64位)
- 更高效的参数传递(寄存器传参)
- 更简洁的代码结构
- 更大的寻址空间
- 额外的寄存器(r8-r15)
- 更现代的调用约定
但基本功能流程相同:
- 设置栈帧和保护
- 调用GetEnv获取JNIEnv
- 查找Java类
- 注册本地方法
- 返回JNI版本
x86_64版本的代码更简洁高效,这得益于:
- 更多的寄存器
- 更简单的调用约定
- 64位架构的优势
- 更现代的ABI设计
对arm64的二进制分析
HelloJni_1.0.apk改名为HelloJni_1.0.apk.zip解压后有如下数据包
安卓支持很多不同cpu框架上运行,但是随着不断的发展淘汰了mips和arm32位,目前arm64位和x86成为了主流支持cpu