freeBuf
主站

分类

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

特色

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

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

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

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

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

安卓逆向-Native 方法的静态注册和动态注册详解与实战(下)
FreeBuf_362808 2021-09-10 11:57:14 172285

前言

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

安卓逆向-Native 方法的静态注册和动态注册详解与实战(上)
紧接上话继续开始


一、静态注册-案例三

目标:使用C层调用java层次的静普通和静态的方法。

1、修改java文件

现在在java层写入普通方法和静态方法

//定义普通方法
	public void _method(){
		Toast.makeText(this, "我是普通方法",1).show();
	}
	
	//定义静态方法
	public void _staticmethod(){
		Toast.makeText(this, "我是静态方法",1).show();
	}

image-20210821140843809

创建好两个方法还需要调用这一个native修饰过的方法(这一次是要java层的普通方法和静态方法两个方法,在javaToc()中实现调用

//调用静态和普通两个方法
    private native String javaToc();

private:在同一类内可见。使用对象:变量、方法、注意:不能修饰类(外部类)
Native方法一般用于两种情况:在方法中调用一些不是由java语言写的代码;在方法中用java语言直接操纵计算机硬件。

image-20210821141811049

java称代码写完,就要写C文件了,首先生成.h文件

javah -encoding UTF-8 -jni com.example.yunjian.MainActivity
或
javah  -jni com.example.yunjian.MainActivity

image-20210821142051864

image-20210821142111348

重命名名字,删除之前的JNI_yunj.h文件,移动到jni目录

image-20210821142223365

2、修改调用普通方法的JNI接口

1、修改.c文件

将.h文件中的Java_com_example_yunjian_MainActivity_javaToc复制到c文件中,补全参数,方法体等

image-20210821145156788这里用到jni表里找接口:CallVoidMethod

void (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);

image-20210821145255085

补全接口的参数,指出等,因为返回值为void,意思没有返回值

JNIEnv* 默认,填写yunj
jobject 默认,填写obj
jmethodID 需要调用GetMethodID获取
...  参数列表,删除就可以

image-20210821150357047

第三个参数jmethodID需要调用GetMethodID获取

jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);

image-20210821150437445

补全GetMethodID的参数等信息

JNIEnv* 默认 填写yunj
jclass 调用FindClass获取
const char* 为java层的方法名:”_method“
const char* 第四个参数为java层的方法的返回值,但是_method返回值为空,所以这里填写:"()V"

image-20210821150501179

发现GetMethodID第二参数jclass,这里需要调用FindClass

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

image-20210821152723182

补充FindClass的参数等信息

JNIEnv* 默认 填写yunj
const char* java的类的全名:"com/example/yunjiananqian/MainActivity"

image-20210821153045538

FindClass的第二个参数const char*也上面一样,java的类的全名

com/example/yunjiananqian/MainActivity

image-20210821153359602

继续补充GetMethodID的参数

jclass第二个参数为FindClass方法返回值:j_class

const char*第三个参数为java层的方法名:”_method“

image-20210821153653028

const char*第四个参数为java层的方法的返回值,但是_method返回值为空,所以这里填写:"()V"

最后FindClass方法完成状态为:

image-20210821154043362

补全GetMethodID方法,完整变调用普通方法代码

image-20210822014729666

2、ndk编译so文件

C语言文件完成了,下面开始通过ndk编译so文件了

cd E:\data\Yunjian\jni\
ndk-build

image-20210821154658302

代码没有问题,看来调用普通方法的jni接口写好了,开始静态方法调用把

3、修改调用静态方法的JIN接口

1、修改java文件

静态方法需要在重新修改下java层代码

image-20210821162621023

getApplicationContext() 是通过makeApplication创建的 ,所以说你使用getApplicationContext()返回的是一个对象,返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁。getApplicationContext()是在Context 定义的,所以说只要继承Context的子类都可以调用getApplicationContext()方法.

因为java没有新增native修饰的方法,.h文件中不会新增方法,所以不用新生成.h文件

2、修改.c文件

直接在c文件中增加方法,在之前获取普通方法的方法上加上static即可CallStaticVoidMethod,后面的参数列表用不上,删除...就可以了:

void        (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);

image-20210821164624673补充CallStaticVoidMethod方法参数等,因为返回值为void,意思没有返回值

JNIEnv* 默认填写junj
jclass 使用FindClass返回值,但是这个前面已经写了吗
jmethodID:需要调用GetStaticMethodID方法

jclass在一个前面FindClass方法了,直接将FindClass返回值放进来就可以

image-20210822144711549

GetStaticMethodID方法参数

