dumpDex 脱壳原理

使用文章:http://martinhan.site/2018-12-13/Android%E9%80%86%E5%90%91%E4%B9%8B%E8%B7%AF---%E8%84%B1%E5%A3%B3360%E5%8A%A0%E5%9B%BA%E3%80%81%E4%B8%8Exposed%20hook%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9.html

 

dumpDex , 一个开源的 Android 脱壳插件工具,需要在 Xposed 环境中使用,支持市面上大多数加密壳,这里我们将分析 dumpDex 的脱壳原理……

准备事项

  • Android Studio v3.1

原理分析

这里假设我们已经了解 Xposed 相关的知识,如果还未了解的,可以先查看这篇文章

源码导入

打开 Android Studio ,新建项目,以 git 方式将 dumpDex 项目源码 clone 到本地。
导入后的项目结构如下图所示。

源码分析

首先要找到入口类,因为该项目是一个 Xposed 插件,所以先看 assets 文件夹里的 xposed_init 文件。

com.wrbug.dumpdex.XposedInit

这里的 XposedInit 就是入口类了。

点击进入查看 XposedInit 源码。

public class XposedInit implements IXposedHookLoadPackage {

    ......

    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
        PackerInfo.Type type = PackerInfo.find(lpparam);
        if (type == null) {
            return;
        }
        
        final String packageName = lpparam.packageName;
        if (lpparam.packageName.equals(packageName)) {
            String path = "/data/data/" + packageName + "/dump";
            File parent = new File(path);
            if (!parent.exists() || !parent.isDirectory()) {
                parent.mkdirs();
            }
            log("sdk version:" + Build.VERSION.SDK_INT);
            if (Build.VERSION.SDK_INT >= 26) {
                OreoDump.init(lpparam);
            } else {
                LowSdkDump.init(lpparam,type);
            }
        }
    }
}
public class PackerInfo {
    private static List<String> sPackageName = new ArrayList<>(); // 存放支持的加密壳的包名
    private static Map<String, Type> sTypeMap = new HashMap<>(); // 存放加密壳的包名及其名称
    
    ......

    public static Type find(final XC_LoadPackage.LoadPackageParam lpparam) {
        for (String s : sPackageName) {
            Class clazz = XposedHelpers.findClassIfExists(s, lpparam.classLoader);
            if (clazz != null) {
                Type type = getType(s);
                return type;
            }
        }
        return null;
    }

    private static Type getType(String packageName) {
        return sTypeMap.get(packageName);
    }

    public enum Type {
        QI_HOO("360加固"), AI_JIA_MI("爱加密"), BANG_BANG("梆梆加固"), TENCENT("腾讯加固"), BAI_DU("百度加固");

        ......
    }

}

首先检测当前运行的 APP 是否存在 dumpDex 支持的加密壳,如腾讯乐固、360加固等,如果没找到则不进行任何处理,找到了则进行脱壳,并将脱壳后的 dex 文件存放到 APP 所在路径的 dump 子文件夹里。

脱壳过程视 Android 手机的版本而定,分为两种处理方式:

  1. Android 8.0 ( SDK 26 )及以上的手机,调用
    OreoDump.init() 方法进行处理,这部分是通过 NDK 实现的。
  2. 其它低版本的手机,调用 LowSdkDump.init() 方法进行处理,这部分是通过 Hook 实现的。

这里我们只对第2种方式进行源码分析,至于第1种方式,后面有时间再追加了,有兴趣的小伙伴可以自行研究。

public class LowSdkDump {

    ......

    public static void init(final XC_LoadPackage.LoadPackageParam lpparam, PackerInfo.Type type) {
        if (type == PackerInfo.Type.BAI_DU) {
            XposedHelpers.findAndHookMethod("com.baidu.protect.CrashHandler", lpparam.classLoader, "uncaughtException", Thread.class, Throwable.class, new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    param.setResult(null);
                }
            });
        }
        
        XposedHelpers.findAndHookMethod("android.app.Instrumentation", lpparam.classLoader, "newApplication", ClassLoader.class, String.class, Context.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                dump(lpparam.packageName, param.getResult().getClass());
                attachBaseContextHook(lpparam, ((Application) param.getResult()));
            }
        });
    }
    
    ......
    
}

init() 先检测是否为百度加固,如果是,拦截其 uncaughtException() 方法。这里猜测百度在该方法里做了一些特殊处理导致无法正常脱壳,所以才进行拦截,有知道详情的小伙伴也可留言说明一下。

然后 Hook 了 android.app.Instrumentation 类里的 newApplication() 方法,也就是说,当该方法被调用时,插件会在该方法被调用之后调用 dump() 和 attachBaseContextHook() 这两个方法。

Instrumentation 的 newApplication() 方法,在 Activity 启动时会被调用,关于 Activity 启动过程,查看这篇文章

