引言

一直对安全方面挺感兴趣的,以前拜读过《白帽子讲 web 安全》,现在做移动端 Android 测试,在一次关于热埋点的学习中接触到了 Xposed,之后也会关注一点 Android 安全方面的东西,最近学习了一点 Android 动态加载的机制,跟大家分享一下

一点资源

https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://blog.csdn.net/jiangwei0910410003/article/details/48415225
http://blog.csdn.net/itfootball/article/details/49806269
http://blog.csdn.net/androidsecurity/article/details/8664778
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
还有一些工具: https://mp.weixin.qq.com/s?__biz=MjM5NjA0NjgyMA==&mid=2651061026&idx=1&sn=ae6aea9af3b082ea543e1051728bd0bc&scene=1&srcid=0530rqezgS1iLqK0W8tT8909&key=f5c31ae61525f82ea7ae094c2f43f3300c4f648faee145a20953616868278e738c3c425d7098c2cdfab164fbfe02339a&ascene=0&uin=MjQwNzI1NDc1&devicetype=iMac+MacBookPro12%2C1+OSX+OSX+10.10.5+build14F1605)&version=11020201&pass_ticket=ytjLePanA95PuTtMOQpYePApF6e0GJEgj%2BtLjnU6Q4Y%3D(

一点概念

类加载器

上面说到了类加载器,其实动态加载机制的核心就是类加载器的使用,在 java 里它是一种机制----反射(Reflection)
类加载器的基本职责,就是根据一个指定的类的名称,找到或者生成其对应的字节码,然后从这些字节码中定义出一个 java 类,即 java.lang.Class 的一个实例
说到类加载器,或者反射机制,涉及到下面三个类:

应用

在常见的 java 项目(J2SE,J2EE)中,特别是跨项目合作的场景,经常会把 B 方的业务代码编译后的 class 文件打成一个 jar 包,A 在需要时动态加载 jar 包里的类。又或者要实现配置型的项目,会把需要加载的类(通常是一个接口)的名字写在一个配置文件中,自己程序里写关于这个接口的各种实现,运用动态加载机制,用父类(那个接口)引用指向子类(各种实现)对象,利用面向对象中的多态,来最大程度的实现可配置

但是

在 Android 里并不能用常规 java 项目中那种自定义 ClassLoader,重写 ClassLoader 里各种方法的方式来实现动态加载,主要原因有以下两点:

  1. Android 中 ClassLoader 的 defineClass 方法具体是调用 VMClassLoader 的 defineClass 本地静态方法。而这个本地方法除了抛出一个 “UnsupportedOperationException” 之外,什么都没做,甚至连返回值都为空
  2. 在 Android 中,一个类不仅仅是一个类,为啥?比如 Android 里面的组件之一的 Activity,它在 Android 整体框架下,是有各种生命周期,内置了各种回调、监听器的,但是动态加载只能根据类名找到或者生成对应的字节码,然后从字节码中定义一个 java 类,一个普普通通的 java 类,即加载出来它就不再是什么 Activity,Service 了

肿么办

在 Android 里,我们不是自己重写 ClassLoader,而是用下面这两种 ClassLoader 来实现动态加载,他们也都是 ClassLoader 的子类

  1. DexClassLoader(提供了一个释放.dex 文件的路径)
  2. PathClassLoader(默认,提供完整路径后会把'.'替换成'/')

Demo

关于动态加载,google 一下就会有挺多例子的,那我们就先实现一个最基本的例子,了解一下动态加载它到底是怎么一回事儿,它可以用来做什么
原材料准备:

首先,在 Eclipse 里建一个项目,项目结构长这样:

我新建了一个 Android 5 的 lib,里面放的是 Android SDK 里 api level=22 的 android.jar,各个版本大家可以到自己 Android SDK 路径下的 platforms 里面找
项目里有一个 package,com.fenfenzhong.interfaces,这是沿用了普通 java 项目的做法,即先定义接口,到时候可以根据这个接口写各种不同的实现类,充分利用多态
还有另外一个 package,com.fenfenzhong.impl,这个是专门放实现类

com.fenfenzhong.interfaces.IDynamic

