您好,欢迎来到源码搜藏!分享精神,快乐你我!提示:担心找不到本站?在百度搜索“源码搜藏”,网址永远不丢失!
  • 首 页
  • 在线工具
  • 当前位置:首页 > 安卓源码 > 技术博客 >

    Android逆向分析APK的打包与安装

    时间:2016-12-02 00:03 来源:互联网 作者:源码搜藏 浏览:收藏 挑错 推荐 打印

    前言 上一次我们反编译了手Q,并遇到了Apktool反编译直接crash的问题,虽然笔者很想在这次解决这个问题,但在解决途中,发现该保护依赖于很多知识,所以本次先插入一下,正所谓知其然知其所以然,授之鱼不如授之以渔,只有知道一些基本原理,才能让我们以后

    前言

    上一次我们反编译了手Q,并遇到了Apktool反编译直接crash的问题,虽然笔者很想在这次解决这个问题,但在解决途中,发现该保护依赖于很多知识,所以本次先插入一下,正所谓知其然知其所以然,授之鱼不如授之以渔,只有知道一些基本原理,才能让我们以后能自行解决更多问题。

    那么,你知道么?从我们在Android Studio中,点击run,到app运行在手机上,之间究竟发生了什么,代码和资源是怎么变成APK的,而APK又是怎么安装上去,并能执行的呢。

    Android逆向分析APK的打包与安装

    我们或许都能说出来像上图这样一个简单的过程:Android工程编译打包为APK,签名后通过ADB push到设备或者模拟器上安装。但是再深入就蒙了。

    希望看完下文,大家能对整个过程有一定了解。

    源码:资源部分为Android 4.4,后半段改为了6.0_r2

    打包

    APK是Android Package的缩写,实际上APK就是一个zip压缩包,使用zip解压软件直接就能对其进行解压,解压后会发现就是由各种资源文件、一或多个dex文件(odex过的apk除外)、AndroidManifest.xml、resources.arsc以及其他一些文件组成的。

    我们先看看从Android在线文档找来的APK文件构建流程图,如下(方形为对象,圆形为动作)。

    Android逆向分析APK的打包与安装

    从该图来看,整个打包过程可以分为以下七个步骤:

    第1步:aapt

    打包资源文件,生成R.java和编译后的资源。aapt的可执行文件位于sdk的build-tools下,而源码则在frameworks\base\tools\aapt目录下。打包过程主要是调用了Resources.cpp下的buildResources()

    路径为Main.cpp下的handleCommand(Bundle* bundle)Command.cpp下的doPackage(Bundle* bundle),经过一些初始化和检查后调用了我们要深入看的buildResources(Bundle* bundle, const sp<AaptAssets>& assets)。因为代码都比较长,这里不贴了,主要说一下大概的逻辑和流程。

    检查AndroidManifest.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    // String8是android framework源码定义的数据格式,用来保存UTF-8字符的字符串,类似的还有一个String16,用来放UTF-16字符串的。
    sp<AaptGroup> androidManifestFile =
            assets->getFiles().valueFor(String8("AndroidManifest.xml"));
    if (androidManifestFile == NULL) {
        fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n");
        return UNKNOWN_ERROR;
    }
    
    status_t err = parsePackage(bundle, assets, androidManifestFile);
    if (err != NO_ERROR) {
        return err;
    }
    
    NOISY(printf("Creating resources for package %s\n",
                 assets->getPackage().string()));
    

    主要做一些检查并使用parsePackage初始化并设置一些attribute,比如packageminSdkVersionuses-sdk

    添加被引用资源包

    使用table.addIncludedResources(bundle, assets)添加被引用资源包,比如系统的那些android:命名空间下的资源。

    收集资源文件

    收集资源文件:

    1
    2
    
    static void collect_files(const sp<AaptDir>& dir,
            KeyedVector<String8, sp<ResourceTypeSet> >* resources);
    

     

    处理overlay(重叠包,如果指定的重叠包有和当前编译包重名的资源,则使用重叠包的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    // apply the overlay files to the base set
    if (!applyFileOverlay(bundle, assets, &drawables, "drawable") ||
            !applyFileOverlay(bundle, assets, &layouts, "layout") ||
            !applyFileOverlay(bundle, assets, &anims, "anim") ||
            !applyFileOverlay(bundle, assets, &animators, "animator") ||
            !applyFileOverlay(bundle, assets, &interpolators, "interpolator") ||
            !applyFileOverlay(bundle, assets, &transitions, "transition") ||
            !applyFileOverlay(bundle, assets, &xmls, "xml") ||
            !applyFileOverlay(bundle, assets, &raws, "raw") ||
            !applyFileOverlay(bundle, assets, &colors, "color") ||
            !applyFileOverlay(bundle, assets, &menus, "menu") ||
            !applyFileOverlay(bundle, assets, &mipmaps, "mipmap")) {
        return UNKNOWN_ERROR;
    }
    

     

    将收集到的资源文件加到资源表(ResourceTable)

    对res目录下的各个资源子目录进行处理,函数为makeFileResources:

    1
    2
    3
    4
    
    static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
                                      ResourceTable* table,
                                      const sp<ResourceTypeSet>& set,
                                      const char* resType);
    

     

    makeFileResources会对资源文件名做合法性检查,并将其添加到ResourceTable内。

    编译values资源并添加到资源表

    在上一步添加过程中,其实并没有对values资源进行处理,因为values比较特殊,需要经过编译之后,才能添加到资源表中。

    编译会调用ResourceTablecompileResourceFile进行:

    1
    2
    3
    4
    5
    6
    
    status_t compileResourceFile(Bundle* bundle,
                                 const sp<AaptAssets>& assets,
                                 const sp<AaptFile>& in,
                                 const ResTable_config& defParams,
                                 const bool overwrite,
                                 ResourceTable* outTable);
    

     

    给bag资源分配id

    在继续编译其他资源之前,我们需要先给bag资源(attrs,比如orientation这种属性的取值范围定义的子元素)分配id,因为其他资源可能对它们有引用。

    1
    
    status_t ResourceTable::assignResourceIds();
    

    编译xml资源文件

    最后我们终于可以编译xml文件了,因为我们已经为它准备好了一切可能引用到的东西(value, drawable等)。

    程序会对layouts, anims, animators等逐一调用ResourceTable.cpp的:

    1
    2
    3
    4
    
    status_t compileXmlFile(const sp<AaptAssets>& assets,
                            const sp<AaptFile>& target,
                            ResourceTable* table,
                            int options);
    

     

    进行编译,内部流程又可以分为:解析xml文件,赋予属性名称资源id,解析属性值,扁平化为二进制文件(调用flatten(Bundle* bundle, const sp<AaptFile>& dest))。

    编译AndroidManifest.xml文件

    该步骤其实也可以归为上一步,但由于manifest文件的特殊,所以姑且抽了出来。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    // 拿到AndroidManifest.xml文件
    const sp<AaptFile> manifestFile(androidManifestFile->getFiles().valueAt(0));
    String8 manifestPath(manifestFile->getPrintableSource());
    
    // 生成最终编译后的manifest文件
    
    // 清空原来的数据,重新解析
    manifestFile->clearData();
    sp<XMLNode> manifestTree = XMLNode::parse(manifestFile);
    if (manifestTree == NULL) {
        return UNKNOWN_ERROR;
    }
    // 马杀鸡manifest(从bundle读取必要的tag并写到manifestTree,
    // 处理package name重载,把各种相对路径的名字改为绝对路径)
    err = massageManifest(bundle, manifestTree);
    if (err < NO_ERROR) {
        return err;
    }
    // 编译manifest xml文件
    err = compileXmlFile(assets, manifestTree, manifestFile, &table);
    if (err < NO_ERROR) {
        return err;
    }
    

    生成最终资源表

    生成我们解压后看到的那个resources.arsc:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    if (table.hasResources()) {
        // 生成资源符号表
        sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R"));
        err = table.addSymbols(symbols);
        if (err < NO_ERROR) {
            return err;
        }
        // 生成资源索引表resources.arsc
        resFile = getResourceFile(assets);
        if (resFile == NULL) {
            fprintf(stderr, "Error: unable to generate entry for resource data\n");
            return UNKNOWN_ERROR;
        }
    
        err = table.flatten(bundle, resFile);
        if (err < NO_ERROR) {
            return err;
        }
    
        // 可能有一些同学用到过,这就是public.xml里定义的固定资源id
        if (bundle->getPublicOutputFile()) {
            FILE* fp = fopen(bundle->getPublicOutputFile(), "w+");
            if (fp == NULL) {
                fprintf(stderr, "ERROR: Unable to open public definitions output file %s: %s\n",
                        (const char*)bundle->getPublicOutputFile(), strerror(errno));
                return UNKNOWN_ERROR;
            }
            if (bundle->getVerbose()) {
                printf("  Writing public definitions to %s.\n", bundle->getPublicOutputFile());
            }
            table.writePublicDefinitions(String16(assets->getPackage()), fp);
            fclose(fp);
        }
    
        // 把资源读回来到最终资源表
        finalResTable.add(resFile->getData(), resFile->getSize(), NULL);
        // 生成完毕
    }
    

     

    而具体的resources.arsc生成则在ResourceTable.cpp:

    1
    2
    
    // 一个400行的函数,具体的生成实现在这里
    status_t ResourceTable::flatten(Bundle* bundle, const sp<AaptFile>& dest);
    

     

    写入顺序为 索引资源表头部(ResourceTypes:ResTable_header) -> 资源项的值字符串资源池 -> Package数据块。

    验证AndroidManifest.xml文件

    验证manifest各个属性对应值的合法性,即value中能出现的字符,完成后资源正式处理完毕,添加到AaptAssets:

    1
    2
    3
    4
    5
    6
    7
    8
    
    if (resFile != NULL) {
        // These resources are now considered to be a part of the included resources, for others to reference.
        err = assets->addIncludedResources(resFile);
        if (err < NO_ERROR) {
            fprintf(stderr, "ERROR: Unable to parse generated resources, aborting.\n");
            return err;
        }
    }
    

     

    生成R.java

    终于,我们已经读取并处理好了需要的一切,是时候开始写文件了,于是又回到了Command.cppdoPackage:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    
    // 更新那些需要被作为Java符号的符号
    assets->applyJavaSymbols();
    if (SourcePos::hasErrors()) {
        goto bail;
    }
    
    // 如果需要则在这里生成.d依赖文件
    ...
    
    // 写R.java常量
    if (!assets->havePrivateSymbols()) {
        if (bundle->getCustomPackage() == NULL) {
            // 将R.java文件写到恰当的目录下
            // 如 gen/com/foo/app/R.java
            err = writeResourceSymbols(bundle, assets, assets->getPackage(), true);
        } else {
            const String8 customPkg(bundle->getCustomPackage());
            err = writeResourceSymbols(bundle, assets, customPkg, true);
        }
        if (err < 0) {
            goto bail;
        }
        // 如果有library文件,则需要把R.java也写到那些libraries的对应class目录下
        // 如 gen/com/foo/app/lib/R.java
        if (bundle->getExtraPackages() != NULL) {
            // 冒号分割
            String8 libs(bundle->getExtraPackages());
            char* packageString = strtok(libs.lockBuffer(libs.length()), ":");
            while (packageString != NULL) {
                err = writeResourceSymbols(bundle, assets, String8(packageString), true);
                if (err < 0) {
                    goto bail;
                }
                packageString = strtok(NULL, ":");
            }
            libs.unlockBuffer();
        }
    } else {
        // 有不对外开放的私有符号
        err = writeResourceSymbols(bundle, assets, assets->getPackage(), false);
        if (err < 0) {
            goto bail;
        }
        err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true);
        if (err < 0) {
            goto bail;
        }
    }
    

    生成ProGuard文件

    1
    2
    3
    4
    
    err = writeProguardFile(bundle, assets);
    if (err < 0) {
        goto bail;
    }
    

    writeProguardFile(bundle, assets)则会调用

    • writeProguardForAndroidManifest(&keep, assets);
    • writeProguardForLayouts(&keep, assets);

    将规则更新到ProguardKeepSet中,然后打开proguard文件进行写入(proguard文件由-G命令指定)。

    生成apk

    又是一个洋洋洒洒150多行的函数,浓缩一下看看删减版Package.cpp:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    
    status_t writeAPK(Bundle* bundle, const sp<AaptAssets>& assets, const String8& outputFile)
    {
        ... 计时,初始化状态变量
        // so,apk就是作为一个zip包创建的
        ZipFile* zip = NULL;
        int count;
    
        /*
         * 准备Zip文件,如果文件存在,则根据bundle设置的"update"或者"force"进行对应处理
         */
        FileType fileType = getFileType(outputFile.string());
        ...
    
        // 准备完毕,new ZipFile并打开准备开始写
        status_t status;
        zip = new ZipFile;
        status = zip->open(outputFile.string(), ZipFile::kOpenReadWrite | ZipFile::kOpenCreate);
        if (status != NO_ERROR) {
            fprintf(stderr, "ERROR: unable to open '%s' as Zip file for writing\n",
                    outputFile.string());
            goto bail;
        }
    
        // 先把assets丢进去(即第10步我们准备好的东西,包括了assets目录, res目录下values以外的子目录 - 因为values已经被编译到资源索引表了, resources.arsc)
        count = processAssets(bundle, zip, assets);
        if (count < 0) {
            fprintf(stderr, "ERROR: unable to process assets while packaging '%s'\n",
                    outputFile.string());
            result = count;
            goto bail;
        }
    
        // 再把用-j指定的包含classes的jar或者zip包给丢进去
        count = processJarFiles(bundle, zip);
        if (count < 0) {
            fprintf(stderr, "ERROR: unable to process jar files while packaging '%s'\n",
                    outputFile.string());
            result = count;
            goto bail;
        }
    
        // 走到这儿算是处理成功了
        result = NO_ERROR;
    
        /*
         * 各种标记删除,然后flush zip
         */
        ...
    
        /* 没东西要你何用,删 */
        if (zip->getNumEntries() == 0) {
            ...
        }
    
        // 如果被要求生成依赖文件,则在这里进行,最后写到指定apk输出目录下,如bin/resources.ap_.d
        ...
    
        assert(result == NO_ERROR);
    
    // 上面任何一部出错则goto跑来这儿,删除文件
    bail:
        delete zip;        // must close before remove in Win32
        if (result != NO_ERROR) {
            ...删除并unlink
        }
    
        if (result == NO_ERROR && bundle->getVerbose())
            printf("Done!\n");
    
        return result;
    }
    

     

    第2步:aidl

    处理aidl文件,调用build-tools下的aidl可执行文件生成对应的Java文件。该工具的源码位于frameworks\base\tools\aidl。

    aidl,全名Android Interface Definition Language,即Android接口定义语言。

    输入:aidl后缀的文件。
    输出:可用于进程通信的C/S端java代码,位于build/generated/source/aidl。

    对于这块,推荐Android开发艺术一书,其在跨进程一章对AIDL有比较详尽的描述。其实我们也完全可以自己去实现AIDL生成的那套,这里不进行赘述。

    插入 - RenderScript & BuildConfig

    插入一下官网图里没说到的。

    编译RenderScript,生成BuildConfig。

    第3步:Java源码编译

    我们有了R.java和aidl生成的Java文件,再加上工程的源代码,现在可以使用javac进行正常的java编译生成class文件了。

    输入:java source的文件夹(另外还包括了build/generated下的:R.java, aidl生成的java文件,以及BuildConfig.java)。
    输出:对于gradle编译,可以在build/intermediates/classes里,看到输出的class文件。

    第4步:dex

    调用dx.bat将所有的class文件(上一步生成的以及第三方库的)转化为classes.dex文件,实际调用的是build-tools\lib\dx.jar,其源码位于libcore\dex(描述Dex文件的格式)及dalvik\dx(包含dx及multidex打包)下。

    dx会将class转换为Dalvik字节码,生成常量池,消除冗余数据等。

    关于dex,我们下一篇会单独去细说。

    第5步:apkbuilder

    打包生成APK文件。旧的apkbuilder脚本已经废弃,现在都已经通过sdklib.jarApkBuilder类进行打包了。输入为我们之前生成的包含resources.arcs的.ap_文件,上一步生成的dex文件,以及其他资源如jni、jar包内的资源。

    大致步骤为

    1. 以包含resources.arcs的.ap_文件为基础,new一个ApkBuilder,设置debugMode
    2. apkBuilder.addZipFile(f);
    3. apkBuilder.addSourceFolder(f);
    4. apkBuilder.addResourcesFromJar(f);
    5. apkBuilder.addNativeLibraries(nativeFileList);
    6. apkBuilder.sealApk(); // 关闭apk文件
    7. generateDependencyFile(depFile, inputPaths, outputFile.getAbsolutePath());

    第6步:Jarsigner

    对apk文件进行签名。APK需要签名才能在设备上进行安装,源码在build\tools\signapk下。

    很多时候我们在逆向改完后,会因为没有签名文件导致最后的apk无法正常使用,又细分为本地验证和服务器验证。

    第7步:zipalign

    调用buildtools\zipalign,对签名后的apk文件进行对齐处理,使apk中所有资源文件距离文件起始偏移为4字节的整数倍,从而在通过内存映射访问apk文件时会更快。

    这样我们的最终apk就生成完毕了,对gradle是如何在输入gradle assembleDebug之后打包的,可以参见aosp下builder/src/main/java/com/android/builder目录,这样你可以更了解整个流程和每个gradle子任务做了什么(像是BuildConfig是怎么生成的)。

    ADB

    ADB, 全名 Android Debug Bridge,不仅仅是命令行我们输入的adb xxx命令,Debug, Device Monitor, DDMS也都是通过adb来完成设备与我们的开发机器的通信的。

    比如当我们在命令行输入
    Android逆向分析APK的打包与安装

    实际上就会有2个进程被起起来(这就是下文提到的组件中的client和server了)
    Android逆向分析APK的打包与安装

    角色

    ADB扮演了2个角色

    • 传输。host和设备间的通信路径。可能是USB,也可能是TCP,但host不需要关心。
    • 服务。通过传输提供服务,在目标设备上执行指定命令。

    组件

    ADB中有3个组件

    • adb clients。其实就是那个子命令的可执行文件。比如起了3个adb shell,那就是3个clients。
    • adb server(就是那个动不动卡死要restart的东西)。在开发机器的后台运行,扮演着adb clients和adbd之间的中介,让彼此可以通信。
    • adb daemon(adbd)。在目标设备上运行的后台进程;由init启动,死掉后会由init重启。

    server的启动

    当启动adb client的时候,client首先会检查是否有adb server进程在运行中,如果没有则启动进程。

    server启动后会绑定到TCP端口5037,并监听来自adb clients的命令。接着server会通过扫描5555到5585之间的奇数端口(被模拟器和物理设备所使用),建立到所有运行中设备实例的连接。一旦server找到adb daemon,就会建立到那个端口的连接(而未开启USB调试的设备则没有adb daemon运行)。

    每个设备实例都需要一对连续的端口(这就是为什么刚才只扫描奇数端口),一个偶数端口用于控制连接,一个奇数端口用于adb连接,例如:

    模拟器1,控制: 5554
    模拟器2,adb: 5555
    Nexus6,控制: 5556
    Nexus6, adb: 5557

    如上,5554和5555其实都是被同一台设备所使用。

    内部实现

    源码位于aosp的system/core/adb目录下,adb和adbd都是从这儿编译出来的。
    有一部分文件是共用的:adb.c, fdevent.c, transort.c, transport_local.c, tansport_usb.c, service.c, sockets.c, util.c。

    举个例子adb.c:

    1
    2
    3
    4
    5
    6
    7
    
    std::string get_trace_setting() {
    #if ADB_HOST
        return get_trace_setting_from_env();
    #else
        return get_trace_setting_from_prop();
    #endif
    }
    

     

    通过ADB_HOST这个宏编译不同的代码。其他大部分文件则由server和client后缀可以区分。

    跟我们的主题息息相关的主要就是install系列的命令了,先看看命令使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    adb install [-lrtsdg] <file>
                                 - 把安装包文件push到设备并安装
                                   (-l: forward lock application)
                                   (-r: replace existing application)
                                   (-t: allow test packages)
                                   (-s: install application on sdcard)
                                   (-d: allow version code downgrade)
                                   (-g: grant all runtime permissions)
    
    adb install-multiple [-lrtsdpg] <file...>
                                 - 把安装包文件push到设备并安装
                                   (-l: forward lock application)
                                   (-r: replace existing application)
                                   (-t: allow test packages)
                                   (-s: install application on sdcard)
                                   (-d: allow version code downgrade)
                                   (-p: partial application install)
                                   (-g: grant all runtime permissions)
    
    adb uninstall [-k] <package> - 从设备上卸载该app
                                   ('-k' means keep the data and cache directories)
    

     

    分别对应commandline.cpp下的三个方法:

    1
    2
    3
    
    static int install_app(transport_type t, const char* serial, int argc, const char** argv);
    static int install_multiple_app(transport_type t, const char* serial, int argc, const char** argv);
    static int uninstall_app(transport_type t, const char* serial, int argc, const char** argv);
    

     

    adb install

    这里以install命令为例看看adb做了什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    static int install_app(transport_type transport, const char* serial, int argc,
                           const char** argv)
    {
        static const char *const DATA_DEST = "/data/local/tmp/%s";
        static const char *const SD_DEST = "/sdcard/tmp/%s";
        const char* where = DATA_DEST;
    
        // 解析-s参数,有则把where改成SD_DEST
        ...
    
        // 找到最后的APK参数,检查是否是APK,不是则提示"Invalid APK file",然后直接退出
        ...
    
        const char* apk_file = argv[last_apk];
        char apk_dest[PATH_MAX];
        // apk_dest这样就是最终要拷贝过去的完整路径了,比如"/data/local/tmp/app.apk"
        snprintf(apk_dest, sizeof apk_dest, where, get_basename(apk_file));
        // 把文件push上去
        int err = do_sync_push(apk_file, apk_dest, 0 /* no show progress */);
        if (err) {
            goto cleanup_apk;
        } else {
            argv[last_apk] = apk_dest; /* destination name, not source location */
        }
        // 通过pm安装
        err = pm_command(transport, serial, argc, argv);
    
    // push出错了就删了文件
    cleanup_apk:
        delete_file(transport, serial, apk_dest);
        return err;
    }
    
    // pm命令
    static int pm_command(transport_type transport, const char* serial,
                          int argc, const char** argv)
    {
        std::string cmd = "shell:pm";
    
        while (argc-- > 0) {
            cmd += " " + escape_arg(*argv++);
        }
    
        return send_shell_command(transport, serial, cmd);
    }
    

     

    彩蛋

    还有几个有趣的命令

    1
    2
    3
    4
    5
    
    # 跟adb shell差不多,不过颜色很hell
    adb hell
    
    # 笑你妹
    adb lolcat
    

     

    安装

    为什么有时候会安装不上apk呢?安装的界面是怎么弹出来的?抱着这些疑问,我们看下去。

    安装方式

    大致上有四种

    1. 系统程序安装,开机时安装,没有安装界面。
      由开机时启动的PackageManagerService服务完成,会在启动时扫描/system/appvender/app/data/app/data/app-private并安装。
    2. 通过Android市场安装,Google Play可以直接安装,其他市场除非root,否则需要自己点击安装(除非定制rom),即和第4种一样。
    3. ADB安装,即上一节说的,也没有安装界面。shell:pm是PackageManagerService的Shell客户端,源码位于
      /frameworks/base/cmds/pm

    执行路径大致是从main -> run -> runInstall,挑一段最后的核心代码Pm.java:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    private int runInstall() {
        ....
        try {
            VerificationParams verificationParams = new VerificationParams(verificationURI,
                    originatingURI, referrerURI, VerificationParams.NO_UID, null);
            // 通过IPackageManager
            mPm.installPackageAsUser(apkFilePath, obs.getBinder(), installFlags,
                    installerPackageName, verificationParams, abi, userId);
    
            synchronized (obs) {
                while (!obs.finished) {
                    try {
                        obs.wait();
                    } catch (InterruptedException e) {
                    }
                }
                // 我们看到的成功失败返回信息
                if (obs.result == PackageManager.INSTALL_SUCCEEDED) {
                    System.out.println("Success");
                    return 0;
                } else {
                    System.err.println("Failure ["
                            + installFailureToString(obs)
                            + "]");
                    return 1;
                }
            }
        } catch (RemoteException e) {
            ...
        }
    }
    

     

    1. 手机自行通过文件浏览器打开安装,有安装界面。

    PackageInstaller

    当我们在手机的文件管理器或者notification点击apk文件,就会出现如下图所示(Nexus6 Android 6.0.1)的界面,点击安装按钮即可开始安装,点击取消按钮返回。

    Android逆向分析APK的打包与安装

    这个安装界面是Android系统程序PackageInstaller的PackageInstallerActivity,dump一下可以看到如下图信息

    Android逆向分析APK的打包与安装

    当Android系统请求安装apk程序时,会启动这个Activity,并通过Intent读取传来的apk信息,我们来简单看看该Activty onCreate的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    
    public class PackageInstallerActivity extends Activity implements OnCancelListener, OnClickListener {
        ...
        @Override
        protected void onCreate(Bundle icicle) {
            super.onCreate(icicle);
    
            // 拿到pm和installer
            mPm = getPackageManager();
            mInstaller = mPm.getPackageInstaller();
            mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
    
            final Intent intent = getIntent();
    
            // 检查permission,初始化读取mSessionId,mPackageURI,mOriginatingURI,mReferrerURI
            ...
    
            // 检查是否允许未知来源
            ...
    
            // 检查scheme是否支持,不支持则直接结束
            final String scheme = mPackageURI.getScheme();
            if (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) {
                Log.w(TAG, "Unsupported scheme " + scheme);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
                mInstallFlowAnalytics.setFlowFinished(
                        InstallFlowAnalytics.RESULT_FAILED_UNSUPPORTED_SCHEME);
                finish();
                return;
            }
    
            final PackageUtil.AppSnippet as;
            if ("package".equals(mPackageURI.getScheme())) {
                // package scheme
                mInstallFlowAnalytics.setFileUri(false);
                try {
                    mPkgInfo = mPm.getPackageInfo(mPackageURI.getSchemeSpecificPart(),
                            PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES);
                } catch (NameNotFoundException e) {
                }
                // 无法获得PackageInfo,直接退出
                if (mPkgInfo == null) {
                    ...
                }
                as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo),
                        mPm.getApplicationIcon(mPkgInfo.applicationInfo));
            } else {
                // file scheme
                mInstallFlowAnalytics.setFileUri(true);
                final File sourceFile = new File(mPackageURI.getPath());
                PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);
    
                // 检查解析错误,显示错误对话框,直接退出
                if (parsed == null) {
                    ...
                }
                // 生成PackageInfo
                mPkgInfo = PackageParser.generatePackageInfo(parsed, null,
                        PackageManager.GET_PERMISSIONS, 0, 0, null,
                        new PackageUserState());
                // manifest校验
                mPkgDigest = parsed.manifestDigest;
                // 设置apk的程序名和图标
                as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
            }
            mInstallFlowAnalytics.setPackageInfoObtained();
    
            setContentView(R.layout.install_start);
            mInstallConfirm = findViewById(R.id.install_confirm_panel);
            mInstallConfirm.setVisibility(View.INVISIBLE);
            PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);
    
            mOriginatingUid = getOriginatingUid(intent);
    
            // 如果必要则禁止来自未知来源的安装
            if (!requestFromUnknownSource) {
                // 进行一些其他的初始化工作
                initiateInstall();
                return;
            }
    
            // 未知来源检查,如果admin禁止则直接提示错误退出。否则显示选项提示用户去设置里修改该设置。
            final boolean isManagedProfile = mUserManager.isManagedProfile();
            if (!unknownSourcesAllowedByAdmin || (!unknownSourcesAllowedByUser && isManagedProfile)) {
                showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);
                mInstallFlowAnalytics.setFlowFinished(
                        InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);
            } else if (!unknownSourcesAllowedByUser) {
                showDialogInner(DLG_UNKNOWN_SOURCES);
                mInstallFlowAnalytics.setFlowFinished(
                        InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);
            } else {
                initiateInstall();
            }
        }
    }
    

    整个方法有2个重点函数。

    1) PackageUtil.getPackageInfo(sourceFile)

    getPackageInfo会构造PackageParser,调用Package parseMonolithicPackage(File apkFile, int flags)去解析该apk程序包,然后记录下manifest的校验码。

    parseMonolithicPackage()对于我们普通的app又会调用parseBaseApk(File apkFile, AssetManager assets, int flags)去做真正的解析并获得Package对象(该类里有很多给split apk用的方法和逻辑)。

    解析过程会首先读取AndroidManifest.xml获取程序包名以构建Package对象,然后再处理manifest的其他标签包括四大组件,并把信息全都存到Package对象里面。

    2) initiateInstall()

    首先检测该程序是否已安装,是则弹框提示是否替换程序,否则直接调用startInstallConfirm(),做UI初始化和事件绑定,于是当我们点击安装的时候则会触发onClick下的OK按钮事件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    if (mOkCanInstall || mScrollView == null) {
        mInstallFlowAnalytics.setInstallButtonClicked();
        if (mSessionId != -1) {
            mInstaller.setPermissionsResult(mSessionId, true);
    
            // We're only confirming permissions, so we don't really know how the
            // story ends; assume success.
            mInstallFlowAnalytics.setFlowFinishedWithPackageManagerResult(
                    PackageManager.INSTALL_SUCCEEDED);
            finish();
        } else {
            startInstall();
        }
    } else {
        mScrollView.pageScroll(View.FOCUS_DOWN);
    }
    

     

    对于本地app则会继续走startInstall的逻辑,开启一个新的activity,InstallAppProgress,该activity判断scheme进行不同的安装:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    if ("package".equals(mPackageURI.getScheme())) {
        try {
            pm.installExistingPackage(mAppInfo.packageName);
            observer.packageInstalled(mAppInfo.packageName,
                    PackageManager.INSTALL_SUCCEEDED);
        } catch (PackageManager.NameNotFoundException e) {
            observer.packageInstalled(mAppInfo.packageName,
                    PackageManager.INSTALL_FAILED_INVALID_APK);
        }
    } else {
        pm.installPackageWithVerificationAndEncryption(mPackageURI, observer, installFlags,
                installerPackageName, verificationParams, null);
    }
    

     

    installPackageWithVerificationAndEncryption()其实还是会调用installPackage(),结果和adb安装殊途同归,整个转的路径为installPackage() -> installPackageAsUser()(这儿会先检查调用者是否有安装的权限) -> processPendingInstall() -> installPackageLI():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
        ....
        if (replace) {
            // 替换已有程序
            replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,
                    installerPackageName, volumeUuid, res);
        } else {
            // 安装新程序
            installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
                    args.user, installerPackageName, volumeUuid, res);
        }
        ....
    }
    

     

    无论是替换还是新安装,都会调用scanPackageLI(),然后跑去scanPackageDirtyLI,它会判断是否为系统程序,解析apk程序包,检查依赖库,验证签名,检查sharedUser签名、权限冲突、ContentProvider冲突,更新native库目录文件(检测abi),进行dexopt,杀掉现有进程(仅对覆盖安装的场景)等等,最后调用createDataDirsLI()进行实际安装:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
    private int createDataDirsLI(String volumeUuid, String packageName, int uid, String seinfo) {
        int[] users = sUserManager.getUserIds();
        int res = mInstaller.install(volumeUuid, packageName, uid, uid, seinfo);
        if (res < 0) {
            return res;
        }
        for (int user : users) {
            if (user != 0) {
                res = mInstaller.createUserData(volumeUuid, packageName,
                        UserHandle.getUid(user, uid), user, seinfo);
                if (res < 0) {
                    return res;
                }
            }
        }
        return res;
    }
    

     

    mInstallerInstaller类的实例,但实际安装并不是在java做的,而会通过InstallerConnection把命令使用socket通信发到/system/bin/installd。

    在这里第一次call的install()对应命令为
    install uuid name uid gid seinfo
    而第二次call的createUserData则会使用命令
    mkuserdata uuid name uid userId seinfo

    installd是一个常驻进程,可以在adb shell通过ps | grep installd查看进程信息。源码位于frameworks/native/cmd/installd/installd.cpp下(dexopt也在这里哦),处理install命令的函数为do_install(), do_install调用了Command.cppinstall():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    
    int install(const char *uuid, const char *pkgname, uid_t uid, gid_t gid, const char *seinfo)
    {
        if ((uid < AID_SYSTEM) || (gid < AID_SYSTEM)) {
            ALOGE("invalid uid/gid: %d %d\n", uid, gid);
            return -1;
        }
    
        std::string _pkgdir(create_data_user_package_path(uuid, 0, pkgname));
        const char* pkgdir = _pkgdir.c_str();
    
        if (mkdir(pkgdir, 0751) < 0) {
            ALOGE("cannot create dir '%s': %s\n", pkgdir, strerror(errno));
            return -1;
        }
        if (chmod(pkgdir, 0751) < 0) {
            ALOGE("cannot chmod dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -1;
        }
    
        if (selinux_android_setfilecon(pkgdir, pkgname, seinfo, uid) < 0) {
            ALOGE("cannot setfilecon dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -errno;
        }
    
        if (chown(pkgdir, uid, gid) < 0) {
            ALOGE("cannot chown dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -1;
        }
    
        return 0;
    }
    
    // 5.0支持多用户后把用户数据和app数据的创建分开了,完成install后,
    // java层会调用这儿依次为所有用户创建用户数据,而过去这两个函数是合并的一个函数
    int make_user_data(const char *uuid, const char *pkgname, uid_t uid, userid_t userid, const char* seinfo)
    {
        std::string _pkgdir(create_data_user_package_path(uuid, userid, pkgname));
        const char* pkgdir = _pkgdir.c_str();
    
        if (mkdir(pkgdir, 0751) < 0) {
            ALOGE("cannot create dir '%s': %s\n", pkgdir, strerror(errno));
            return -errno;
        }
        if (chmod(pkgdir, 0751) < 0) {
            ALOGE("cannot chmod dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -errno;
        }
    
        if (selinux_android_setfilecon(pkgdir, pkgname, seinfo, uid) < 0) {
            ALOGE("cannot setfilecon dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -errno;
        }
    
        if (chown(pkgdir, uid, uid) < 0) {
            ALOGE("cannot chown dir '%s': %s\n", pkgdir, strerror(errno));
            unlink(pkgdir);
            return -errno;
        }
    
        return 0;
    }
    

     

    执行完毕后,通过socket回传结果,而PackageInstaller根据返回结果做对应处理并显示给用户,至此为止,整个apk安装过程结束。

    总结和下期预告

    我们了解了一个android工程是怎么变成apk的,apk是怎么跑到设备上,而最后又是如何安装的。下一次我们来看看dex和odex,art上的elf和oat都是什么,而dexopt又做了什么优化。dex加壳技术大多就是在dex上面做了手脚。

    Android逆向分析APK的打包与安装转载http://www.codesocang.com/anzhuoyuanma/boke/33921.html
    标签:网站源码