这里为啥要 Hook Instrumentation 类的 newApplication() 方法呢?这就涉及到 Android APK 加固原理了,一般进行加固的壳软件,都有自己的一个 Application 类,然后把源 APK 里的 Application 类给隐藏掉,所以该壳软件在运行加固后的 APK 时,要先进行脱壳,加载源 APK 里的 Application ,而加载 Application 一定会调用 newApplication() 方法,所以 Hook 这个方法是一个不错的选择。

关于 APK 的加固原理,后面将新开文章示例说明。

TODO: APK 加固原理

接下来先看看 dump() 方法及其相关类:

private static void dump(String packageName, Class<?> aClass) {
    Object dexCache = XposedHelpers.getObjectField(aClass, "dexCache");
    Object o = XposedHelpers.callMethod(dexCache, "getDex");
    byte[] bytes = (byte[]) XposedHelpers.callMethod(o, "getBytes");
    String path = "/data/data/" + packageName + "/dump";
    File file = new File(path, "source-" + bytes.length + ".dex");
    if (file.exists()) {
        return;
    }
    FileUtils.writeByteToFile(bytes, file.getAbsolutePath());
}
public final class Class<T> implements Serializable, AnnotatedElement, GenericDeclaration, Type {

    ......
    
    /**
     * DexCache of resolved constant pool entries. Will be null for certain runtime-generated classes
     * e.g. arrays and primitive classes.
     */
    private transient DexCache dexCache;
    
}

 

final class DexCache {
    
    ......
    
    Dex getDex() {
        Dex result = dex;
        if (result == null) {
            synchronized (this) {
                result = dex;
                if (result == null) {
                    dex = result = getDexNative();
                }
            }
        }
        return result;
    }
}
public final class Dex {
    private ByteBuffer data;

    ......

    public byte[] getBytes() {
        ByteBuffer data = this.data.duplicate(); // positioned ByteBuffers aren't thread safe
        byte[] result = new byte[data.capacity()];
        data.position(0);
        data.get(result);
        return result;
    }
}

dump() 首先通过反射获得 Class 类里的 dexCache 变量,该变量是一个 DexCache 类,然后通过 DexCache 类里的 getDex() 方法获得一个 Dex 类的实例,接着调用 Dex.getBytes() 方法得到字节数组,最后将这些数据导出到一个 dex 文件里,该文件位于 APP 所在路径里的 dump 子文件夹里。

其实就是通过反射获取类所在的 dex 的数据,导出到特定的 dex 文件里。

最后看看 attachBaseContextHook() 方法:

private static void attachBaseContextHook(final XC_LoadPackage.LoadPackageParam lpparam, final Application application) {
    ClassLoader classLoader = application.getClassLoader();
    XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, boolean.class, new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            log("loadClass->" + param.args[0]);
            Class result = (Class) param.getResult();
            if (result != null) {
                dump(lpparam.packageName, result);
            }
        }
    });
    XposedHelpers.findAndHookMethod("java.lang.ClassLoader", classLoader, "loadClass", String.class, boolean.class, new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            log("loadClassWithclassLoader->" + param.args[0]);
            Class result = (Class) param.getResult();
            if (result != null) {
                dump(lpparam.packageName, result);
            }
        }
    });
}

attachBaseContextHook() 主要就是 Hook 了 ClassLoader 类的 loadClass() 方法,然后也是调用 dump() 方法将 dex 数据导出到特定的 dex 文件里。

这里为啥又要 Hook ClassLoader 类的 loadClass() 方法呢?因为壳软件在脱壳时必须重新加载源 APK 的入口 Activity ,而这种加载方式,要通过这个 loadClass() 方法来实现的。

关于 Activity / Class 的动态加载,后面将新开文章讲讲。

TODO: Android 动态加载

【优化建议】

其实在调用 dump() 方法之前,可以通过以下代码比较包名,看当前调用的类是不是在特定的包里,仅当加载的 Application 或 Activity 为当前运行的 APP 类时才进行 dex 的导出。

if (!param.args[0].toString().startsWith(lpparam.packageName)) {
    return;
}

这样可以减少生成的 dex 文件数。

总结与声明

总结

整个脱壳流程如下:

  1. 根据类名查找当前运行的 APP 是否存在支持的加密壳;
  2. 存在支持的加密壳的情况下,根据手机的 Android 版本进行不同的处理,Android 8.0 及以上手机走 NDK 方式,其它低版本手机则走 Hook 方式;
  3. Hook 方式,主要是通过 Hook Instrumentation 类的 newApplication() 方法和 ClassLoader 类的 loadClass() 方法,获取 Application 或 Activity 所在的 dex 的数据;
  4. 将这些数据导出到 APP 所在路径的 dump 子文件夹里的 dex 文件。

 

分析Native层文章:

https://blog.csdn.net/hanchaohao2012/article/details/85086477

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值