freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

安卓逆向-Native 方法的静态注册和动态注册详解与实战(上)
2021-09-09 16:04:12

前言

当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。注册分为 静态注册 和 动态注册 两种。默认的实现方式即静态注册。

一、Eclipse安装与配置

1、Eclipse安装

双击运行exe文件

image-20210817112713980

选择项目保存的目录

image-20210817112720919

首先Eclipse创建一个工程

image-20210817112856119

项目名称

image-20210817112918125

一直默认即可

image-20210817112926309

选择图标

image-20210817112936019![im

image-20210817112951976

创建成功

image-20210817113012867

2、Eelipse配置

配置java编译环境:

image-20210817113122726

image-20210817113210187

配置当前文件的编码

image-20210817113537881

添加LogCat,可以看到安装的一些输出的log信息

image-20210817113632499

image-20210817113639181

配置java代码提示:(配置之后输入java关键字前几个字母后,会提示java关键字后面部分)

abcdefghijklmnopqrstuvwxyz

image-20210817114042889

image-20210817114001788

二、Native 方法的静态注册和动态注册

1、前言

当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。
注册分为 静态注册 和 动态注册两种。默认的实现方式即静态注册。

2、Native 方法的静态注册

NDK 开发中通过 javah -jni 命令生成的包含 JNI 的头文件,接口的命名方式一般是 Java_
,程序执行时系统会根据这种命名规则来调用对应的 Native 方法,这种注册方式称之为静态注册。

静态注册方式的优点:方便简单,IDE (高版本的 AndroidStudio)就可以自动帮你完成;

静态注册方式的缺点:JNI (Native 方法)名字过长,可读性差,由于命名规则的限制,不能灵活改变。

3、Native 方法的动态注册

由于静态注册存在命名局限性,生产环境中一般不采用静态注册的方式。动态注册的优点是可以自由命名 Native 方法,缺点是如果 Native 方法过多,操作比较麻烦。

动态注册的时机是在加载函数库(.a 或 .so)的时候进行注册,即在JNI_OnLoad方法里进行注册。

三、静态注册-案例一

目标:通过C层返回java层一个字符串并在真机的app中显示

1、java层代码

来到MainActivity,java文件,

image-20210817114255912

删除onCreateOptionsMenu方法,因为用不上

在定义一个方法

public native string Getsting();

用native关键字修饰方法,Native方法一般用于两种情况(这里是与C语言联动):

1)在方法中调用一些不是由java语言写的代码。
2)在方法中用java语言直接操纵计算机硬件。

image-20210817114723582

这里string类型报错,先等等,我们之后在修改

这里已经定义了一个被native修饰的方法,那么修饰这个方法就要做一些操作,在java层用Toast展示出来:

Toast 是一个 View 视图,快速的为用户显示少量的信息。
Toast 在应用程序上浮动显示信息给用户,它永远不会获得焦点,不影响用户的输入等操作,主要用于 一些帮助 / 提示。
Toast 最常见的创建方式是使用静态方法 Toast.makeText。

image-20210817205804479

Toast.makeText()方法的参数接收解释

context:上下文
text:弹出的内容字符
duration:显示的时长

修改Toast.makeText()方法的参数

context改为this上下文,

text改为我们的方法Getsing(),

duration改为Toast.LENGTH_SHORT 或者修改为1显示时长
添加.show():输入结果

image-20210817210429443

我们发现在Getsing()部分和string部分都报错了

image-20210821231335874

去除报错

image-20210821231500754

image-20210817210459790

点击处置修改错误后,定义的Getsting()静态方法前面的数据类型由string修改为CharSequence

CharSequence类是java.lang包下的一个接口,此接口对多种不同的对char访问的统一接口,像String、StringBuffer、StringBuilder类都是CharSequence的子接口;
CharSequence类和String类都可以定义字符串,但是String定义的字符串只能读,CharSequence定义的字符串是可读可写的;
对于抽象类或者接口来说不可以直接使用new的方式创建对象,但是可以直接给它赋值;

image-20210821232435645

这时候java层的简单操作就写好了。

2、编译.h文件

我们需要获取src目录位置

image-20210817232215140

image-20210817232228676

复制该目录,cmd进入

E:\data\yunjiananquan\src

image-20210817232330198

使用javah执行命令

javah 生成实现本地方法所需的 C 头文件和源文件。C 程序用生成的头文件和源文件在本地源代码中引用某一对象的实例变量。.h 文件含有一个 struct 定义,该定义的布局与相应类的布局平行。该 struct 中的域对应于类中的实例变量。
-jni 生成JNI样式的标头文件(默认值)

image-20210817232553436

-jni需要一个参数:完整路径

包名加类名,就是完整路径

