1. Android 的签名保护机制到底是什么?
Android 系统禁止更新安装签名不一致的 Apk,如果我们修改了 Apk 又用别的签名文件签名,肯定是不一致的。
我们从签名工具 autoSign 分析,看一下 sign.bat 文件内容:
-----------------------------------
@ECHO OFF
Echo Auto-sign Created By Dave Da illest 1
Echo Update.zip is now being signed and will be renamed to update_signed.zip
java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
Echo Signing Complete
Pause
EXIT
-----------------------------------
看一下 java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk 这行的意义:
以testkey.x509.pem 这个公钥文件和 testkey.pk8 这个私钥文件对 update.apk 进行签名,签名后保存为 update_signed.apk。
我们可以看到签名前和签名后比较,签名后的文件中多了一个文件夹“META-INF”,里面有三个文件 MANIFEST.MF 、 CERT.SF 、 CERT.RSA。
我们通过 jd-gui 工具打开 signapk.jar,找到 main 函数,通过这个函数跟踪代码
1.addDigestsToManifest 这个函数,遍历 Apk 中所有文件,对非文件夹非签名文件的文件逐个生成 SHA1 数字签名信息,再 base64 编码。
然后再写入 MANIFEST.MF 文件中,生成文件如下:
-----------------------------------
Manifest-Version: 1.0
Created-By: 1.0 (Android)
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=
Name: AndroidManifest.xml
SHA1-Digest: NaPhUBH5WO7uGk/CfRu/SHsCvW0=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=
Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=
Name: res/layout/activity_main.xml
SHA1-Digest: kxwMyILwF2K+n9ziNhcQqcCGWIU=
Name: resources.arsc
SHA1-Digest: q7Ystu6WoSWih53RGKXtE3LeTdc=
Name: classes.dex
SHA1-Digest: Ao1WOs5PXMxsWTDsjSijS2tfnHo=
Name: res/drawable-xxhdpi/ic_launcher.png
SHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=
-----------------------------------
SHA1 生成的摘要信息,如果你修改了某个文件,Apk 安装校验时,取到的该文件的摘要与 MANIFEST.MF 中对应的摘要不同,则安装不成功。
2.接下来对之前生成的 manifest 使用 SHA1withRSA 算法, 用私钥签名,writeSignatureFile 这个函数,最后生成 CERT.SF 文件,如下:
-----------------------------------
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: pNZ9UXN9GMqTgqAwKD6uEN6aD34=
Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=
Name: AndroidManifest.xml
SHA1-Digest: oRzzLkwuvxC78suvJcAEvTqcjSA=
Name: res/drawable-mdpi/ic_launcher.png
SHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=
Name: res/drawable-hdpi/ic_launcher.png
SHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=
Name: res/layout/activity_main.xml
SHA1-Digest: Yr3img6SqiKB+1kwcg/Fga2fwcc=
Name: resources.arsc
SHA1-Digest: j1g8I4fI9dM9hAFKEtS9dHsqo5E=
Name: classes.dex
SHA1-Digest: Sci9MmGXNGnZ1d04rCrEEV7MWn4=
Name: res/drawable-xxhdpi/ic_launcher.png
SHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=
-----------------------------------
用私钥通过 RSA 算法对 manifest 里的摘要信息进行加密,安装的时候只能通过公钥解密,解密之后才能获得正确的摘要,再对比。
3.最后就是如何生成 CERT.RSA,打开这个文件看到的是乱码,说明整个文件都被编码加密了,而且这个文件和公钥有关,从源码中看出他是通过 PKCS7 将整个文件加密了。
总结:1.签名只是对完整性和签名发布机构的校验机制 2.不能阻止 Apk 被修改,只是签名无法保持一致 3.不同私钥对应着不同的公钥,实质上不同的公钥就代表了不同的签名。
2. Android 系统如何获取签名
我们从获取下面一段代码开始分析:
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager ;
}
IPackageManager pm = ActivityThread.getPackageManager ();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null ;
}
可以看到 ActivityThread 中方法 getPackageManager 获取 IPackageManager。
继续看 ActivityThread 的代码:
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
//Slog.v("PackageManager", "returning cur default = " + sPackageManager);
return sPackageManager ;
}
IBinder b = ServiceManager.getService("package");
//Slog.v("PackageManager", "default service binder = " + b);
sPackageManager = IPackageManager.Stub.asInterface(b);
//Slog.v("PackageManager", "default service = " + sPackageManager);
return sPackageManager;
}
看源码知道是通过 ServiceManager.getService(“package”);获取 PackageManagerService 并得到 IBinder对象,然后通过 asInterface 函数取得接口类 IPackageManager 实例。
然后作为参数构造 ApplicationPackageManager,再看 ApplicationPackageManager 这个类里的方法:
@Override
public PackageInfo getPackageInfo(String packageName, int flags)
throws NameNotFoundException {
try {
PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId());
if (pi != null) {
return pi;
}
} catch (RemoteException e) {
throw new RuntimeException( "Package manager has died" , e);
}
throw new NameNotFoundException(packageName);
}
这里的mPM就是上面构造函数传进来的 IPackageManager,就可以调用 PackageManagerService 的方法了。
下图是 PackagerManager 静态类结构图:
ok,看完上图的结构图,继续跟代码 ,看到这里是 mPM 继续调用的getPackageInfo(代理模式),通过进程通信到 PackageManagerService 中执行响应操作:
@Override
public PackageInfo getPackageInfo(String packageName, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
enforceCrossUserPermission(Binder.getCallingUid (), userId, false, "get package info");
// reader
synchronized (mPackages) {
PackageParser.Package p = mPackages.get(packageName);
if (DEBUG_PACKAGE_INFO)
Log.v (TAG, "getPackageInfo " + packageName + ": " + p);
if (p != null) {
return generatePackageInfo(p, flags, userId);
}
if((flags & PackageManager. GET_UNINSTALLED_PACKAGES ) != 0) {
return generatePackageInfoFromSettingsLPw(packageName, flags, userId);
}
}
return null ;
}
这里 mPackages 是 hashMap,其调用put方法的时机是在 scanPackageLI 方法中,而 scanPackageLI 的调用地方是在程序安装和替换函数中,还有就是 scanDirLi 中,代码略。
scanDirLi 是用来扫描一些系统目录的的,在 PackageManagerService 的构造函数中调用的:
File dataDir = Environment. getDataDirectory();
mAppDataDir = new File(dataDir, "data");
mAppInstallDir = new File(dataDir, "app");
mAppLibInstallDir = new File(dataDir, "app-lib" );
mAsecInternalPath = new File(dataDir, "app-asec" ).getPath();
mUserAppDataDir = new File(dataDir, "user");
mDrmAppPrivateInstallDir = new File(dataDir, "app-private" );
上面是扫描的位置↑↑↑↑↑↑↑
PackageManagerService 处理各种应用的安装、卸载、管理等工作,开机时由 systemServer 启动此服务。就是说之前安装过的应用或者系统应用信息都会在开机扫描过程中存到 mPackages 这个 hashMap 中。开机后用户的安装操作也会同样存到这个 hashMap 里面。
继续看 getPackageInfo,调用的 generatePackageInfo , 里面调用的是 PackageParser 中的 generatePackageInfo,继续跟。
这个函数的代码比较长,只贴出部分关键代码:
if ((flags&PackageManager. GET_SIGNATURES ) != 0) {
int N = (p.mSignatures != null) ? p.mSignatures.length : 0;
if (N > 0) {
pi.signatures = new Signature[N];
System.arraycopy (p.mSignatures, 0, pi. signatures, 0 , N);
}
}
return pi;
这段代码之前主要是做一些复制的操作,就是 new 一个 PackageInfo,然后把 PackageParser.Package 对象中的一些内容复制到这个 PackageInfo 中。
从这段代码可以看出来,最终得到的 signatures 信息就是中 p(PackageParser.Package )中的成员 mSignatures 中得来的。
好了,现在就是看这个PackageParser.Package是从哪来的了,通过跟踪代码,installPackageLI 和 scanPackageLI 中的:
final PackageParser.Package pkg = pp.parsePackag (tmpPackageFile, null, mMetrics, parseFlags);
这行代码只是生成了 pkg,并没有赋值里面的 mSignatures,继续跟踪,找到函数 collectCertificatesLI,找到 pp.collectCertificates(pkg , parseFlags)。
PS:最终又进行了一系列的跟代码,找到了 JarVerifier.java 这个类的 readCertificates 这个就是用来读取.RSA 文件的,最终我们看到的里面的代码是通过 loadCertificates 取得的 certs 赋值给了 pkg.mSignatures。
至此,获取签名的所有逻辑就算是简单的过一遍了,以下是简略流程图:(发现 ppt 画图比 Windows 画图工具好用多了,哈哈)
OK,绕了这么久我们终于找到源头了,获取签名就是在 META-INF 中寻找,并解析。试想一下,如果我们修改了这个函数,让他解析原来正版的 META-INF 中的 CERT.RSA 文件,这样就可以伪造为真正的签名了。
那么我们就想到了 HOOK,(很多人都是从看雪论坛上找到的一篇文章看到的http://bbs.pediy.com/showthread.php?t=186054hook 的原来简单来说就是,找到原函数和新函数的指针位置,然后兑换内容,将新函数替代原函数。
关于 hook 呢,网上也有很多框架可以使用,比如:
1.Cydia substrate :http://www.cydiasubstrate.com/
2.Xposed :http://repo.xposed.info/
网上也有很多教程可以看看。
乌云(wooyun)上有一篇很有意思的教程,就是利用 hook 进行微信运动作弊,原帖地址:http://drops.wooyun.org/tips/8416
下图就是我用了上面的方法产生的效果,还差点被微信部门的人请去喝茶。
这里用的是 Xposed 框架,原理就是 hook 了手机的计步传感器的队列函数,然后把步数的返回值每步乘1000返回,前提是,你的手机硬件本身有计步传感器功能,这里微信运动里面列出了支持的手机列表:https://kf.qq.com/touch/sappfaq/151013AvyyeQ151013r63qmq.html?platform=15好像最高就是98800了,可能是微信做了步数限制吧。
我这里用的是小米4联通版,发现虽然是1000基数的加,但是好像隔了很久才变化,估计又是 MIUI 做了一些省电策略,传感器的采集做了对齐吧?
Xposed 框架,很多玩机爱好者,会拿它修改一些主题,字体之类的,或者系统界面,定制自己想要的系统插件等等。然而,也有缺点,需要手机root,而且这个框架,还有可能让手机变砖,还有的系统可能对这个框架支持的不好,或者不支持。XDA 论坛里面也有很多大神把 Xposed 对某些机型做了适配,大神一般都是说,如果手机变砖他们不负责,哈哈。
正式由于这些框架的诸多不便,root 等等的问题,于是就有了一些非 root 的 hook 的黑科技,比如阿里巴巴的开源框架 Dexposed(https://github.com/alibaba/dexposed)其也是根据 Xposed 框架修改而来的,不过看 github 上他们也好久没更新了。
也有像其他个人写的和这种比较类似的框架,这里就不介绍了。但是这类框架的缺点就是,只能在该进程下hook,不能全局 hook,即只对这个进程的应用起作用,不能对另一个应用起作用,优点是可以 hook 自定义函数也能hook 系统函数,并且不用 root 和重启。阿里用这个框架来打在线热补丁。
那对于 App 内部签名校验的就不用再搜相应的代码了,直接 hook 就一步到位了,android.app.ApplicationPackageManager 这个类的 getPackageInfo 这个方法直接把正确的签名返回就好了,接下来我们就需要把hook的代码注入到某个 App 里就好了。
未完待续。。。。。。
欢迎阅读:手把手教你逆向分析 Android 程序(连载三)