JNIEnv*	默认 输入:yunj
jclass	使用FindClass返回值,输入j_class
const char*  和上面的GetMethodID方法一样,这里是java层静态方法的方法名:"_staticmethod"
const char*  和上面的GetMethodID方法一样,java层静态方法的返回值,这里返回值为空void,输入:"()V"

image-20210822144530235

C语言文件完成了,下面开始通过ndk编译so文件了

cd E:\data\Yunjian\jni\
ndk-build

image-20210821173100698

这个是没有回显的,因为这是个调用的方法

静态注册我们已经可以很清晰的明白概念了,下面让我们开始动态注册的学习

二、动态注册-计算器案例一

目标:编写一个可以加减乘除的简单计算器APP

1、创建项目

创建一个项目

image-20210822224924501

image-20210822224935235

image-20210822224942490

image-20210822224951086

image-20210822225000001

image-20210822225245854

一直默认即可,这时候创建成功一个工程项目

2、编辑app界面

放入两个纯文本框 plain Text

image-20210822225526668

放入两个文本视图,TextView

image-20210822225722306

然后改下名称

image-20210822230437586

修改完名称,有个输入框和描述后,还需要加减乘除的四个框架:Button

image-20210822230809602

修改名称

image-20210822230925679

修改完四个框架为加减乘除符号!

双击四个框架进入,在id处修改:

image-20210822231857378

image-20210822232056045

加:add  减:sub  乘:mul 除:div

修改之后记得保存哦~~~

继续给最初的Text赋予个初始值0:

image-20210822232330689

那么这时候第一部分就完成了,接下来开始写java代码

3、java层编写

首先创建个方法来初始化控件,在MainActivity.java编辑

image-20210823001436961

初始化控件后,绑定之前,先定义一个编辑框的变量

image-20210823001840036

这里先一个变量first,然后first绑定第一个编辑框,但是报错了,是因为没有定义他的类型

image-20210823003530866

定义好第一个编辑框,继续定义第二个编辑框

image-20210823003711778

定义完成两个编辑框后,开始绑定按钮,和上面编辑框差不多,只是类型不同,绑定加减乘除四个按钮

image-20210823004458304

这时候按钮界面就绑定写完了!绑定完成后需要设置按钮监听获取编辑框的值,定义几个加减乘除就需要用到native修饰,实现在so层,先是定义方法定义两个全局变量

image-20210823005137429

定义好两个全家变量,在这是输入加减乘除的输入参数,还需要定义方法来进行加减乘除

定义native方法,实现java与c之间的联动

image-20210823130345454

定义好了四种方法后,接下来要获取两个编辑框中的值,就要用到gettext的tostring方法了

parseFloat()方法是将String 类型装换成 float 类型

toString 能将引用型转换成String 类型

这就话的意思是出入first(输入框)中内容转换为string类型,在转化为Float类型,赋予one变量

image-20210823130608852

继续添加second第二个即可

image-20210823131250300

获得编辑框里的内容后,就要进行一个运算了,定义一个运算方法

image-20210823131445808

创建简单的运算方法,先给add这个方法进行一个按钮绑定的监听,new了一个OnClickListener类,这里需要导入OnClickListener类(点击一下即可),导入进来后,onClick监听按钮,逻辑是通过按钮的ID来执行一个命令操作。

image-20210823131812362

使用swith 对接收的按钮的ID来进行执行不同操作!

image-20210823141711748

将add.setOnClickListener()删除即可,这里的key的view V中,这个V的getid按钮,value的按钮的ID值:

image-20210823142411461

获取之后要进行一个对用的操作,因为获取完之后,需要调用这个方法显示到界面上,那么继续写:

image-20210823143328461

Toast.makeText()三个参数:
context:填写MainActivity.this,代表MainActivity使用中的上下文
text:第二个参数就是我们要获取的add(one,two),代表之前我们定义的add()方法,后面加+"",add返回值为float格式,但是这里需要string格式,所以加个字符拼接。
第三个参数时间改为1,
.show显示下!

完成加法后,开始减乘除的编写

image-20210823143732801

设置完后需要进行一个默认值的绑定:OnClickListener

这里使用OnClickListener进行默认值绑定,定义cl,用final表示这个字段就是最终字段不可修改,然后绑定了加减乘除cl!

image-20210823144036220

这时候编辑框的值要放入case里面,不然点不动会数据异常:

image-20210823144442346

这里就可以动态获取信息了,java层代码告一段落,保存

