移动安全测试 Android 动态加载初探

fenfenzhong · 2016年05月30日 · 最后由 fenfenzhong 回复于 2016年06月01日 · 2244 次阅读
本帖已被设为精华帖!

引言

一直对安全方面挺感兴趣的,以前拜读过《白帽子讲 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 程序是跑在虚拟机中的,原生 java 虚拟机叫 JVM(当然也分很多厂家,很多种类),Android 也是 java 程序,但 Android 的虚拟机,叫 Dalvik,Android 5.0 之后,舍弃了 Dalvik,用了 ART(Android Runtime)
  • 一般来说,虚拟机使用 java 类的方法如下:java 源程序 (.java 文件) 在经过 java 编译器编译之后,就被转换成 java 字节码 (JVM 对应.class 文件,Dalvik 对应.dex 文件),类加载器负责读取 java 字节码,并转换成 java.lang.Class 的一个实例,每个这样的实例用来表示一个 java 类
  • 动态加载:在运行时动态加载或者重载类

类加载器

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

  • java.lang.Class
  • java.lang.ClassLoader
  • java.lang.reflect

应用

在常见的 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 Studio
  • Android SDK

首先,在 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 这里说起:

  • 我们先定义 dexPath 为外部存储根目录下的 dynamic_for_android.jar,即指定到时候从这里去拿 dex 文件
  • dexOutputDir 这个变量主要是指定释放 dex 文件后的路径,因为我们使用 DexClassLoader,上面我说到,这种 ClassLoader 需要这么一个路径
  • DexClassLoader dcl 这里,就初始化了一个实例,叫 dcl,根据上面对 ClassLoader 的描述,我们可以指定一个类名,通过这个实例去找,或者生成该类的字节码
  • 利用反射机制的newInstance()方法初始化一个对象,并用父类引用 dy 指向该对象,到此多态的实现条件已满足
  • 这个 Activity 里有一个按钮,但它的 onClick 方法,调用的是 dy 的 clickMe()

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

然后,运行一下程序

点击按钮

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

总结

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 7 条回复 时间 点赞

怪不得这么少. 不尽兴

最近也开始在用 Android 的动态加载技术,好文,关注

期待你回来写完

#1 楼 @seveniruby 哈哈,去发版了,sorry sorry

#3 楼 @cesc 来了来了 😄

之前有一点事,所以导致没有一次性写完,各位坛友见谅

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册