package com.fenfenzhong.interfaces;
import android.app.Activity;

public interface IDynamic {

    public void init(Activity activity);

    public void clickMe();

}

com.fenfenzhong.impl.Dynamic

package com.fenfenzhong.impl;
import android.app.Activity;
import android.widget.Toast;
import com.fenfenzhong.interfaces.IDynamic;

//动态类的实现

public class Dynamic implements IDynamic {
    private Activity mActivity;

    @Override
    public void init(Activity activity) {
        mActivity = activity;

    }

    @Override
    public void clickMe() {
        Toast.makeText(mActivity, "hello dynamic", 1500).show(); 
    }


}

将上面的 com.fenfenzhong.impl.Dynamic 这个动态类打包,一般 jar 包里放的是字节码,是编译好的.class 文件,但那是为了不暴露源码,这里我们直接打包源文件,即.java 文件

打好的 jar 包,我命名为 dynamic.jar

然后我们把这个 jar 包放到 Android SDK build-tools 下某个版本里(随意),build-tools 里有很多很厉害的工具,比如 aapt,zipalign,但这次我们要用 dx 这个工具
执行
dx --dex --output=dynamic_for_android.jar dynamic.jar
这条指令的操作就是把 dynamic.jar 先打成一个.dex 文件,以便 Dalvik 虚拟机能认识,再把这个.dex 打成一个 jar 包,即 dynamic_for_android.jar,这个保管好,待会要用

接下来,我们需要一个连接宿主项目和动态实现类的桥梁,当然就是 com.fenfenzhong.interfaces.IDynamic 这个接口啦,再打一个 jar 包

命名为 dynamic_interface.jar

在 Android Studio 新建一个项目,叫 DynamicApp,把 dyncmic_interface.jar 导入到项目的 libs 里面

这个项目相当简单,就一个按钮,但我们想实现的是:点击这个按钮的时候,调用的是动态类中的实现方法

package com.fenfenzhong.dynamicapp;

import android.content.Context;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.fenfenzhong.interfaces.IDynamic;

import java.io.File;

import dalvik.system.DexClassLoader;

public class MainActivity extends AppCompatActivity {

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

        Button clickMeBtn = (Button) findViewById(R.id.click_me);

        String dexPath = Environment.getExternalStorageDirectory().toString() + File.separator + "dynamic_for_android.jar";
        Log.i("dex", dexPath);
        Context context=getApplicationContext();
        File dexOutputDir = context.getDir("dex", 0);
//        String dexOutputDirs = Environment.getExternalStorageDirectory().toString();//如果使用这种outputdir的话,可能报异常

        DexClassLoader dcl = new DexClassLoader(dexPath,dexOutputDir.getAbsolutePath(),null,getClassLoader());
        try {
            Class dynamicClass = dcl.loadClass("com.fenfenzhong.impl.Dynamic");
            dy = (IDynamic)dynamicClass.newInstance();
            if(dy != null) {
                dy.init(MainActivity.this);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        clickMeBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(dy != null) {
                    dy.clickMe();
                }else {
                    Toast.makeText(getApplicationContext(), "cannot load class", Toast.LENGTH_LONG).show();
                }
            }
        });
    }
}

以上便是这个项目所有的代码,在最外层定义一个private IDynamic dy,这个 dy 就是父类(接口)引用
主要从 Sring dexPath 这里说起:

好了,我们把那个 dynamic_for_android.jar 包 push 到 sd 卡的根目录,执行命令:
adb push dynamic_for_android.jar sdcard/

然后,运行一下程序

点击按钮

成功啦!!看到这个还是非常兴奋的!

总结

虽然只是一个特别小的 demo,但里面的一些步骤,一些坑,一些思想,只有一点点踩过来才能体会,Android 自身的框架决定了其动态加载不同于普通 java 项目,在这个 demo 里我们是直接把实现类 push 到 sdcard 的根目录,但如果从网上动态下载呢?反射的机制能让我们动态的定制功能,现在很多应用的换肤,添加模块都是通过这种方式,在安全方面,它还被应用到 apk 的加固,这个之后慢慢写~


↙↙↙阅读原文可查看相关链接,并与作者交流