从Android源码看APK加壳(一)

1.文章难易度【★★★】
2.文章作者:penguin_wwy
3.本文参与i春秋社区原创文章奖励计划,未经许可禁止转载
4.阅读基础:自备Android源码

【预备~~~起】
曾经有个女(xue)的(jie)问我,为什么APP的壳代码一般都放在Application类的attachBaseContext函数中,如图
112448uupmemyfvv6skew2.png
能不能放在其他函数里。当时我说可以,当时没办法达到同样的效果,因为应用启动时,ActivityThread类负责整个环境,它有一个Application类型的app变量,指向当前的APK的Application,在ActivityThread启动onCreate函数之前会调用app.attachBaseContext,所以Application的attachBaseContext会在整个APP启动前调用。。。。(我编不下去了)
PS:本人看的Android源码版本为4.3-r2和5.0-r1,文章中主要介绍跟Dalvik虚拟机相关的,所以使用4.3-r2,如果遇到Art虚拟机部分再看5.0-r1


【一二三四】
脱壳的入门教程都会教大家在dvmDexFileOpenPartial函数下断,该函数第一个参数是dex文件在内存中的位置,然后从dex头部获取dex文件大小,dump出整个dex文件,那么这个dvmDexFileOpenPartial函数是怎么来的呢,如何调用到它的,我们从Android源码一步一步分析。


【二二三四】
首先是加载过程,我们先省略掉之前的内容,从dex文件的加载开始。Dalvik中的ClassLoader有DexClassLoader和PathClassLoader两种

public class DexClassLoader extends BaseDexClassLoader { 
    
    public DexClassLoader(String dexPath, String optimizedDirectory, 
            String libraryPath, ClassLoader parent) { 
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);  
    } 
}
public class PathClassLoader extends BaseDexClassLoader {  
    
    public PathClassLoader(String dexPath, ClassLoader parent) {  
        super(dexPath, null, null, parent);  
    }  
    
    public PathClassLoader(String dexPath, String libraryPath,  
            ClassLoader parent) {  
        super(dexPath, null, libraryPath, parent);  
    }  
}

区别在于optimizedDirectory这个参数,DexClassLoader通过它new出一个File对象。这里直接告诉大家,这个参数的作用是用于dex的优化。前面我也提到过,dex文件经过优化后变为odex文件,执行效率更高。PathClassLoader载入的是已经经过优化的odex,相应的DexClassLoader是没有经过优化的,这里的new File(optimizedDirectory)就是通过optimizedDirectory对dex进行优化。
继续看BaseDexClassLoader

public BaseDexClassLoader(String dexPath, File optimizedDirectory,  
        String libraryPath, ClassLoader parent) {  
    super(parent);  
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);  
}

下面一步是DexPathList,我们关注的重点在optimizedDirectory

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);

如果optimizedDirectory存在(PathClassLoader的情况),那么验证文件是否存在,是否可读写,然后传入makeDexElements函数。这个函数稍微有点长,看重点

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);  
}

如果文件是dex、apk、jar或者zip那么就会调用loadDexFile。而这个loadDexFile其实是对DexFile构造函数的封装,所以直接看DexFile的构造函数

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,从这里进入到native层,之后的过程不(又)是(臭)重(又)点(长),我们简单看下。

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 },  
};

Dalvik_dalvik_system_DexFile_openDexFile是Java层对应的函数,该函数会调用dvmRawDexFileOpen

if (newFile) {  
    u8 startWhen, copyWhen, endWhen;  
    bool result;  
    off_t dexOffset;  
   
    dexOffset = lseek(optFd, 0, SEEK_CUR);  
    result = (dexOffset > 0);  
   
    if (result) {  
        startWhen = dvmGetRelativeTimeUsec();  
        result = copyFileToFile(optFd, dexFd, fileSize) == 0;  
        copyWhen = dvmGetRelativeTimeUsec();  
    }  
   
    if (result) {  
        result = dvmOptimizeDexFile(optFd, dexOffset, fileSize,  
            fileName, modTime, adler32, isBootstrap);  
    }  
   
    if (!result) {  
        ALOGE("Unable to extract+optimize DEX from '%s'", fileName);  
        goto bail;  
    }  
   
    endWhen = dvmGetRelativeTimeUsec();  
    ALOGD("DEX prep '%s': copy in %dms, rewrite %dms",  
        fileName,  
        (int) (copyWhen - startWhen) / 1000,  
        (int) (endWhen - copyWhen) / 1000);  
}