com.example.yunjiananquan.MainActivity	#完整路径

image-20210817232818836

生成.h文件

javah -jni com.example.yunjiananquan.MainActivity

image-20210821232719073

注意:编译出现中文提示编码GBK的不可映射字符,加上-enconding UTF-8即可

javah -encoding UTF-8 -jni com.example.yunjiananquan.MainActivity

image-20210820000141525

但是在Eclipse无显示,这里需要刷新一下安卓项目,或者按F5

image-20210817233129975

这时候我们看到在src(source)同目录下生成了一个.h的文件

image-20210817233307293

3、分析.h文件

开始分析com_example_yunjiananquan_MainActivity.h文件

首先第一行代码

#include <jni.h>	
#include是preprocessor的一条指令, 负责把 #include 的文件中的内容全部copy到当前文件的#include所在的位置,也就是用该文件中的内容取代#include 这条指令。这里的意思是引入jni.h文件

image-20210817233824047

接下来,在c语言中,头文件中的加入 #ifndef #define #endif目的防止该头文件被重复引用

“被重复引用”是指一个头文件在同一个c文件中被include了多次,这种错误是由于include嵌套造成的。比如在a.h文件#include "c.h" 而此时b.cpp文件包含#include "a.h" 和#include "c.h"此时就会造成c.h重复引用

可以理解防止重名,防止重复导入,就是提前做一个预判,如果有异常会进行一些处理

image-20210817234107065

extern关键字:在C语言中全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。如果在定义点之前的函数想引用该全局变量,则应该在引用之前用关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。有了此声明,就可以从“声明”处起,合法地使用该外部变量。

image-20210817234545953

下面就是重点JNI接口方法

详细可以参考我之前写过的文章

https://forum.butian.net/share/483

这是更加java层的Getsting()方法生成的JNI接口的方法

JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
  (JNIEnv *, jobject);

jobject:返回值
Java_com_example_yunjiananquan_MainActivity_Getsting:方法名
(JNIEnv *, jobject):参数

image-20210817234559777

JNI文件生成分析好了,我们接下来要开始通过NDK编译SO文件了

4、NDK编译SO文件准备

**NDK编译方式:**需要放在$PROJECT/jni/目录下, 其中$PROJECT表示你的工程目录,这样就可以被ndk-build脚本文件找到.(注:在这种方式下,进入jni目录,即$PROJECT/jni/,然后执行ndk-build,就可以直接编译jni生成.so文件了)。

在NDK编译中,最少需要三个文件,这里需要四个,需要把.h文件放在jni目录下:

test.c
Application.mk
Android.mk

首先创建一个jni目录

image-20210817235229805

image-20210817235258259

创建好jni文件后,把.h文件修改一下名称,修改为yunjian.h(名字随意,后缀名不变即可)

image-20210817235417172

image-20210817235545908

将Yunjian.h拖到jni文件夹中

image-20210821234505384

5、编写.C文件

开始编写c文件,创建Yunjian.c

image-20210818000044355

image-20210818000129604

开头引入Yunjian.h

#include <Yunjian.h>

image-20210818000650023

然后开始写函数名,函数是从Yunjian.h中Java_com_example_yunjiananquan_MainActivity_Getsting函数,复制并放在C文件中

image-20210818000714522

将后面的;删除改为{},这里代表方法体

JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
  (JNIEnv *, jobject);

image-20210818001037880

这里我们说一下函数的三要素是什么呢?

返回值、参数、方法体

那些现在参数都有了吗?目前只有两个类型,但是只有参数类型,需要给参数取名称

第一个参数类型是:JNIEnv 我们随便取名,就叫yunj(*要保留,放在参数前面)

第二个参数类型是:jobject 我们叫obj(参数名可随意修改,传统写法是env obj)

JNIEXPORT jobject JNICALL Java_com_example_yunjiananquan_MainActivity_Getsting
  (JNIEnv *yunj,jobject obj){}

image-20210819010128589

参数补全之后还差什么呢,因为方法名是get开头,所以最终我们要给jobject返回一个回显内容,目的是从c层返回一个字符串给java层

image-20210821235434420

如果在return后面加上yunjiananquan,这时候就可以让这个字符串返回到java层了吗?

显然是不能的,这里需要我们借助一个JNI接口:NewStringUTF方法

jstring     (*NewStringUTF)(JNIEnv*, const char*);

image-20210819005432665

直接放进来,需要使用的话还需要定义参数,不然yunjiananquan无法从c层返回到java层!

image-20210819010214811

首先yunjiananquan是个字符串,那么可以使用str接收,使用JNIEnv指针指出:

image-20210819173337361

将NewStringUTF方法外面的括号和*删除,并补充参数