package com.example.yunjiandongtai;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity {
	
	//第一个编辑框变量
	private EditText first;
	//第二个编辑框变量
	private EditText second;
	
	//加减乘除四个变量
	private Button add;
	private Button sub;
	private Button mul;
	private Button div;
	
	//编辑框内容的变量
	private float one;
	private float two;
	
	 static{
		 	System.loadLibrary("jisuanqi");
		 }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //初始化控件
        init();

        //这就话的意思是出入first(输入框)中内容转换为string类型,在转化为Float类型,赋予one变量
        one = Float.parseFloat(first.getText().toString());
        two = Float.parseFloat(second.getText().toString());

        //开始运算
        yunsuan();

    }




    //运算方法
    private void yunsuan() {
		// TODO Auto-generated method stub
    	final OnClickListener cl = new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				
				switch (v.getId()) {
				case R.id.add:
			        one = Float.parseFloat(first.getText().toString());
			        two = Float.parseFloat(second.getText().toString());
					Toast.makeText(MainActivity.this, add(one,two)+"", 1).show();
					break;

				case R.id.sub:
			        one = Float.parseFloat(first.getText().toString());
			        two = Float.parseFloat(second.getText().toString());
					Toast.makeText(MainActivity.this, sub(one,two)+"", 1).show();
					break;
				
				case R.id.mul:
			        one = Float.parseFloat(first.getText().toString());
			        two = Float.parseFloat(second.getText().toString());
					Toast.makeText(MainActivity.this, mul(one,two)+"", 1).show();
					break;
					
				case R.id.div:
			        one = Float.parseFloat(first.getText().toString());
			        two = Float.parseFloat(second.getText().toString());
					Toast.makeText(MainActivity.this, div(one,two)+"", 1).show();
					break;
				}
			}
		};
		
		add.setOnClickListener(cl);
		sub.setOnClickListener(cl);
		mul.setOnClickListener(cl);
		div.setOnClickListener(cl);
		
	}

	//初始化控件
    private void init() {
		// TODO Auto-generated method stub
    	//绑定第一个编辑框对象
    	first = (EditText) findViewById(R.id.editText1);
    	//绑定第二个编辑框对象
    	second = (EditText) findViewById(R.id.editText2);
		
    	//绑定加按钮的对象
    	add = (Button) findViewById(R.id.add);
    	
    	//绑定减按钮的对象
    	sub = (Button) findViewById(R.id.sub);
    	
    	//绑定乘按钮的对象
    	mul = (Button) findViewById(R.id.mul);
    	
    	//绑定除按钮的对象
    	div = (Button) findViewById(R.id.div);
	}


    //定义native方法,实现与c层联动
    public native float add(float one,float tow);
    public native float sub(float one,float tow);
    public native float mul(float one,float tow);
    public native float div(float one,float tow);

}

4、C层编写

创建jni文件夹

image-20210823153352056

创建jisuanqi.c

image-20210823153435577

开始写C代码,写头文件

头文件就用自带的.h即可

在JNI接口文件中看到有jfloat返回值,那么使用jfloat然后方法add传入四个参数,前面两个是固定的,后面两个才是最重要的需要传入的参数

image-20210823153725668

那么这里创建好了加法,在依此创建减乘除:

image-20210823154123249

这里有个问题,C语言的代码中add、sub、mul、div加减乘除四个方法名和java层需要一样吗?

这里是不需要一样的,因为会只用到jni接口进行java层和c层的方法关联,这里为了区分,修改一下c层的方法名称

image-20210823155256815

接下来使用结构体定义结构体数据:JNINativeMethod

image-20210823170740279

这个数组大括号就是用来绑定c层和java层

image-20210823171447983

可以看到里面三个参数:

第一个是java方法名称

第二个是参数、返回值、签名信息,

第三个是函数指针!(F就是Smali代码里的)

image-20210823172157180

接下来用RegisterNatives方法

image-20210823172434956

需要先定义一个函数和一个参数:

image-20210823172932506

在定义函数中将RegisterNatives方法放进去

image-20210823173033278

env自定义即可,补全指出

image-20210823173306561

补全参数

JNIEnv* 默认 修改为:env
jclass 需要FindClass方法返回值
const JNINativeMethod*  定义的结构体的名称
jint  放入的数组数量

第一个参数

image-20210823173720628

第二个参数涉及FindClass方法返回值

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

image-20210823173822945

补全FindClass方法

JNIEnv*   默认 填写env
const char* 填写java文件的全部名称"com.example.yunjiandongtai.MainActivity"

image-20210823174306071

第三个参数

image-20210823174421515

第四个参数,数量

image-20210823174533169

