今天来写一下 app 的签名与加固的原理。
签名的原理
1. release apk 的签名文件
可以看到,签名打包好的 apk 中有一个 META-INF 目录,里面包括三个重要的文件:
- MANIFEST.MF
- CERT.SF
- CERT.RSA
来简单看下这三个文件的内容。
MANIFEST.MF:
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 2.3.0Name: res/layout/design_text_input_password_icon.xml
SHA1-Digest: YoJd6IrT+4Fi8q5etiV4CGfrcMg=Name: res/drawable-xxhdpi-v4/abc_ic_star_half_black_16dp.png
SHA1-Digest: EikVyBT5I7pmbJO2k8qF0V5hUc0=Name: solidity/ens/build/AbstractENS.bin
SHA1-Digest: 2jmj7l5rSw0yVb/vlWAYkK/YBwk=Name: res/drawable/bg_top_bar_light_theme.xml
SHA1-Digest: G1WVIvrL6vVO7cHlz5sOJM2Yw9g=Name: res/layout/activity_transaction_send.xml
SHA1-Digest: WrZZ/cTwgm92KS+iW82elSy2ets=Name: res/drawable/bg_solid_selector_light_theme.xml
SHA1-Digest: CRQNmUrjsUhbW5g0TaJQTTTg49s=Name: res/drawable/notification_bg_low.xml
SHA1-Digest: T3aVUj+I2HyqpDuAMmdpIkSA/yY=Name: res/drawable-xhdpi-v4/abc_ic_star_black_48dp.png
SHA1-Digest: pUh1yzyRb6c3D0mL7cI4wKExeTI=Name: res/layout/activity_about_us.xml
SHA1-Digest: j58VkJ7ytUJJnCLv/u8bLBZSHLc=Name: res/drawable/btn_security_keyboard_delete_selector_light_theme.xml
SHA1-Digest: 2pcdMLjyn5er5D8lZSP2LE/dLFI=Name: res/drawable/abc_list_selector_background_transition_holo_light.xml
SHA1-Digest: AdlBQF7mCKx3VlJQ4HaRjQeNXyo=Name: res/drawable-xxhdpi-v4/ic_nav_wallet_switcher.png
SHA1-Digest: g8DLztAbX8rqkub9B0fD6rwsces=Name: assets/ShareSDK.xml
SHA1-Digest: spQnpYXJlxaiSoFJKoxJuqRGow0=Name: res/color/abc_primary_text_disable_only_material_dark.xml
SHA1-Digest: 4t4rkEo1veLwAALxNPcESWEUDcg=Name: META-INF/rxjava.properties
SHA1-Digest: +hrVyVtjCoOVFh3dKfYglbzj5S8=Name: res/drawable-xxhdpi-v4/ic_nav_delete.png
SHA1-Digest: mOta3HgN3CGFfmJ5Yrp0F7Ggv5E=Name: res/mipmap-hdpi-v4/ic_launcher.png
SHA1-Digest: 6r5KrzZ9OlIwT0RuvqKZoTI3DRI=Name: res/drawable-xhdpi-v4/ic_qrcode_scan_line.png
SHA1-Digest: 1hGLNSq7gfO1cX0Nbd59GOPwh+M=// ...
MANIFEST.MF 就是逐一遍历 apk 里面的所有条目,如果是目录就跳过,如果是一个文件,就用 SHA1 或者 SHA256 消息摘要算法提取出该文件的摘要然后进行 BASE64 编码后,作为 "SHA1-Digest" 属性的值写入到 MANIFEST.MF 文件中的一个块中。该块有一个 "Name" 属性,其值就是该文件在 apk 包中的路径。
CERT.SF:
Signature-Version: 1.0
X-Android-APK-Signed: 2
SHA1-Digest-Manifest: 3PRb1OObhDRdv3ZpZ5FBNQzRGtw=
Created-By: 1.0 (Android)Name: res/layout/design_text_input_password_icon.xml
SHA1-Digest: YoJd6IrT+4Fi8q5etiV4CGfrcMg=Name: res/drawable-xxhdpi-v4/abc_ic_star_half_black_16dp.png
SHA1-Digest: EikVyBT5I7pmbJO2k8qF0V5hUc0=Name: solidity/ens/build/AbstractENS.bin
SHA1-Digest: 2jmj7l5rSw0yVb/vlWAYkK/YBwk=Name: res/drawable/bg_top_bar_light_theme.xml
SHA1-Digest: G1WVIvrL6vVO7cHlz5sOJM2Yw9g=// ...
计算这个 MANIFEST.MF 文件的整体 SHA1 值,再经过 BASE64 编码后,记录在 CERT.SF 主属性块 (在文件头上) 的 "SHA1-Digest-Manifest" 属性值下。
然后,再逐条计算 MANIFEST.MF 文件中每一个块的 SHA1,并经过 BASE64 编码后,记录在 CERT.SF 中的同名块中,属性的名字是 "SHA1-Digest"。
CERT.RSA:
3082 0354 0609 2a86 4886 f70d 0107 02a0
8203 4530 8203 4102 0101 310b 3009 0605
2b0e 0302 1a05 0030 0b06 092a 8648 86f7
0d01 0701 a082 021f 3082 021b 3082 0184
a003 0201 0202 0457 5abd 4d30 0d06 092a
8648 86f7 0d01 0105 0500 3051 3110 300e
0603 5504 0713 0762 6569 6a69 6e67 3113
3011 0603 5504 0a13 0a73 7569 7368 6974
6563 6831 1330 1106 0355 040b 130a 7375
6973 6869 7465 6368 3113 3011 0603 5504
0313 0a73 7569 7368 6974 6563 6830 2017
0d31 3630 3631 3031 3331 3435 335a 180f
3330 3135 3130 3132 3133 3134 3533 5a30
// ...
把之前生成的 CERT.SF 文件,用私钥计算出签名,然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA 是一个满足 PKCS7 格式的文件,可以通过 openssl 工具来查看签名证书的信息。
2. 签名流程
- 对 apk 中的每个文件做一次算法 (数据摘要 + BASE64 编码),保存到 MANIFEST.MF 文件中。
- 对 MANIFEST.MF 整个文件做一次算法 (数据摘要 + BASE64 编码),存放到 CERT.SF 文件的头属性中,再对 MANIFEST.MF文件中各个属性块做一次算法 (数据摘要+Base64编码),存到到一个属性块中。
- 用私钥对 CERT.SF 文件做签名,然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。
3. 验证流程
Android 平台上所有应用程序安装都是由 PackageManangerService,Android 的安装流程非常复杂,与签名验证相关的步骤位于 下面两个类,有兴趣的可以看看:
frameworks/base/core/java/android/content/pm/PackageManagerService.java
安装应用时 PackageManagerService 会对 apk 进行签名检查,具体分为以下几步:
- 1. 读取 CERT.RSA (证书信息包括公钥)、MANIFEST.MF、CERT.SF。
- 2. 使用获取的公钥对 CERT.SF 解密,将解密结果和 MANIFEST.MF 进行比较,如果相同说明证书有效、MANIFEST.MF 未被更改。
- 3. 对 apk 中所有文件内容分别进行消息摘要计算,将结果的 BASE64 编码和 MANIFEST.MF 里的相应内容进行比较,全部相同则 apk 的内容未被更改。
如何判断证书是否有效?
因为签名的时候是使用私钥对 MANIFEST.MF 进行加密保存在 CERT.SF 中,之后只需要用证书中的公钥对 CERT.SF 进行解密,将结果和 MANIFEST.MF 进行比较即可。注意,在证书正确的情况下如果更改的 apk 里面文件内容,此时以上判断还是不会通过。因为 MANIFEST.MF 保存的是 apk 里面所有文件的哈希值,只要改变了 apk 里文件内容,哈希值就会变化。
如何判断 apk 是否被更改?
在签名保证 MANIFEST.MF 有效的前提下,只要对当前 apk 所有文件的内容的哈希值的 BASE64 编码和 MANIFEST.MF 相应文件存的值进行比较即可。
如何防范被重新签名?
通过反编译工具 (apktool) 可以轻易的对 apk 进行重新签名,针对这种情况可以通过以下几个方法加强被成功重新签名的难度:
- 对 apk 进行加壳处理,增加反编译难度。
- 程序中对签名自行验证,可配合服务端进行。
不过没有什么是绝对安全的,只是破解的成本和收益的博弈。比如方法一可以通过出壳进行破解,只是增加了出壳的成本。方法二可以在反编译的时候通过更改相关 smali 代码绕过签名验证的地方,只是增加了反编译成本。
加固的原理
1. 加固原理解析
在该过程中涉及到3个对象,分别如下:
源程序也就是我们的要加固的对象,这里面主要修改的是原 apk 文件中的 classes.dex 文件和 AndroidManifest.xml 文件。
壳程序主要用于解密经过加密了的 dex 文件,并加载解密后的原 dex 文件,并正常启动原程序。
加密程序主要是对原 dex 文件进行加密,加密算法可以是简单的异或操作、反转、RC4、DES、RSA 等加密算法。
加固过程可以分为如下4个阶段:
- 加密阶段。
- 合成新的 dex 文件。
- 修改原 apk 文件并重打包签名。
- 运行壳程序加载原 dex 文件。
下面来分别分析这4个阶段。
2. 加密阶段
加密阶段主要是讲把原 apk 文件中提取出来的 classes.dex 文件通过加密程序进行加密。加密的时候如果使用 DES 对称加密算法,则需要注意处理好密钥的问题。同样的,如果采用非对称加密,也同样存在公钥保存的问题。
顺便来说下 dex 的结构:
这就是 dex 的文件格式了,那么 Header 中存储了什么内容呢?
3. 合成新的 dex 文件
这一阶段主要是讲上一步生成的加密的 dex 文件和壳 dex 文件合并,将加密的 dex 文件追加在壳 dex 文件后面,并在文件末尾追加加密 dex 文件的大小数值。
在壳程序里面,有个重要的类:ProxyApplication 类,该类继承 Application 类,也是应用程序最先运行的类。所以,我们就是在这个类里面,在原程序运行之前,进行一些解密 dex 文件和加载原 dex 文件的操作。
4. 修改原 apk 文件并重打包签名
在这一阶段,我们首先将 apk 解压,修复 classes.dex 和 AndroidManifest.xml 这两个文件,其他文件和文件加都不需要改动。首先,我们把解压后 apk 目录下原来的 classes.dex 文件替换成合成的新的 classes.dex 文件。然后,由于我们程序运行的时候,首先加载的其实是壳程序里的 ProxyApplication 类。所以,我们需要修改 AndroidManifest.xml 文件,指定 application 为ProxyApplication,这样才能正常找到识别 ProxyApplication 类并运行壳程序。
5. 运行壳程序加载原 dex 文件
Dalvik/ART 虚拟机会加载我们经过修改的新的 classes.dex 文件,并最先运行 ProxyApplication 类。在这个类里面,有2个关键的方法:attachBaseContext() 和 onCreate() 方法。ProxyApplication 先运行 attachBaseContext() 再运行 onCreate() 方法。
在 attachBaseContext() 方法里,主要做两个工作:
- 读取 classes.dex 文件末尾记录加密 dex 文件大小的数值,则加密 dex 文件在新 classes.dex 文件中的位置为:len (新classes.dex 文件) – len (加密 dex 文件大小)。然后将加密的 dex 文件读取出来,加密并保存到资源目录下。
- 然后使用自定义的 DexClassLoader 加载解密后的原 dex 文件。
在 onCreate() 方法中,主要做两个工作:
- 通过反射修改 ActivityThread 类,并将 Application 指向原 dex 文件中的 Application。
- 创建原 Application 对象,并调用原 Application 的 onCreate() 方法启动原程序。
6. 加固的优缺点
优点:
- 保护自己核心代码算法,提高破解/盗版/二次打包的难度。
- 缓解代码注入/动态调试/内存注入攻击。
缺点:
- 影响兼容性。
- 影响程序运行效率。
- 部分流氓、病毒也会使用加壳技术来保护自己。
- 部分应用市场会拒绝加壳后的应用上架。