第一个参数为JNIEnv*,与上面的参数一样,所以这里修改为:yunj

第二个参数为const char*,修改为传入值“yunjiananquan”,不然她无法返回java层。

image-20210819173556883

const char*修改为传入值yunjiananquan,不然她无法返回java层:“yunjiananquan”,并把return返回的内容修改为str变量

image-20210819174221159

回顾一下我们前面的操作

通过java生成一个.h文件,在.c文件首先引入.h文件,然后复制过来.h文件中的方法,定义两个参数名,将最后的分号改为{}方法体加载jnu接口中NewStringUTF方法,然后用到了NewStringUTF要求把yunjiananquan传入java层,为了实现传出,NewStringUTF是在头库中找到的,然后改了两个参数类型,改了两个参数类型后,然后提出str参数去接收yunjiananquan,那么最后直接return把yunjiananquan传过去。

6、Android.mk编写

这时候还不能传过去,因为还缺少两个mk文件

Android.mk

LOCAL_PATH := $(call my-dir)	#获取jni文件路径
include $(CLEAR_VARS)
LOCAL_MODULE := Yunjian	#模块名称
LOCAL_SRC_FILES := Yunjian.c	#源文件 .c或者.cpp
LOCAL_ARM_MODE := arm	#编译后的指令集ARM指令
LOCAL_LDLIBS += -llog	#依赖库
include $(BUILD_SHARED_LIBRARY)	#指定编译文件的类型.so

image-20210822003320734

7、Android.mk编写

Application.mk

APP_ABI := armeabi-v7a

image-20210822003413581

注意:如果发现注释#后面的中文乱码无法显示的问题可以配置

image-20210819175903045

image-20210819175911806

8、NDK编译so文件并在真机运行

1、生成so文件

下面来到jni目录下,通过ndk-build命令生成so文件

image-20210819230013010

刷新F5,就可以在libs看到so文件了

image-20210819230110012

2、修改java层代码

这时候如果想让java代码联动C文件,需要装载库文件,System.loadLibrary("Yunjian");就是载入一个JNI库文件

static{ } : 静态代码块,类加载的时候,先执行里面的命令

static{
 	System.loadLibrary("Yunjian");
 }

image-20210819230544042

3、Eclipse连接真机测试

连接真机(root过后的),执行

image-20210819232836087

image-20210819232920409

我们看到成功在真机上弹出窗口

image-20210819232759036

但是显示时间只有2秒钟,怎么加显示时间呢,在MainAcivity.java中修改,在重新生成.h文件,在ndk-build生成.so文件

image-20210819233138714

四、静态注册-案例二

目标:在java层设置普通变量和静态变量,C层通过不同的方法分别调用java层的变量,并返回给java,通过app展示

1、java层代码

继续深入学习,来定义两个变量

在MainAcivity类中定义了两个变量,一个是普通的、一个是静态的

public String yunjian1 = "我是yunjian1";
	public static String yunjian2 = "我是静态yunjian2";

image-20210819233824419

在定义native修饰的两个方法

//获取普通的变量yunjian1
    public native CharSequence GetStingyunjian1();

  //获取普通的静态变量yunjian2
    public native CharSequence Getstingyunjian2();

image-20210819234914900

继续用Toast来调用方法:

//调用GetStingyunjian1()方法
        Toast.makeText(this, GetStingyunjian1(), 1).show();
      //调用GetStingyunjian2()方法
        Toast.makeText(this, Getstingyunjian2(), 1).show();

image-20210819235036653

使用javah -jni生成.h

javah -jni com.example.yunjiananquan.MainActivity

image-20210820000011441

F5刷新,看到了.h文件,在文件中看到了我们刚刚定义的两个方法名称

image-20210820000238231

接下来将.h文件改名改Yunjian.h,并将原jni目录下的Yunjian.h删除,将刚刚生成的文件复制进去

image-20210820000527399

2、修改.c文件

接下来继续将.h文件中的两个方法复制到在.c文件

image-20210820000841252

修改方法的参数名,方法体等

image-20210820001056729

1、配置调用普通方法的JNI接口

开始GetStingyunjian1方法体的配置

有了参数,开始写方法体内容,获取普通变量的方法需要用到jni接口中的:GetObjetField()方法

image-20210820001240563

jobject (*GetObjectField)(JNIEnv*, jobject, jfieldID);

image-20210820001310308

定义指出,返回给str变量

image-20210822005854855

然后修改GetObjectField里面的参数

第一个参数JNIEnv*,默认的,填写yunj

第二个参数jobject,默认的,填写obj

第三个参数jfieldID,需要调用GetFidldID()方法获取返回值

GetFidldID()方法

image-20210820001619809

GetFidldID()方法

jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);

