Android逆向分析(2) APK的打包与安装
http://blog.zhaiyifan.cn/2016/02/13/android-reverse-2/
2/18日增加对aidl和java编译的描述。
前言
上一次我们反编译了手Q,并遇到了Apktool反编译直接crash的问题,虽然笔者很想在这次解决这个问题,但在解决途中,发现该保护依赖于很多知识,所以本次先插入一下,正所谓知其然知其所以然,授之鱼不如授之以渔,只有知道一些基本原理,才能让我们以后能自行解决更多问题。
那么,你知道么?从我们在Android Studio中,点击run,到app运行在手机上,之间究竟发生了什么,代码和资源是怎么变成APK的,而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文件构建流程图,如下(方形为对象,圆形为动作)。
从该图来看,整个打包过程可以分为以下七个步骤:
第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 |
// String8是android framework源码定义的数据格式,用来保存UTF-8字符的字符串,类似的还有一个String16,用来放UTF-16字符串的。 |
主要做一些检查并使用parsePackage
初始化并设置一些attribute,比如package
, minSdkVersion
, uses-sdk
。
添加被引用资源包
使用table.addIncludedResources(bundle, assets)
添加被引用资源包,比如系统的那些android:命名空间下的资源。
收集资源文件
收集资源文件:
1 |
static void collect_files(const sp<AaptDir>& dir, |
处理overlay(重叠包,如果指定的重叠包有和当前编译包重名的资源,则使用重叠包的):
1 |
// apply the overlay files to the base set |
将收集到的资源文件加到资源表(ResourceTable
)
对res目录下的各个资源子目录进行处理,函数为makeFileResources
:
1 |
static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets, |
makeFileResources
会对资源文件名做合法性检查,并将其添加到ResourceTable内。
编译values资源并添加到资源表
在上一步添加过程中,其实并没有对values资源进行处理,因为values比较特殊,需要经过编译之后,才能添加到资源表中。
编译会调用ResourceTable
的compileResourceFile
进行:
1 |
status_t compileResourceFile(Bundle* bundle, |
给bag资源分配id
在继续编译其他资源之前,我们需要先给bag资源(attrs,比如orientation这种属性的取值范围定义的子元素)分配id,因为其他资源可能对它们有引用。
1 |
status_t ResourceTable::assignResourceIds(); |
编译xml资源文件
最后我们终于可以编译xml文件了,因为我们已经为它准备好了一切可能引用到的东西(value, drawable等)。
程序会对layouts, anims, animators等逐一调用ResourceTable.cpp
的:
1 |
status_t compileXmlFile(const sp<AaptAssets>& assets, |
进行编译,内部流程又可以分为:解析xml文件,赋予属性名称资源id,解析属性值,扁平化为二进制文件(调用flatten(Bundle* bundle, const sp<AaptFile>& dest)
)。
编译AndroidManifest.xml文件
该步骤其实也可以归为上一步,但由于manifest文件的特殊,所以姑且抽了出来。
1 |
// 拿到AndroidManifest.xml文件 |
生成最终资源表
生成我们解压后看到的那个resources.arsc
:
1 |
if (table.hasResources()) { |
而具体的resources.arsc生成则在ResourceTable.cpp
:
1 |
// 一个400行的函数,具体的生成实现在这里 |
写入顺序为 索引资源表头部(ResourceTypes:ResTable_header) -> 资源项的值字符串资源池 -> Package数据块。
验证AndroidManifest.xml文件
验证manifest各个属性对应值的合法性,即value中能出现的字符,完成后资源正式处理完毕,添加到AaptAssets
:
1 |
if (resFile != NULL) { |
生成R.java
终于,我们已经读取并处理好了需要的一切,是时候开始写文件了,于是又回到了Command.cpp
的doPackage
:
1 |
// 更新那些需要被作为Java符号的符号 |
生成ProGuard文件
1 |
err = writeProguardFile(bundle, assets); |
而writeProguardFile(bundle, assets)
则会调用
- writeProguardForAndroidManifest(&keep, assets);
- writeProguardForLayouts(&keep, assets);
将规则更新到ProguardKeepSet中,然后打开proguard文件进行写入(proguard文件由-G命令指定)。
生成apk
又是一个洋洋洒洒150多行的函数,浓缩一下看看删减版Package.cpp
:
1 |
status_t writeAPK(Bundle* bundle, const sp<AaptAssets>& assets, const String8& outputFile) |
第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.jar
的ApkBuilder
类进行打包了。输入为我们之前生成的包含resources.arcs的.ap_文件,上一步生成的dex文件,以及其他资源如jni、jar包内的资源。
大致步骤为
- 以包含resources.arcs的.ap_文件为基础,new一个ApkBuilder,设置debugMode
- apkBuilder.addZipFile(f);
- apkBuilder.addSourceFolder(f);
- apkBuilder.addResourcesFromJar(f);
- apkBuilder.addNativeLibraries(nativeFileList);
- apkBuilder.sealApk(); // 关闭apk文件
- 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来完成设备与我们的开发机器的通信的。
比如当我们在命令行输入
实际上就会有2个进程被起起来(这就是下文提到的组件中的client和server了)
角色
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 |
std::string get_trace_setting() { |
通过ADB_HOST这个宏编译不同的代码。其他大部分文件则由server和client后缀可以区分。
跟我们的主题息息相关的主要就是install系列的命令了,先看看命令使用:
1 |
adb install [-lrtsdg] <file> |
分别对应commandline.cpp
下的三个方法:
1 |
static int install_app(transport_type t, const char* serial, int argc, const char** argv); |
adb install
这里以install命令为例看看adb做了什么:
1 |
static int install_app(transport_type transport, const char* serial, int argc, |
彩蛋
还有几个有趣的命令
1 |
# 跟adb shell差不多,不过颜色很hell |
安装
为什么有时候会安装不上apk呢?安装的界面是怎么弹出来的?抱着这些疑问,我们看下去。
安装方式
大致上有四种
- 系统程序安装,开机时安装,没有安装界面。
由开机时启动的PackageManagerService
服务完成,会在启动时扫描/system/app
,vender/app
,/data/app
,/data/app-private
并安装。 - 通过Android市场安装,Google Play可以直接安装,其他市场除非root,否则需要自己点击安装(除非定制rom),即和第4种一样。
- ADB安装,即上一节说的,也没有安装界面。shell:pm是
PackageManagerService
的Shell客户端,源码位于
/frameworks/base/cmds/pm
执行路径大致是从main -> run -> runInstall,挑一段最后的核心代码Pm.java
:
1 |
private int runInstall() { |
- 手机自行通过文件浏览器打开安装,有安装界面。
PackageInstaller
当我们在手机的文件管理器或者notification点击apk文件,就会出现如下图所示(Nexus6 Android 6.0.1)的界面,点击安装按钮即可开始安装,点击取消按钮返回。
这个安装界面是Android系统程序PackageInstaller的PackageInstallerActivity,dump一下可以看到如下图信息
当Android系统请求安装apk程序时,会启动这个Activity,并通过Intent读取传来的apk信息,我们来简单看看该Activty onCreate的代码:
1 |
public class PackageInstallerActivity extends Activity implements OnCancelListener, OnClickListener { |
整个方法有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 |
if (mOkCanInstall || mScrollView == null) { |
对于本地app则会继续走startInstall
的逻辑,开启一个新的activity,InstallAppProgress,该activity判断scheme进行不同的安装:
1 |
if ("package".equals(mPackageURI.getScheme())) { |
installPackageWithVerificationAndEncryption()
其实还是会调用installPackage()
,结果和adb安装殊途同归,整个转的路径为installPackage()
-> installPackageAsUser()
(这儿会先检查调用者是否有安装的权限) -> processPendingInstall() -> installPackageLI():
1 |
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) { |
无论是替换还是新安装,都会调用scanPackageLI()
,然后跑去scanPackageDirtyLI
,它会判断是否为系统程序,解析apk程序包,检查依赖库,验证签名,检查sharedUser签名、权限冲突、ContentProvider冲突,更新native库目录文件(检测abi),进行dexopt,杀掉现有进程(仅对覆盖安装的场景)等等,最后调用createDataDirsLI()进行实际安装:
1 |
private int createDataDirsLI(String volumeUuid, String packageName, int uid, String seinfo) { |
mInstaller
为Installer
类的实例,但实际安装并不是在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.cpp
的install()
:
1 |
int install(const char *uuid, const char *pkgname, uid_t uid, gid_t gid, const char *seinfo) |
执行完毕后,通过socket回传结果,而PackageInstaller
根据返回结果做对应处理并显示给用户,至此为止,整个apk安装过程结束。
总结和下期预告
我们了解了一个android工程是怎么变成apk的,apk是怎么跑到设备上,而最后又是如何安装的。下一次我们来看看dex和odex,art上的elf和oat都是什么,而dexopt又做了什么优化。dex加壳技术大多就是在dex上面做了手脚。
参考文献:
- http://developer.android.com/tools/building/index.html
- http://developer.android.com/sdk/installing/studio-build.html
- http://blog.csdn.net/luoshengyang/article/details/8744683
- 《Android软件安全与逆向分析》,作者:丰生强,人民邮电出版社
- https://events.linuxfoundation.org/images/stories/pdf/lf_abs12_kobayashi.pdf
- http://developer.android.com/tools/help/adb.html
原文:http://blog.zhaiyifan.cn/2016/02/13/android-reverse-2/
写的途中还不慎看到csdn上某排名500多的百度大V声称自己看老罗的博客并结合参考资料写的整理,实则完全就是照抄书上的,连错误的地方都照抄了,也没有说是人家的,我也是呵呵哒。更有趣的是该作者还在新的文章里提到觉得网上的文章内容重复太多。恩恩,不愧是伟大的百度公司的开发。实在忍不住喷一下。果然还是太年轻。
Android逆向分析(2) APK的打包与安装的更多相关文章
- Android逆向分析(2) APK的打包与安装背后的故事
前言 上一次我们反编译了手Q,并遇到了Apktool反编译直接crash的问题,虽然笔者很想在这次解决这个问题,但在解决途中,发现该保护依赖于很多知识,所以本次先插入一下,正所谓知其然知其所以然,授之 ...
- android应用分析之apk文件结构
实际上,一个APK文件就是一个.zip格式的压缩包,我们可以用解压缩工具打开任何一个APK文件,由于代码混淆和加密,通过普通解压缩工具打开里面的文件或目录会看到各种乱码.一个典型的ap ...
- Android逆向分析工具表
逆向分析工具表 工具 描述 网址 androidterm Android Terminal Emulator http://code.google.com/p/androidterm/ droidbo ...
- android逆向基础:apk 反编译 重打包 重签名
apk 反编译大家都比较熟悉,这里只做一个笔记. 1 反编译 apk apktool d perfect.apk 这样就把资源文件解压缩了, classes.dex 也反编译成了 smali 文件 2 ...
- [摘]Android逆向分析常用网站
androidterm: Android Terminal Emulator http://code.google.com/p/androidterm/ droidbox: Andro ...
- android逆向分析之smali语法
一 .smali数据类型 1.Dalvik字节码 Davlik字节码中,寄存器都是32位的,能够支持任何类型,64位类型(Long/Double)用2个连续的寄存器表示: Dalvik字节码有两种类型 ...
- 路由器逆向分析------sasquatch和squashfs-tools工具的安装和使用
本文博客地址:http://blog.csdn.net/qq1084283172/article/details/68942660 一.sasquatch工具的安装和使用 sasquatch工具支持对 ...
- 【转】Android逆向入门流程
原文:https://www.jianshu.com/p/71fb7ccc05ff 0.写在前面 本文是笔者自学笔记,以破解某目标apk的方式进行学习,中间辅以原理性知识,方便面试需求. 参考文章的原 ...
- android逆向学习小结--CrackMe_1
断断续续的总算的把android开发和逆向的这两本书看完了,虽然没有java,和android开发的基础,但总体感觉起来还是比较能接收的,毕竟都是触类旁通的.当然要深入的话还需要对这门语言的细节特性和 ...
随机推荐
- Asp.net Webform 使用Repository模式实现CRUD操作代码生成工具
Asp.net Webform 使用Repository模式实现CRUD操作代码生成工具 介绍 该工具是通过一个github上的开源项目修改的原始作者https://github.com/Supere ...
- IdentityServer4 中文文档 -8- (快速入门)设置和概览
IdentityServer4 中文文档 -8- (快速入门)设置和概览 原文:http://docs.identityserver.io/en/release/quickstarts/0_overv ...
- [PHP] 算法-根据前序和中序遍历结果重建二叉树的PHP实现
输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5 ...
- centos7使用yum安装mysql 【转】
转自:http://blog.csdn.net/eclothy/article/details/52733891 使用: yum install mariadb* (注意,带星号) 安装好后,启 ...
- 通过Eureka自带REST API强行剔除失效服务
1.确定需要强行剔除的服务 2.执行接口 方便复制: http://{ip}:{port}/eureka/apps/CONFIG-SERVER-TEST/tom:config-server-test: ...
- Linux常用基本命令:三剑客命令之-awk基础用法
awk是一个超级强大的文本格式化处理工具,他与grep, sed命令被成为linux 三剑客命令 三剑客命令的特点: grep:只要用来匹配和查找文本 sed: 编辑匹配到文本 awk: 格式化文本, ...
- echarts2.0仪表盘
option = { backgroundColor: '#0e0b2a', tooltip : { formatter: "{a} <br/>{b} : {c}%" ...
- 初学CSS-2-文本的属性
文本装饰属性: 格式:text-decoration:underline: 取值:underline(下划线) line-through(删除线) overline(上划线) none(什么都没有) ...
- canvas-2rect.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- AIMLBot (中文自动回复)文本自动回复机器人
https://github.com/chivandikwa/AIMLBot (csharp) https://github.com/gunthercox/ChatterBot (python) ht ...