用户
搜索
  • TA的每日心情
    慵懒
    2018-6-4 11:18
  • 签到天数: 59 天

    连续签到: 1 天

    [LV.5]常住居民I

    i春秋作家

    Rank: 7Rank: 7Rank: 7

    28

    主题

    92

    帖子

    1188

    魔法币
    收听
    0
    粉丝
    13
    注册时间
    2016-7-3

    i春秋签约作者

    发表于 2016-9-29 09:20:12 1116151
    本文原创作者:penguin_wwy,本文属i春秋原创奖励计划,未经许可禁止转载!

    零、问题出现
    对dex文件进行加密,解密后动态加载是一种常用的加壳方式(一代壳以这种方式为主)。但这种在解密之后往往会产生一个解密后的完整dex。过程一般是这样的
    打开文件
    File file = new File("classes.dex");

    读取字节码
    byte[] buffer = new FileInputStream(file).read();

    解密字节码
    decrypt(buffer)

    重写到文件
    File newFile = new File("classes_decrypt.dex");
    new FileOutputStream(newFile).write(buffer);

    加载dex
    DexClassLoader dexClassLoader = new DexClassLoader("classes_decrypt.dex"...);

    可见在重写到文件这一步,就有可能被截获到解密后的dex,那加密dex的意义就完全不存在了。
    当然也有过许多办法,比如加载完后删除文件、或者隐藏文件等等,但都没法从根本上解决问题。而最有实际意义的方法就是今天要说的,不落地加载dex。

    一、理论基础
    不落地的含义就是说在解密后直接由字节码进行加载,不需要变成dex文件。Dalvik中的两种类加载器DexClassLoader和PathClassLoader显然都不具备这个能力。我们需要自己定义一个类加载器。
    那如何自己定义呢?我们先分析一下DexClassLoader加载的过程(详细分析请看我的博客)。这里简单说明一下,首先是DexClassLoader的构造函数
    源码位置libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
    [Java] 纯文本查看 复制代码
    public class DexClassLoader extends BaseDexClassLoader {
    /**
    * Creates a {[url=home.php?mod=space&uid=74926]@Code[/url] DexClassLoader} that finds interpreted and native
    * code. Interpreted classes are found in a set of DEX files contained
    * in Jar or APK files.
    *
    * <p>The path lists are separated using the character specified by the
    * {@code path.separator} system property, which defaults to {@code :}.
    *
    * @param dexPath the list of jar/apk files containing classes and
    * resources, delimited by {@code File.pathSeparator}, which
    * defaults to {@code ":"} on Android
    * @param optimizedDirectory directory where optimized dex files
    * should be written; must not be {@code null}
    * @param libraryPath the list of directories containing native
    * libraries, delimited by {@code File.pathSeparator}; may be
    * {@code null}
    * @param parent the parent class loader
    */
    public DexClassLoader(String dexPath, String optimizedDirectory,
    String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
    }
    实质上是对它的父类,BaseDexClassLoader的构造
    源码位置libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
    [Java] 纯文本查看 复制代码
     public BaseDexClassLoader(String dexPath, File optimizedDirectory,
    String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    
    libcore\dalvik\src\main\java\dalvik\system\DexPathList.java
    [Java] 纯文本查看 复制代码
     public DexPathList(ClassLoader definingContext, String dexPath,
    String libraryPath, File optimizedDirectory) {
    if (definingContext == null) {
    throw new NullPointerException("definingContext == null");
    }
    
    if (dexPath == null) {
    throw new NullPointerException("dexPath == null");
    }
    
    if (optimizedDirectory != null) {
    if (!optimizedDirectory.exists()) {
    throw new IllegalArgumentException(
    "optimizedDirectory doesn't exist: "
    + optimizedDirectory);
    }
    
    if (!(optimizedDirectory.canRead()
    && optimizedDirectory.canWrite())) {
    throw new IllegalArgumentException(
    "optimizedDirectory not readable/writable: "
    + optimizedDirectory);
    }
    }
    
    this.definingContext = definingContext;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory);
    this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }
    重点在函数makeDexElements
    [Java] 纯文本查看 复制代码
     private static Element[] makeDexElements(ArrayList<File> files,
    File optimizedDirectory) {
    ArrayList<Element> elements = new ArrayList<Element>();
    
    /*
    * Open all files and load the (direct or contained) dex files
    * up front.
    */
    for (File file : files) {
    File zip = null;
    DexFile dex = null;
    String name = file.getName();
    
    if (name.endsWith(DEX_SUFFIX)) {
    // Raw dex file (not inside a zip/jar).
    try {
    dex = loadDexFile(file, optimizedDirectory);
    } catch (IOException ex) {
    System.logE("Unable to load dex file: " + file, ex);
    }
    } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
    || name.endsWith(ZIP_SUFFIX)) {
    zip = file;
    
    try {
    dex = loadDexFile(file, optimizedDirectory);
    } catch (IOException ignored) {
    /*
    * IOException might get thrown "legitimately" by
    * the DexFile constructor if the zip file turns
    * out to be resource-only (that is, no
    * classes.dex file in it). Safe to just ignore
    * the exception here, and let dex == null.
    */
    }
    } else if (file.isDirectory()) {
    // We support directories for looking up resources.
    // This is only useful for running libcore tests.
    elements.add(new Element(file, true, null, null));
    } else {
    System.logW("Unknown file type for: " + file);
    }
    
    if ((zip != null) || (dex != null)) {
    elements.add(new Element(file, false, zip, dex));
    }
    }
    
    return elements.toArray(new Element[elements.size()]);
    }
    根据文件后缀名的判断选择分支,然后调用loadDex函数
    [Java] 纯文本查看 复制代码
     private static DexFile loadDexFile(File file, File optimizedDirectory)
    throws IOException {
    if (optimizedDirectory == null) {
    return new DexFile(file);
    } else {
    String optimizedPath = optimizedPathFor(file, optimizedDirectory);
    return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
    }
    DexFile.loadDex这个函数的内部也只是构造一个DexFile对象,所以直接看DexFile的构造函数就好
    [Java] 纯文本查看 复制代码
     private DexFile(String sourceName, String outputName, int flags) throws IOException {
    if (outputName != null) {
    try {
    String parent = new File(outputName).getParent();
    if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
    throw new IllegalArgumentException("Optimized data directory " + parent
    + " is not owned by the current user. Shared storage cannot protect"
    + " your application from code injection attacks.");
    }
    } catch (ErrnoException ignored) {
    // assume we'll fail with a more contextual error later
    }
    }
    
    mCookie = openDexFile(sourceName, outputName, flags);
    mFileName = sourceName;
    guard.open("close");
    //System.out.println("DEX FILE cookie is " + mCookie);
    }
    重点的重点在openDexFile,这个函数负责最终的dex文件加载

    运行流程
    DexClassLoader ——> BaseDexClassLoader ——> DexPathList ——> makeDexElements ——> loadDex ——> DexFile

    这个openDexFile函数是一个native函数,在libdvm.so中,看对应的函数表
    [Java] 纯文本查看 复制代码
    const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { 
    { "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", 
    Dalvik_dalvik_system_DexFile_openDexFile }, 
    { "openDexFile", "([B)I", 
    Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, 
    { "closeDexFile", "(I)V", 
    Dalvik_dalvik_system_DexFile_closeDexFile }, 
    { "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", 
    Dalvik_dalvik_system_DexFile_defineClass }, 
    { "getClassNameList", "(I)[Ljava/lang/String;", 
    Dalvik_dalvik_system_DexFile_getClassNameList }, 
    { "isDexOptNeeded", "(Ljava/lang/String;)Z", 
    Dalvik_dalvik_system_DexFile_isDexOptNeeded }, 
    { NULL, NULL, NULL }, 
    }; 
    调用表中第一个openDexFile所对应的Dalvik_dalvik_system_DexFile_openDexFile ,这个就是实际执行的函数,函数参数
    "(Ljava/lang/String;Ljava/lang/String;I)I"
    两个字符串一个整型。
    而意外的发现在它的下一个位置Dalvik_dalvik_system_DexFile_openDexFile_bytearray,它的参数
    ([B)I
    一个byte数组和一个整型,也就是说如果我们直接调用这个函数的话,就可以将字节码以一个byte数组的形式传入。了解到这里,我们的目标就清晰了。
    (1)构造一个我们自己的类加载器
    (2)通过Dalvik_dalvik_system_DexFile_openDexFile_bytearray,来加载dex文件的字节码

    二、开工实践
    下面我们就来尝试实现一下,首先我们需要一个正常的Apk,越简单越好,最好不需要太多资源文件,加载了dex能直接运行,毕竟只是实验一下。上一篇当中的TestApk就很合适。解压出它的classes.dex,放到手机/data/local/tmp文件夹下
    然后新建一个Apk,就叫DexFile
    准备一个java类,负责native函数
    [Java] 纯文本查看 复制代码
    public class JNITool {
    static {
    System.loadLibrary("JNITool");
    }
    
    public static native int loadDex(byte[] dex,long dexlen);
    }
    
    这个loadDex就负责通过我们前面所述的函数加载dex。在JNITool.so,我们要加载libdvm.so并且找到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函数
    所以需要定义JNI_OnLoad函数
    [C] 纯文本查看 复制代码
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    
    void *ldvm = (void*) dlopen("libdvm.so", RTLD_LAZY);
    dvm_dalvik_system_DexFile = (JNINativeMethod*) dlsym(ldvm, "dvm_dalvik_system_DexFile");
    
    //openDexFile
    if(0 == lookup(dvm_dalvik_system_DexFile, "openDexFile", "([B)I",&openDexFile)) {
    openDexFile = NULL;
    LOGI("openDexFile method does not found ");
    }else{
    LOGI("openDexFile method found ! HAVE_BIG_ENDIAN");
    }
    
    LOGI("ENDIANNESS is %c" ,ENDIANNESS );
    void *venv;
    LOGI("dufresne----->JNI_OnLoad!");
    if ((*vm)->GetEnv(vm, (void**) &venv, JNI_VERSION_1_4) != JNI_OK) {
    LOGI("dufresne--->ERROR: GetEnv failed");
    return -1;
    }
    return JNI_VERSION_1_4;
    }
    dlopen函数链接libdvm.so,dlsym找到并返回dvm_dalvik_system_DexFile。dvm_dalvik_system_DexFile就是我们之前看到的函数表
    [Java] 纯文本查看 复制代码
    const DalvikNativeMethod dvm_dalvik_system_DexFile[] = { 
    { "openDexFile", "(Ljava/lang/String;Ljava/lang/String;I)I", 
    Dalvik_dalvik_system_DexFile_openDexFile }, 
    { "openDexFile", "([B)I", 
    Dalvik_dalvik_system_DexFile_openDexFile_bytearray }, 
    { "closeDexFile", "(I)V", 
    Dalvik_dalvik_system_DexFile_closeDexFile }, 
    { "defineClass", "(Ljava/lang/String;Ljava/lang/ClassLoader;I)Ljava/lang/Class;", 
    Dalvik_dalvik_system_DexFile_defineClass }, 
    { "getClassNameList", "(I)[Ljava/lang/String;", 
    Dalvik_dalvik_system_DexFile_getClassNameList }, 
    { "isDexOptNeeded", "(Ljava/lang/String;)Z", 
    Dalvik_dalvik_system_DexFile_isDexOptNeeded }, 
    { NULL, NULL, NULL }, 
    }; 
    lookup从函数表中寻找我们要的Dalvik_dalvik_system_DexFile_openDexFile_bytearray
    [C] 纯文本查看 复制代码
    int lookup(JNINativeMethod *table, const char *name, const char *sig,
    void (**fnPtrout)(u4 const *, union JValue *)) {
    int i = 0;
    while (table.name != NULL)
    {
    LOGI("lookup %d %s" ,i,table.name);
    if ((strcmp(name, table.name) == 0)
    && (strcmp(sig, table.signature) == 0))
    {
    *fnPtrout = table.fnPtr;
    return 1;
    }
    i++;
    }
    return 0;
    }
    找到之后就用全局的函数指针
    [C] 纯文本查看 复制代码
    void (*openDexFile)(const u4* args, union JValue* pResult);
    来保存这个函数
    [C] 纯文本查看 复制代码
    JNIEXPORT jint JNICALL Java_cn_wjdiankong_dexfiledynamicload_NativeTool_loadDex(JNIEnv* env, jclass jv, jbyteArray dexArray, jlong dexLen)
    {
    // header+dex content
    u1 * olddata = (u1*)(*env)-> GetByteArrayElements(env,dexArray,NULL);
    char* arr;
    arr = (char*)malloc(16 + dexLen);
    ArrayObject *ao=(ArrayObject*)arr;
    ao->length = dexLen;
    memcpy(arr+16,olddata,dexLen);
    u4 args[] = { (u4) ao };
    union JValue pResult;
    jint result;
    if(openDexFile != NULL) {
    openDexFile(args,&pResult);
    }else{
    result = -1;
    }
    
    result = (jint) pResult.l;
    LOGI("Java_cn_wjdiankong_dexfiledynamicload_NativeTool_loadDex %d" , result);
    return result;
    }
    loadDex函数最终会通过这个函数指针来调用dvm_dalvik_system_DexFile,最终加载dex

    那么回到Java层,我们需要定义一个自己的类加载器
    [Java] 纯文本查看 复制代码
    public class DynamicDexClassLoder extends DexClassLoader {
    
    private static final String TAG = "dexlog";
    private int cookie;
    private Context mContext;
    构造函数
    [Java] 纯文本查看 复制代码
     public DynamicDexClassLoder(Context context, byte[] dexBytes,
    String libraryPath, ClassLoader parent, String oriPath,
    String fakePath) {
    super(oriPath, fakePath, libraryPath, parent);
    setContext(context);
    
    int cookie = JNITool.loadDex(dexBytes, dexBytes.length);
    
    setCookie(cookie);
    
    }
    cookie这个变量代表了加载完成后的dex的句柄
    然后实现findClass函数
    [Java] 纯文本查看 复制代码
     @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    Log.d(TAG, "findClass-" + name);
    Class<?> cls = null;
    String as[] = getClassNameList(cookie);
    Class obj_class = Class.forName(DexFile.class.getName());
    Method method = obj_class.getDeclaredMethod("defineClassNative",
    new Class[]{String.class, ClassLoader.class, int.class});
    method.setAccessible(true);
    for (int z = 0; z < as.length; z++) {
    Log.i(TAG, "classname:"+as[z]);
    if (as[z].equals(name)) {
    cls = (Class) method.invoke(null, 
    new Object[]{as[z].replace('.', '/'), mContext.getClassLoader(), cookie});
    } else {
    //加载其他类
    method.invoke(null, 
    new Object[]{as[z].replace('.', '/'), mContext.getClassLoader(), cookie});
    }
    }
    
    if (null == cls) {
    cls = super.findClass(name);
    }
    
    return cls;
    }
    然后在MainActivity中我们就可以通过以下代码,启动TestApk的MainActivity
    [Java] 纯文本查看 复制代码
     DynamicDexClassLoder dLoader = new DynamicDexClassLoder(
    getApplicationContext(),
    dexContent,
    null,
    clzLoader,
    getPackageResourcePath(),getDir(".dex", MODE_PRIVATE).getAbsolutePath()
    );
    Class clazz = dLoader.findClass("com.example.testapk.MainActivity");
    Intent intent = new Intent(this, clazz);
    startActivity(intent);
    三、小结
    以上的代码在Android5.0以下的Android系统上可以正确执行(少数真机可能会出问题),我测试的时候在原生的Android4.4上成功。至于Android5.0?不好意思,从Android5.0开始,谷歌已经放弃了Dalvik虚拟机,转而支持ART,没有了libdvm,所以。。。。之后我会考虑研究一下怎么在ART虚拟机中实现。
    这种不落地的加载方式是现在加壳方式的一部分。现在的加壳方法往往是多种方法捏合在一起的(还有那种丧心病狂的VMP),大家可以试试将上篇的方法和这篇结合起来,对一个加密的dex,解密后不落地加载,之后再修复dex中的错误指令。之后我也会介绍越来越多的加壳、抗反编译方法,都可以尝试结合在一起。

    评分

    参与人数 1价值分 +12 收起 理由
    zusheng + 12 价值分奖励

    查看全部评分

    本帖被以下淘专辑推荐:

    我改行了。。。
    http://penguin-wenyang.wang
    发表于 2016-10-10 15:16:26
    文章奖励介绍及评分标准:http://bbs.ichunqiu.com/thread-7869-1-1.html,如有疑问请加QQ:286894635!

    作者帖子标题内容要求内容稀缺性文章篇幅文章深度文章可读性是否系列文章排版优化总分奖金点评
    penguin_wwy对抗静态分析——dex不落地加载22231111270RMB文章比较详细,不过缺乏可读性

    Hacking the earth.My Blog:https://isbase.cc
    使用道具 举报 回复
    发表于 2016-9-29 14:06:56
    占个沙发再看文章
    使用道具 举报 回复
    发表于 2016-9-30 13:21:53
    请问一下,之前外部动态加载DEX安全风险浅析这篇文章中,说是在Android 4.1之后会对要加载的文件uid与应用线程uid做一个比对,是不是放在私有目录下才能保证这个uid一致?你这个放在data/local/tmp中可以的吗?还是你这种方式可以,一般动态加载不允许这样?望大神解答一下
    使用道具 举报 回复
    发表于 2016-9-30 16:40:40
    zero_bt 发表于 2016-9-30 05:21
    请问一下,之前外部动态加载DEX安全风险浅析这篇文章中,说是在Android 4.1之后会对要加载的文件uid与应用 ...

    我在模拟器中测试的,用的是root权限。如果要在真机中测试的话,要看厂商改没改ROM,可能可以可能不可以,需要测试
    我改行了。。。
    http://penguin-wenyang.wang
    使用道具 举报 回复
    发表于 2016-10-9 19:50:34
    现在还是看不懂安卓的
    使用道具 举报 回复
    发表于 2016-10-10 16:08:08
    太谢谢了!!
    使用道具 举报 回复
    发表于 2016-10-20 20:45:29
    使用道具 举报 回复
    发表于 2016-11-13 22:32:57
    使用道具 举报 回复
    发表于 2016-12-30 22:14:37
    请问你之前的文章在哪里?为啥我找不见呢?
    使用道具 举报 回复
    发表于 2018-9-29 12:27:21
    支持一下~
    使用道具 举报 回复
    发表于 2020-4-14 20:55:50
    为啥这么强
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册