Apk 组成

apk 其实就是一个压缩文件,把文件的后缀改成 .zip 就可以用 windows 解压软件解压了。

  • AndroidManifest.xml:App 的目录,它记录了 App 的名称、权限、组件等的配置信息。

  • classes.dex:Android 平台上的可执行文件,它由 .java 源文件生成 .class 文件,.class 进一步生成 .dex 文件,如果做了分包处理,可能会有 classes1、classes2 …

  • resources.arsc:资源索引表,包含编译后的二进制文件,每当在 res 文件夹下放一个文件时,aapt 就会自动生成对应的 id 并保存在 .R 文件中,但 .R 文件仅仅只是保证编译程序不会报错,实际上在应用运行时,系统会根据 ID 寻找对应的资源路径,而 resources.arsc 文件就是用来记录这些 ID 和 资源文件位置对应关系 的文件。

    Android 资源管理框架又是如何快速定位到最匹配资源的?

    1. 资源 ID 文件 R.java:赋予每一个非 assets 资源一个 ID 值,这些 ID 值以常量的形式定义在 R.java 文件中。
    2. 资源索引表 resources.arsc:用来描述那些具有 ID 值的资源的配置信息。
  • res:资源文件夹,res/animatorres/animres/colorres/drawable(非Bitmap文件,即非.png、.9.png、.jpg、.gif文件)res/layoutres/menures/valuesres/xml 的资源文件均会从文本格式的 XML 文件编译成二进制格式的XML 文件,resources.arsc 包含的二进制文件就是这些。

    为什么 XML 资源文件要从文本格式编译成二进制格式?

    1. 空间占用更小:这是由于所有 XML 元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。
    2. 解析速度更快:这是由于二进制格式的 XML 元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。
  • assets:额外建立的资源文件夹。resassets 的不同在于 res 目录下的文件会在 .R 文件中生成对应的资源 ID,而 assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口来获取。(没有则不会生成)

  • lib: 存放的是 ndk 编出来的 so 库。(没有则不会生成)

  • META-INF:签名文件夹,用于保存 App 的签名和校验信息(当生成 APK 包时,系统会对包中的所有内容做一次校验,然后将结果保存在这里。而手机在安装这一 App 时还会对内容再做一次校验,并和 META-INF 中的值进行比较,以避免 APK 被恶意篡改。),里面存放三个主要文件,有两个是对资源文件做的 SHA1 hash 处理,一个是签名和公钥证书,把这三个文件删了就是一个未签名的 APK

    • MANIFEST.MF:每一个资源文件都有一个对应的 SHA1-Digest 签名。(SHA1 或者 SHA256)

    • CERT.SF: 开头的 SHA1-Digest-ManifestMANIFEST.MF 文件的 SHA1 经过 base64 编码的结果,之后的内容为 MANIFEST.MF 文件中的每项再次 SHA1 经过 base64 编码后的值。(SHA1 或者 SHA256)

    • CERT.RSA:其中包含了公钥、加密算法等信息。首先,对前一步生成的 CERT.SF 使用了 SHA1生成了数字摘要并使用了 RSA 加密,接着,利用了开发者私钥进行签名。然后,在安装时使用公钥解密。最后,将其与未加密的摘要信息(MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被修改。(SHA1 或者 SHA256)

      公钥加密,私钥解密的过程,称为 加密

      私钥加密,公钥解密的过程,称为 签名

编译打包过程

首先查看一下官方的编译打包流程图

详细打包图

通过上图可将打包分为 7 个步骤

  1. AAPT(Asset Packaging Tool):资源文件(包括 AndroidManifest.xml、布局文件、各种 xml 资源等等)将被 AAPTAndroid Gradle Plugin 3.0.0 及之后使用 AAPT2 替代了 AAPT)处理为最终的 resources.arsc,并生成 R.java 文件以保证源码编写时可以方便地访问到这些资源。
  2. AIDL(Android Interface Description Language):.aidl 文件通过 AIDL 工具转换成编译器能够处理的 Java 接口文件
  3. Java Compiler:编译 R.javaJava 接口文件、Java 源文件,最终它们会统一被编译成 .class 文件。
  4. dex:.class 文件通过 dex 工具将它们转化为 Dalvik 所能识别的 .dex 文件。
  5. ApkBuilder:将生成的 .dex 文件、lib 文件、资源文件等,通过 apkbuilder 生成未签名的 .apk 文件
  6. Jarsigner:通过签名工具 Jarsigner 或者其它签名工具对 APK 进行签名得到签名后的 APK
  7. zipalign:ZipAlign 工具进行对齐处理,以提高程序的加载和运行速度。(不进行对齐处理是不能发布到 Google Market 的)