这个函数调用dvmOptimizeDexFile对dex文件进行优化,优化过程:产生一个空文件——>构造文件头——>拷贝——>优化。优化会执行/bin/dexopt,dexopt的main函数

int main(int argc, char* const argv[])  
{  
    set_process_name("dexopt");  
   
    setvbuf(stdout, NULL, _IONBF, 0);  
   
    if (argc > 1) {  
        if (strcmp(argv[1], "--zip") == 0)  
            return fromZip(argc, argv);  
        else if (strcmp(argv[1], "--dex") == 0)  
            return fromDex(argc, argv);  
        else if (strcmp(argv[1], "--preopt") == 0)  
            return preopt(argc, argv);  
    }  
   
    fprintf(stderr,  
        "Usage:\n\n" 
        "Short version: Don't use this.\n\n" 
        "Slightly longer version: This system-internal tool is used to\n" 
        "produce optimized dex files. See the source code for details.\n");  
   
    return 1;  
}

根据文件类型选择fromDex函数

static int fromDex(int argc, char* const argv[])
{
。。。
 
。。。
 
    if (dvmPrepForDexOpt(bootClassPath, dexOptMode, verifyMode, flags) != 0) {
        ALOGE("VM init failed");
        goto bail;
    }
 
    vmStarted = true;
 
    /* do the optimization */
    if (!dvmContinueOptimization(fd, offset, length, debugFileName,
            modWhen, crc, (flags & DEXOPT_IS_BOOTSTRAP) != 0))
    {
        ALOGE("Optimization failed");
        goto bail;
    }
 
    result = 0;
 
bail:
    /*
     * In theory we should gracefully shut the VM down at this point.  In
     * practice that only matters if we're checking for memory leaks with
     * valgrind -- simply exiting is much faster.
     *
     * As it turns out, the DEX optimizer plays a little fast and loose
     * with class loading.  We load all of the classes from a partially-
     * formed DEX file, which is unmapped when we're done.  If we want to
     * do clean shutdown here, perhaps for testing with valgrind, we need
     * to skip the munmap call there.
     */
#if 0
    if (vmStarted) {
        ALOGI("DexOpt shutting down, result=%d", result);
        dvmShutdown();
    }
#endif
 
    free(bootClassPath);
    ALOGV("DexOpt command complete (result=%d)", result);
    return result;
}

函数之前执行一些切割参数的工作被我省略了,重点在最后的dvmContinueOptimization函数。由于这个函数很长,截取我们需要的一部分

if (success) {
    DvmDex* pDvmDex = NULL;
    u1* dexAddr = ((u1*) mapAddr) + dexOffset;
 
    if (dvmDexFileOpenPartial(dexAddr, dexLength, &pDvmDex) != 0) {
        ALOGE("Unable to create DexFile");
        success = false;
    } else {
        /*
         * If configured to do so, generate register map output
         * for all verified classes.  The register maps were
         * generated during verification, and will now be serialized.
         */
        if (gDvm.generateRegisterMaps) {
            pRegMapBuilder = dvmGenerateRegisterMaps(pDvmDex);
            if (pRegMapBuilder == NULL) {
                ALOGE("Failed generating register maps");
                success = false;
            }
        }
 
        DexHeader* pHeader = (DexHeader*)pDvmDex->pHeader;
        updateChecksum(dexAddr, dexLength, pHeader);
 
        dvmDexFileFree(pDvmDex);
    }
}

可以看到我们找了半天的dvmDexFileOpenPartial函数就在这里。简单说下dvmContinueOptimization的内容,大概分为几部分:声明内部变量,检查文件长度将文件mmap到mapAddr,从gDvm中获取信息设置选项。dvmDexFileOpenPartial所在的这部分内容是文件重写,比特位、结构重排,类型核实,字节码优化等等。


【二二三四】
到这里我们就捋顺了dvmDexFileOpenPartial这个函数的意义了。总结下,通过DexClassLoader加载dex文件 ——> 执行dexopt对dex进行优化 ——> dvmContinueOptimization执行优化的内容 ——> 最终到达dvmDexFileOpenPartial。

更多安全技术、精品好文、白帽黑客大佬尽在:http://bbs.ichunqiu.com/portal.php

转载于:https://my.oschina.net/ichunqiu/blog/812656

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值