apk文件
apk实际上就是一个zip文件,可以直接使用zip解压,它包含 classes.dex, 资源文件,证书,动态链接库等。
classes.dex: 代码文件,包含可以被Dalvik解释执行的字节码。build ROM的时候,还可以使用dex2oat把.dex部分代码预编译成 .odex文件,用来提高app运行的速度,因为,odex实质上是一个ELF格式的可执行文件,它里面是机器码,可以直接在虚拟机运行。 当存在 .odex文件的时候,虚拟机不需要使用JIT来运行.dex文件。
动态链接库: native代码,如JNI库。它的目录结构是 lib/{abi}/*.so。如:
apk 签名
签名最大的作用是保证apk不被篡改,如果apk被重新签名,那么原apk也不会被替换。
可以做一个测试:首先, 下载一个第三方的A.apk,安装到设备上。然后,运行命令行工具apksigner使用自己的key来签名,
apksigner sign --ks my-release-key --out my-A.apk A.apk
再使用 adb install -r my-A.apk,来安装被篡改了签名的apk时,系统就会报错。因为,虽然包名相同,但是签名被改了,系统不会去替换之前的app。
Android 7 以下,使用的是'JAR signing (v1 scheme)',它是一个基于"signed JAR"文件格式。签名文件在META-INF目录,主要有如下几个文件。
- MANIFEST.MF -- 记录各文件的hash值。
- CERT.SF -- 证书签名文件。
- CERT.RSA -- 证书。
Android7 引入了 'APK Signature Scheme v2'。
ABI: Application Binary Interface
主要内容包括:机器指令集,可执行文件的格式,内存对齐方式等等。目前,Android系统大概有如下几类:
- armeabi--基于ARM的CPU,至少支持ARMv5TE指令集。
- armeabi-v7a-- armeabi的扩展指令集,主要是增加了Thumb-2 和VFP hardware-FPU等指令。
- arm64-v8a-- 基于ARMv8的64位CPU,支持 AArch64,NEON 和VFPv4指令集
- x86--基于x86和IA-32指令集的CPU
- x86_64--64位的x86。
Application.mk 里面使用可以使用APP_ABI变量来指定 ABI,如:
APP_ABI := armeabi-v7a arm64-v8a x86
APP_ABI := all
AndroidManifest.xml安装相关的一些配置
<application android:multiArch = ["true", "false"]
android:installLocation = ["auto","internalOnly","preferExternal"]
pm instll 命令
常用的参数如下:
-r 重新安装,替换已安装的包。
-t 允许测试。
-s 安装到外置存储卡。
-f 安装到手机内部空间。
-d 允许降级。
--abi 指定cpu架构。
--user
APK的安装流程
以pm install命令为例,流程的起点是,Pm --> INIT_COPY message --> InstallParams.handleStartCopy()
log 信息: Calling main entry com.android.commands.pm.Pm PackageManager: init_copy idx=0:InstallParams { file=/data/local/tmp/x.com.apk cid=null} PackageManager: startCopy UserHandle{-1}: InstallParams{...}
1:先得到 apk的基本信息,确定安装位置。
DefaultContainerService.getMinimalPackageInfo()--主要是确定安装位置,检查存储空间,在空间不足的情况下,系统会释放cache,再进行一次尝试。
- 首先,读取 AndroidManifest.xml中的属性:installLocation, versionCode,revisionCode和coreApp。
- 然后,再解决安装位置,PackageHelper.resolveInstallLocation()会根据用户选择来确定apk是安装到外部空间(SD卡),还是内部存储空间。
- 优先选择apk指定的安装位置。
- 未指定安装位置,优先选择该apk已被安装的位置(升级安装)。
- 默认选择内部空间。
- 内部空间不足的情况下,选择外部空间。
2:检查安装包。
是否允许覆盖(重装)(-r),是否允许降级(-d),以及一些安全方面的校验,校验结束后,发送 CHECK_PENDING_VERIFICATION 消息。
3:拷贝apk文件。
安装位置是sd卡,则走AsecInstallArgs.copyApk(),否则走FileInstallArgs.copyApk()这一路。这两路的过程都差不多,下面以安装到内部空间,为例,来详细说明一下。
- 创建临时文件夹。
PackageInstallService.allocateInternalStageDirLegacy()创建一个随机的临时文件夹。 如:/data/app/vmd1823593572.tmp
- 拷贝apk文件-- apk到拷贝成'临时目录下/base.apk'。
- 从apk包中,提取动态库(*.so)。
扫描apk里面lib目录下的{abi}文件夹,优先扫描 'lib/<primary-abi>/',如果不存在, 则继续扫描 'lib/<secondary-abi>/',如果能找到so文件,则把他们拷贝到'临时目录/lib/'
- 签名校验
- 通过CERT.RSA的签名,确认它和 CERT.SF的完整性。
- 通过CERT.SF里面的hash值来验证MANIFEST.MF文件的完整性。
- 通过MANIFEST.MF里面的hasn值来验证各个文件的完整性。
- 重命名临时文件夹。
把临时文件夹重命名为包名+数字,如果有重复的包名,则末尾数字自动往上加。相 应的工作是installPackageLI() --> FileInstallArgs.doRename()里完 成的。
log 信息:
DefContainer Copying /data/local/tmp/x.apk to base.apk
copy native library:/data/app/vmdl823593572.tmp DIR:lib
installPackageLI: path=/data/app/vmdl823593572.tmp
Renaming /data/app/vmdl823593572.tmp to /data/app/com.x-2 (重复安装)
primary-abi 和 secondary-abi
primary-abi: 设备支持的指令集。
secondary-abi: 设备兼容的指令集。
系统会访问三个属性(abilist, abilist32, abilist64)来得到设备所支持的ABI,如:
[ro.product.cpu.abilist32]: [armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]: []
[ro.product.cpu.abilist] : [armeabi-v7a,armeabi]
上面数组,索引值越低,优先级越高,上面例子中 armeabi-v7a的优先级比armeabi 高。 primary-abi是 'armeabi-v7a',secondary-abi是 'armeabi'。
系统在解包动态库的时候,如果 multiArch设置为true,那么 abilist32 和 abilist64这两个数组都被选中;否则,在abilist和 abilist32间选择一个合适的数组。选择好abilist后,数组中的<primary-abi>/*.so 会被拷贝出来,如果不存在,则选择 secondary-abi下的so文件。
签名校验
1: 证书校验
读取证书签名文件'CERT.SF'内容,并计算其hash值,与证书'CERT.RAS'中的签名进行比对,验证证书和证书签名文件的完整性。这个过程主要在 JarVerifier.readCertificates()里,文件位置在:libcore/luni/src/main/java/java/util/jar/
程序流程是:PackageParser.parseApkLite() --> collectCertificates() --> new StrictJarFile(apkPath)
2: MANIFEST.MF校验
计算'MANIFEST.MF'的hash值,与 CERT.SF 文件中的 xx-Digest-Manifest 属性 值(hash)进行比较,验证.MF文件的完整性。如果,校验失败,则依次对 读取.MF文件中各个段的内容,并计算其hash值与 CERT.SF中的记录进行对比,校验.MF文件中文件段是否被篡改,如:
CERT.SF
Name: res/drawable-hdpi-v4/radiobutton.png
SHA1-Digest: d9QDKL5z9PY+Z7F13tVkg5iXO3Y=MANIFEST.MF
Name: res/drawable-hdpi-v4/radiobutton.png
SHA1-Digest: xvssj1CTSdsivtCO/0Z3JHycuQw=
CERT.SF记录的就是 MANIFEST.MF文件里面对应内容的hash值,如果 MANIFEST.MF SHA1-Digest的值被篡改,那么hash校验就会失败。
代码在 JarVerifier.verifyCertificate()的最后面
// Use .SF to verify the whole manifest.
String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
if (!verify(attributes, digestAttribute, manifestBytes, 0,
manifestBytes.length, false, false)) {
…..
while (it.hasNext()) {
Map.Entry<String, Attributes> entry = it.next();
Manifest.Chunk chunk = manifest.getChunk(entry.getKey());
…..
if (!verify(entry.getValue(), "-Digest", manifestBytes,
chunk.start, chunk.end, createdBySigntool, false)) {
throw invalidDigest(signatureFile, entry.getKey(), jarName);
}
}
}
我们可以故意在 MANIFEST.MF的第2行版本号后加个空格,导致 digestAttribute 校验不过,然后,把entry key, start,end都打印出来看一下。
digestAttribute: -Digest-Manifest
entry.key:res/drawable-mdpi-v4/icnfinder.png s:3935 e:4022
entry.key:res/drawable-hdpi-v4/splash3.png s:11570 e:11655
entry.key:res/drawable-hdpi-v4/notify_selected.png s:4289 e:4382
entry.key:res/drawable-hdpi-v4/backarrow_n.png s:9568 e:9657
3: 所有文件的校验
确认了MANIFEST.MF的完整性后,就可以扫描APK内的所有文件,逐一计算hash值,并与 MANIFEST.MF中对应的文件hash属性值进行校验。PackageParser.collectCertificates(),遍历所有的文件,通过loadCertificates()调用JarFileInputStream.read()一边读取文件,一边使用 VerifierEntry.write()计算hash值,最后,与MANIFEST.MF中的hash值进行比较。
至此,签名校验结束,在这个流程下,想要篡改某个文件是不可能的。
假如,修改了某个文件A吧,那么MANIFEST.MF中的A的hash值就对不上了,不得不修改.MF文件。这样一来,连锁反应,CERT.SF里面的hash也对不上了,只好再来修改.SF中的hash值。接下来,CERT.RSA的校验过不了。 当你重新计算了 CERT.SF文件的hash值,踌躇满志地准备修改CERT.RSA签名的时候,顿时傻眼了,因为--.RSA里面的签名信息是用开发者的私钥加密的, 也就意味着,你无法对它进行修改,你算不出hash加密后的数据。
这个时候,你恶向胆边生,干脆用自己的私钥重新签名了这个apk,这下,虽然,文件校验可以通过了,但是,apk的签名key被改变,安装的时候,如果,系统里面有该apk,你的山寨货就会被拒绝。如果,系统没有该apk,呢?
恭喜你,该手机就能安装上你的山寨apk了。所以,下载apk一定要确认来源,是否打开"未知来源"选项也要慎重。
PS:
1:SHA1 是 digest的算法中的一种,其它还有 "SHA-512","SHA-384","SHA-256"。
不同的算法,对应.SF和.MF文件内容会有差异,有的可能会是SHA-512-Digest:xxx
可以使用下面命令查看证书信息。
openssl pkcs7 -inform DER -in CERT.RSA -noout -print_certs -text