image-20210820001650253

修改GetFidlID()方法指出等信息

image-20210822010732989

补充GetFidlID的参数

JNIEnv* 默认参数,为:yunj
jclass:需要调用FindClass,获取FindClass返回值
const char*在上面章节中将const char*修改为字符串“yunjiananquan",那么这里修改为”yunjian1"变量名
const char*:”yunjian1"变量的类型,unjian1变量定义的为string类型,那么这里返回值填写string的全名

第二个参数jclass,FindClass方法

将FindClass()方法,继续放到.c文件中

jclass (*FindClass)(JNIEnv*, const char*);

image-20210820004032288

image-20210820004540636

修改参数,定义变量

JNIEnv*,默认参数:yunj
const char*,这里填写java类的路径:com.example.yunjiananquan.MainActivity

image-20210820004726826image-20210820004902213

这里java的类名放到C中,需要将.换为/

"com/example/yunjiananquan/MainActivity"

image-20210820005126228

完成FindClass方法编写

继续GetFieldID,目前第二个参数jclass以及确定了,下面配置两个const char*

第三个参数修改为java层”yunjian1"变量名:

image-20210820002343647

第三个参数就是java层的变量名称

第四个参数即使返回值类型,yunjian1变量定义的为string类型,那么这里返回值填写string的全名

image-20210820002558080

java.lang.String

那么在C里面的的写法不一样,这种编码叫做JNI字段描述符,L开头的描述符,就是类描述符,它后紧跟着类的字符串,然后分号“;”结束。

比如"Ljava/lang/String;"就是表示类型String;

"Ljava/lang/String"

image-20210820003930721

到这里GetObjectField()中参数全部定义好,加入return返回str,并修改指出的返回值名,不能和类型一样

image-20210820005329140

到这里获取java层普通变量的方法在c层就写好了,刚开始我们定义了GetObjectField()方法,回来发现缺少变量,就往上套娃。我们仔细观察发现,在上面的三个方法中,FindClass方法定义了java层的类名,GetFieldID方法定义了yunjian1变量的类型和名字,GetObjectField实现调用。

2、配置调用静态方法的JNI接口

那么接下来开始修改java层的获取静态变量方法

构建Java_com_example_yunjiananquan_MainActivity_Getstingyunjian2的方法体,修改参数,变量,return

image-20210822011850110

这里使用GetStaticObjectField()方法获取java层的静态变量

jobject (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);

image-20210820010842390

将GetStaticObjectField()方法加入.c文件,并定义变量,指出,参数等

JNIEnv*	默认参数,为:yunj
jclass 需要调用FindClass,获取FindClass返回值
jfieldID	需要调用GetStaticFieldID方法获取返回值

image-20210820011130396

发现第二参数jclass与上面获取普通字段使用过的方法一样,复制下来即可

//为下面的jclass参数获取内容,GetStaticObjectField方法的第二个参数
jclass jclass=(*yunj)->FindClass(yunj, "com/example/yunjiananquan/MainActivity");

image-20210820011536561

第三个参数jfieldID,怎么获取呢,在上面获取普通变量是使用的GetFieldID,那么获取静态变量,使用:GetStaticFieldID

jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char*,const char*);

image-20210820011733059

继续补全GetStaticFieldID方法的参数指向等

JNIEnv* 默认类型,填写obj
jclass 需要调用FindClass,获取FindClass返回值
const char*参数填写要获取的java层的静态变量名 yunjian2
const char*参数设置静态变量的类型:"Ljava/lang/String"

image-20210820012747218

最后补充return stt。

注意:这时候排错一下,这里方法返回值的类型和返回值的名称不可以一样,需要修改:

image-20210822012734140

3、生成so文件,通过真机测试apk

接下来生成so文件

cd E:\data\Yunjiananquan\src
ndk-build

image-20210820142017630

image-20210820142050405

这似乎我们继续回到java层,写一个静态方法

image-20210820142215351

连接真机,执行查看结果

image-20210820155630776

image-20210820155745984

image-20210820155725225
未完待续。。。。。。

五、总结

1. jni接口:java native interface
2.作用:用于javahe/ c++代码的交互3.使用方法:jni静态注册和jni动态方法
(1) jni静态注册流程
a.在java代码中定义nativef修饰的方法;
b.来到指定路径(src路径)执行javah -jni,根据java中native修饰的方法生成.h头文件;
c.编写C、C++代码,导入头文件,同时实现我们.h头文件中的方法;
d.编写两个mk文件: android.mk文件, application.mk文件(把四个文件放到jni目录);
e.来到指定路径(jni文件夹所在路径)ndk-build生成so文件(完整总结见下篇)
# java # Android # 逆向基础 # NDK
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录