具体命令请看 Android应用程序(APK)的编译打包过程

更加详细的旧版打包图

签名

什么是签名?

如果把这个问题放在生活中,签名的意思就很好理解了,就是在某处写下自己名字,作为自己特殊的标识,当别人开到这个签名时,就知道这是和你有关,而不是其他人。

Android 开发中也是一样的道理,通过数字签名来标识作者和应用程序之间的信任关系,保证 APK 不被他人篡改,用私钥根据 APK 内容在 APK 中写入一个 指纹 ,如果 APK 中有任何修改,都会导致这个指纹无效,Android 系统在安装是进行签名校验就会不通过。

Android 系统要求每一个 Android 应用程序必须要经过数字签名才能够安装到系统中,也就是说如果一个 Android 应用程序没有经过数字签名,是没有办法安装到系统中的,在日常开发中 Android Studio 会给一个默认的 debug.keystore 文件,专门用于日常开发使用,具体目录是 C:\User\<用户名>\.android\debug.keystore ,所以 Android Studio 在编译时就会用这个默认的 debug.keystore 进行签名打包,就可以安装啦。

如何有效地做签名校验?

常见的签名校验方法如下,使用了 AndroidUtilCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean doNormalSignCheck() {
String trueSignMD5 = "95:21:D9:10:2B:C6:56:1F:4D:93:25:17:71:65:14:8A";
String appSignatureMD5 = AppUtils.getAppSignatureMD5();
return trueSignMD5.equals(appSignatureMD5);
}
// ...
public static Signature[] getAppSignature(final String packageName) {
if (UtilsBridge.isSpace(packageName)) return null;
try {
PackageManager pm = Utils.getApp().getPackageManager();
@SuppressLint("PackageManagerGetSignatures")
PackageInfo pi = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
return pi == null ? null : pi.signatures;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return null;
}
}

系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManagergetPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息。