这时候写完参数之后,还需要进行一个判断使用if,如果不等于(!=)零(JNI_OK),就返回一个-1(JNI_ERR)

image-20210824103701402

image-20210824103941757

registerNative方法在返回一个JNI_OK零!

image-20210824104256556

这里是为了写法严谨改为:

sizeof是一个操作符(operator),其作用是返回一个对象或类型所占的内存字节数。

sizeof(nativeMethod)/sizeof(nativeMethod[0])

image-20210824105310594

这时候注册函数就写完了,开始写动态注册用到的:JNI_OnLoad

image-20210824105639453

image-20210824105809178

先定义一个方法体给:JNI_OnLoad,这里使用GetEnv,

image-20210824110146006

这里GetEnv有三个参数,

第一个参数vm补全

第二个参数void**二级指针指向(&)指针的地址env

第三个参数是版本

image-20210824105911119

这里参数补全后,还需要指出

image-20210824110400283

接下来判断这个env是否获取成功,就要写if条件,这里如果不等于0就返回JNI_ERR(-1)

image-20210824110538198

还需要对上面的注册函数进行判断:registerNative判断,如果registerNative注册不等于0就返回-1,还需要加上env参数

image-20210824110802184

成功最终返回一个版本

image-20210824111005886

这里就完成使用JNI_OnLoad进行动态注册,这里的JNl_OnLoad类似Java里面的类,系统会自动调用!所以逆向在JNl_OnLoad下断点即可分析找->registerNative->RegisterNatives三个参数->Java层和C层绑定的一个逻辑关系!

最终代码

#include <jni.h>

jfloat addc(JNIEnv * env,jobject obj,jfloat a,jfloat b){

	return a+b;
}

jfloat subc(JNIEnv * env,jobject obj,jfloat a,jfloat b){

	return a-b;
}

jfloat mulc(JNIEnv * env,jobject obj,jfloat a,jfloat b){

	return a*b;
}

jfloat divc(JNIEnv * env,jobject obj,jfloat a,jfloat b){

	return a/b;
}

JNINativeMethod nativeMethod[]={
		{"add","(FF)F",(void*)addc},
		{"sub","(FF)F",(void*)subc},
		{"mul","(FF)F",(void*)mulc},
		{"div","(FF)F",(void*)divc}
};

jint registerNative(JNIEnv* env){
	//获取类
	jclass j_class=(*env)->FindClass(env, "com/example/yunjiandongtai/MainActivity");
	//注册
    if((*env)->RegisterNatives(env, j_class,nativeMethod,sizeof(nativeMethod)/sizeof(nativeMethod[0]))!=JNI_OK){

    	return JNI_ERR;
    }

    return JNI_OK;

}

//使用JNI_OnLoad进行动态注册
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
	JNIEnv* env;

	//如果不等于0
    if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4)!=JNI_OK){
    	return JNI_ERR;
    }

    if(registerNative(env)!=JNI_OK){
    	return JNI_ERR;
    }
    return JNI_VERSION_1_4;
}

5、编写Android.mk

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

image-20210824143410979

6、编辑Application.mk

APP_ABI := armeabi-v7a

image-20210824143437997

7、修改java层代码与真机联动

在java层新增代码块,将java层联动jni

image-20210824143944439

运行程序,在真机上测试

image-20210824144428394

完成~~~

三、总结

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文件;
(2)动态注册流程

a.在java代码中定义nativef修饰的方法;
b.新建C/C++文件,导入jni.h头文件,编写c代码,实现java层被native修饰的方法;
c.通过JNINativeMethod结构体绑定java和c、c++方法
d.通过RegisterNatives方法注册java相应的类以及方法;
e.把C/C++注册方法写入到JNI_onload(两个参数VM、保留参数)﹔注意:JNI_inload,是系统调用的;
f.来到指定路径(jni文件夹所在路径)ndk-build生成so文件;
4.两种注册方法有确定的对比:
(1).静态注册:
a.编写不方便,jni方法名字必须遵守规则而且名字长;
b.运行效率不高,不安全;
(2)动态注册:
流程清晰可控;运行效率高;
# c语言 # java # Android # android逆向 # NDK
本文为 FreeBuf_362808 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
移动攻防之路
FreeBuf_362808 LV.4
这家伙太懒了,还未填写个人描述!
  • 35 文章数
  • 63 关注者
web安全基础篇-点击劫持(Click Jacking)
2022-05-27
WEB安全基础篇-跨站脚本攻击(XSS)
2022-05-25
web安全基础篇-跨站点请求伪造(CSRF)
2022-05-25
文章目录