然后我们使用「M* 管理器」,破解验证!(由于这个功能是收费的!让很多小伙伴望而却步,包括我,但是我不服输,于是下面的图来自于 Android APK:为何你的应用老是被破解,该如何有效地做签名校验?

明明已经提示签名不一致了,但是签名校验还是通过了,我是自己将反编译后的代码写入了自己的程序中,看看有啥不一样

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
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class HookApplication extends Application implements InvocationHandler {
private static final int GET_SIGNATURES = 64;
private String appPkgName = BuildConfig.FLAVOR;
private Object base;
private byte[][] sign;

private void hook(Context context) {
try {
DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(Base64.decode("AQAAAskwggLFMIIBraADAgECAgQncO4WMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCGRlbW9k\nZW1vMB4XDTIwMDExOTEwNTA1MloXDTQ1MDExMjEwNTA1MlowEzERMA8GA1UEAxMIZGVtb2RlbW8w\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCJUC42nO6yvO0hG5SVceKgRPjHm34VMBEi\nN/8WAYA2sdXJ8JvpT2VV0aBVat+KqKwXSgxLxMs9+HtBCEohjQbrRa5HqUTrVmgFLGTLMBCBSsAo\nflol3wVHhdxYZRjlq9mki5bjTPawBCFMgPPH0rTC7c93m6u0PoveN+Nac0SKv3i3LsHxu6YJvbqp\nK1EH5gxoN9aDTz3WGqg+Kmti7VYUlgyA573ZjULWfjblN1r1megtKcPUPxkXqR4zkWAtZt4ddMKE\nAwDcc6QAE3JfvsJtcTnMpMoqmlpy/z08XyP01WMnOqREvsyMuWVVlGXDgaWBXSxIGs3QgOU0xbWM\n9SGpAgMBAAGjITAfMB0GA1UdDgQWBBR38YijC32IoCPUrwSx1LtpKpqXNjANBgkqhkiG9w0BAQsF\nAAOCAQEARlRfoZsS7ckIefGK4jbF+sJD5ltX4SU8iLIqR4+qAWqlWNSAj0/JGEWw7lwyN3iYfHCr\nWBXksPTyJ0KrOza5OYpdLqxSUrzHOzoiS/zz7h9+PgwXABFj6Xa9SJeAV+svdKEmaQw/juYB9ofS\njAXHgJtoL5eBZdqVA2s5mVTWazcHC5Sqh60xn3e3d9VNhAmWpIZEbPWHuVJRO1vigJe6mKxpQ5Y+\nmfYJaC99NAFqZU/Ngb7MecisJ8wuTGhA59ztZuE7c4d1uLPGoZBISfvih+ZedFvbzf6jGOC7CpqI\nb79SoBuKi/Yebs+557NdedQWtiHkMgFE5QKo1ut997J4qQ==\n", 0)));
byte[][] bArr = new byte[(dataInputStream.read() & 255)][];
for (int i = 0; i < bArr.length; i++) {
bArr[i] = new byte[dataInputStream.readInt()];
dataInputStream.readFully(bArr[i]);
}
Class<?> cls = Class.forName("android.app.ActivityThread");
Object invoke = cls.getDeclaredMethod("currentActivityThread", new Class[0]).invoke((Object) null, new Object[0]);
Field declaredField = cls.getDeclaredField("sPackageManager");
declaredField.setAccessible(true);
Object obj = declaredField.get(invoke);
Class<?> cls2 = Class.forName("android.content.pm.IPackageManager");
this.base = obj;
this.sign = bArr;
this.appPkgName = context.getPackageName();
Object newProxyInstance = Proxy.newProxyInstance(cls2.getClassLoader(), new Class[]{cls2}, this);
declaredField.set(invoke, newProxyInstance);
PackageManager packageManager = context.getPackageManager();
Field declaredField2 = packageManager.getClass().getDeclaredField("mPM");
declaredField2.setAccessible(true);
declaredField2.set(packageManager, newProxyInstance);
System.out.println("PmsHook success.");
Log.e("Sign","PmsHook success.");
} catch (Exception e) {
System.err.println("PmsHook failed.");
Log.e("Sign","PmsHook failed.");
e.printStackTrace();
}
}

/* access modifiers changed from: protected */
public void attachBaseContext(Context context) {
hook(context);
super.attachBaseContext(context);
}

public Object invoke(Object obj, Method method, Object[] objArr) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
String str = (String) objArr[0];
if (((Integer) objArr[1] & 64) != 0 && this.appPkgName.equals(str)) {
PackageInfo packageInfo = (PackageInfo) method.invoke(this.base, objArr);
packageInfo.signatures = new Signature[this.sign.length];
for (int i = 0; i < packageInfo.signatures.length; i++) {
packageInfo.signatures[i] = new Signature(this.sign[i]);
}
return packageInfo;
}
}
return method.invoke(this.base, objArr);
}
}

继承自 Application,重写了 attachBaseContext 来调用 hook(context) ,在里面做了 IPackageManager 的动态代理,实现在调用 getPackageInfo 方法的时候,修改 signatures[] 为在破解之前计算好的数值( DataInputStream 里的值为破解时动态生成的,最终输入 signatures 的就是 95:21:D9:10:2B:C6:56:1F:4D:93:25:17:71:65:14:8A)。

Android APK:为何你的应用老是被破解,该如何有效地做签名校验?

Android中如何进行签名校验和完整性校验

Android签名验证原理解析