首页
社区
课程
招聘
[原创]概述APK完整性保护的逆向分析及防护策略
发表于: 2021-7-16 00:44 21952

[原创]概述APK完整性保护的逆向分析及防护策略

2021-7-16 00:44
21952

APK的完整性保护在Android逆向安全中是一个常见的话题,通常指为了防二次打包,防篡改,防独立调用so等,通过验证apk包名、签名,及apk包本身(如META-INF下签名文件,MD5等)来达到防范的目的。笔者曾分析过一些大型的APK,发现即使使用了包名、签名、apk完整性验证,仍然可以通过一定手段来绕过验证。即便多年前尼古拉斯四哥的apk签名爆破,到现在也非常好用,因此本文的案例中也集成了四哥的签名爆破。
本文主要对apk完整性进行逆向及分析相应的安全策略。

本文以Android8.0源码为例,涉及代理application的机制,会分析到Android中一个application的创建,以及如何通过代理Application hook掉目标application中有关apk包的属性(LoadedApk)

本着以实用主义的原则,本文决定不以分析源码为开篇,而是以一个实际的案例(如tb无线保镖,wb等具有完整性保护的绕过原理相同),拆分其so库到自己的Android项目,绕过其包名签名检测,APK完整性校验以成功调用so。


首先将目标so拷贝出来,新应用的包名需与原来的一致,并配置好so所需环境,在application中调用四哥的PMS签名爆破。对于一般的项目来说,绝大多数的so是能调用成功的。
这里简单提供一下目标签名的获取:

但是这里我们调用返回的却是null。这说明了一个问题,我们的环境相对于so原本的环境,必然有某些地方不一致,so仍能校验出来。且不同于某些0335之类的图片签名,也不同于某些在assets目录中读取文件的so,我们的demo并未发生崩溃,也没有其他异常信息。我们已经加好了签名爆破,那么就有可能是apk的完整性问题了。

上文猜测有可能是apk的完整性保护问题。那么我们应该如何绕过呢?

我们知道一个release版本的apk是需要签名才能打包的,并且会在META-INF文件下生成三个签名文件,
MANIFEST.MF、CERT.SF、CERT.RSA。其中CERT.RSA文件以签名的方式“包含”了前两个文件,并且也会将公钥证书一同写进去。(名称解释:RSA算法为非对称加密,通常来说,以公钥加密、私钥解密称为加解密过程;以私钥加密、公钥解密称为签名、验证的过程)

而里面包含的公钥证书,我们可以通过openssl命令提取

提取出来的CERT.cer证书文件,在010中打开,可以发现其hex值正是在Android中获取的签名值(toCharsString())。

当然,我们并不确定so中是否真的去校验这个签名值,更简单的方式可能直接校验CERT.RSA
的MD5,而不用去提取证书。但是有一点我们可以确定,so中一定会通过某种方式获取到原始apk文件。

一个apk在安装成功后,会在/data/app/包名 下存有原始apk的信息,且这个路径是动态变化的。一般获取apk的完整路径,都会使用context.getPackageCodePath()这个方法来获取。因此,我们的目标就变成了如何hook掉getPackageCodePath,并转移其指向到原本的apk。
当然,比较直接的方式就是通过frida或者xposed进行hook,但是使用这两者都需要额外的成本,尤其是如果搭建模拟器服务时,会更加复杂。不过getPackageCodePath方法既然是在Java层,我们便可以利用Java厉害的“武器”反射,来手动hook掉他。

Android中context的实现类是ContextImpl,因此context通过ContextWrapper调用到ContextImpl的getPackageCodePath()方法

这里的mPackageInfo的类型是LoadedApk,getAppDir()返回的是其字段mAppDir。也就是说,如果我们能在运行时,动态替换掉LoadedApk,并把mAppDir字段设置为虚假的apk路径,那么是否就能够绕过apk的完整性验证?

这里我们可以参考插件化的思想,应用启动时先加载代理application,在创建代理application的过程中,替换为目标application,并修改其值。因此我们需要了解一个application是如何创建的。

首先,这张图很好的概括了,当一个应用启动后,Launcher进程首先会通过binder IPC的方式将请求转发到远端服务AMS中(system_server进程),接着又会通过socket,在Zygote进程中fork出我们的应用进程,也就是所谓所有的应用程序都是由zygote孵化而来,最终会通过反射调用到ActivityThread.main()方法。
图来源

因此,我们从ActivityThread.main()方法开始,作为分析application创建的入口点

在attach方法中,getService()最终会通过binder驱动在server_manager中获取已注册好的远端服务AMS的对象,并调用其(AMS)attachApplication方法。

调用到bindApplication方法中

这个方法会创建一个AppBindData对象,并通过handler的方式发送出去,这个消息将会在主线程的handleMessage里面接收,此时swich的分支是BIND_APPLICATION,接着就走到handleBindApplication并把新创建的AppBindData对象传进去

在handleBindApplication中做了最关键的事情就是makeApplication方法,这个方法是在data.info也就是LoadedApk中

makeApplication这个方法非常关键,他代表了一个application是如何创建的。
1部分首先判断当前进程的application是否为空,也就是判断当前进程的application是否已经被加载过,如果加载过,则直接返回。然后会获取此application的全限定类名,获取classloader,通过2处,创建出application,最后把创建出来的application添加进集合,并返回创建出的application,也就是3处。

来看一下2处的newApplication中做了什么?


发现最终会调用到attachBaseContext方法,也就是通常在应用层面,继承Application复写的那个方法,所以attachBaseContext被调用的时机,其实还是很早的,此时2、3处的代码还没有执行到。因此我们可以在代理application中复写attachBaseContext,在此方法内通过反射,重新调用一次makeApplication,并把新的application替换掉原有的application,并完成相应属性的替换工作。

这里我们会通过大量的反射获取成员对象、调用方法,因此下边的表格表示了我们需要反射获取的全部属性\方法,如有忘记可以上下查看。

上节提到,我们需要做的就是重新调用makeApplication创建出一个新的application,来看核心代码

完成hook工作后,我们可以验证一下,发现已经能调用成功,apk的完整性校验已经绕过!

虽然绕过检测的方式不是很复杂,简而言之其实就一句话,hook掉so中获取的apk路径,但本案例仍然是相对安全的典范,这种方式大约是笔者在2年前发现的,不过当时笔者在其他完整性保护的项目中,是hook libc中fopen或open来转移指向,通过inline hook的方式集成在单独应用中,但在此案例中并未hook成功,如果排除笔者的操作失误,那么大概率此案例对libc中的系统库有保护操作,或者以系统调用来完成读取文件路径。
其次,本案例的完整性检查是针对apk全文件的,不同于其他案例只保留apk中meta-inf文件夹下的签名文件就可以调用成功,因此需要将大概70M的apk拷贝至指定的位置,才能调用成功。

通过上文分析,无论是通过open/fopen,还是getPackageCodePath,都是需要获得apk的路径,而我们绕过检测的方式也正是基于此。那么有没有其他的方式可以动态获取apk的路径,并且不会被上述的hook绕过呢?答案是有的,就是使用pm命令。

adb shell的命令基本上都会通过binder调用到远端服务,如pm相关的命令最终会调用到PMS系统服务。我们简单看一下使用pm path之后做了哪些操作


首先获取userId,可以看到通过pm命令获取到的userId是系统用户的Id,0。

其次会调用到pms的getPackageInfo方法,在这个方法里面,最终会通过generatePackageInfo方法重新生成apk相关信息,因此我们在application中替换的值也就没有用了。
这里以Java为例,执行pm命令,可以看到pm打印的apk原始路径是没有被hook掉的。

因此,针对apk的完整性保护,可以采取双重验证,一方面通过context读取路径,另一方面通过pms服务获取路径。

本文概述了apk完整性检测的绕过方式,并增加了一种新的检测方式,不过攻防总是相对的,即便是本文提出的保护方案,可能也会有其他的绕过方式,有兴趣的读者可以继续研究。

最后总结一下独立调用so的好处,最简单的就是搭建Android服务,且不依赖于第三方hook工具,效率很高。其次调用成功可以完全了解so所处环境,作为unicorn模拟调用的环境补全。最后,独立调用可以很好的控制so加载的时机,避免了在原始环境下分析so的困境。

本文案例已上传https://github.com/SliverBullet5563/ApkIntegrityCheck(如侵删!),谢谢!

参考:

https://github.com/fourbrother/HookPmsSignature
http://gityuan.com/2017/04/02/android-application/
https://blog.csdn.net/AndrExpert/article/details/103535201
http://gityuan.com/2016/03/26/app-process-create/
https://www.jianshu.com/p/3ef82ac4bf4f
https://bbs.pediy.com/thread-250990.htm

相关源码路径
frameworks\base\core\java\android\app\ActivityThread.java
frameworks\base\core\java\android\app\LoadedApk.java
frameworks\base\core\java\android\app\ContextImpl.java
frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
frameworks\base\cmds\pm\src\com\android\commands\pm\Pm.java
frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
相关源码路径
frameworks\base\core\java\android\app\ActivityThread.java
frameworks\base\core\java\android\app\LoadedApk.java
frameworks\base\core\java\android\app\ContextImpl.java
frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java
frameworks\base\cmds\pm\src\com\android\commands\pm\Pm.java
frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
frida:
function call_signature(){
    Java.perform(function(){
        var context = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
        var packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 64);
        //.signatures[0]
        var Signatures =packageInfo.signatures.value;
        console.log("Signatures: ",Signatures.length,Signatures[0].toCharsString())
    })
}
 
Xposed:
大多数情况下xposed hook提示class not find的原因是classloader不对
XposedHelpers.findAndHookMethod("com.kwai.hotfix.loader.app.TinkerApplication",loadPackageParam.classLoader , "attachBaseContext", Context.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {    
            Context context = (Context) param.args[0];   
            String signature = context.getPackageManager().getPackageInfo(context.getPackageName(),PackageManager.GET_SIGNATURES).signatures[0].toCharsString();   
            LogUtil.d(TAG, "PackageName"+context.getPackageName()+"   signature: "+signature);   
            super.beforeHookedMethod(param);
    }
}
frida:
function call_signature(){
    Java.perform(function(){
        var context = Java.use("android.app.ActivityThread").currentApplication().getApplicationContext();
        var packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 64);
        //.signatures[0]
        var Signatures =packageInfo.signatures.value;
        console.log("Signatures: ",Signatures.length,Signatures[0].toCharsString())
    })
}
 
Xposed:
大多数情况下xposed hook提示class not find的原因是classloader不对
XposedHelpers.findAndHookMethod("com.kwai.hotfix.loader.app.TinkerApplication",loadPackageParam.classLoader , "attachBaseContext", Context.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {    
            Context context = (Context) param.args[0];   
            String signature = context.getPackageManager().getPackageInfo(context.getPackageName(),PackageManager.GET_SIGNATURES).signatures[0].toCharsString();   
            LogUtil.d(TAG, "PackageName"+context.getPackageName()+"   signature: "+signature);   
            super.beforeHookedMethod(param);
    }
}
 
 
openssl pkcs7 -inform DER -in CERT.RSA -print_certs | openssl x509 -outform DER -out CERT.cer
openssl pkcs7 -inform DER -in CERT.RSA -print_certs | openssl x509 -outform DER -out CERT.cer
 
//ContextImpl.java
@Override
    public String getPackageCodePath() {
        if (mPackageInfo != null) {
            return mPackageInfo.getAppDir();
        }
        throw new RuntimeException("Not supported in system context");
    }
//ContextImpl.java
@Override
    public String getPackageCodePath() {
        if (mPackageInfo != null) {
            return mPackageInfo.getAppDir();
        }
        throw new RuntimeException("Not supported in system context");
    }
 
 
//android.app.ActivityThread
public static void main(String[] args) {
    ......
    thread.attach(false);
}
 
private void attach(boolean system) {
    ......
    if (!system) {
        final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
    }
}
//android.app.ActivityThread
public static void main(String[] args) {
    ......
    thread.attach(false);
}
 
private void attach(boolean system) {
    ......
    if (!system) {
        final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }
    }
}
//ActivityManagerService.java
@Override
    public final void attachApplication(IApplicationThread thread) {
        synchronized (this) {
            int callingPid = Binder.getCallingPid();
            final long origId = Binder.clearCallingIdentity();
            attachApplicationLocked(thread, callingPid);
            Binder.restoreCallingIdentity(origId);
        }
    }
private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
        ......
        if (app.instr != null) {
                thread.bindApplication(processName, appInfo, providers,
                        app.instr.mClass,
                        profilerInfo, app.instr.mArguments,
                        app.instr.mWatcher,
                        app.instr.mUiAutomationConnection, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.persistent,
                        new Configuration(getGlobalConfiguration()), app.compat,
                        getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial);
            }
 }
//ActivityManagerService.java
@Override
    public final void attachApplication(IApplicationThread thread) {
        synchronized (this) {
            int callingPid = Binder.getCallingPid();
            final long origId = Binder.clearCallingIdentity();
            attachApplicationLocked(thread, callingPid);
            Binder.restoreCallingIdentity(origId);
        }
    }
private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid) {
        ......
        if (app.instr != null) {
                thread.bindApplication(processName, appInfo, providers,
                        app.instr.mClass,
                        profilerInfo, app.instr.mArguments,
                        app.instr.mWatcher,
                        app.instr.mUiAutomationConnection, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.persistent,
                        new Configuration(getGlobalConfiguration()), app.compat,
                        getCommonServicesLocked(app.isolated),

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 11
支持
分享
最新回复 (6)
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
支持一下
2021-7-18 10:56
0
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
支持支持
2021-7-18 14:02
0
雪    币: 336
活跃值: (767)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
支持,mark一下
2021-7-20 13:23
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
mark一下
2023-4-24 17:26
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
再次支持 十分精彩
2023-4-24 20:25
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
为什么非要替换application?我测试过了发现直接在原有的application的基础上替换地址也是可以的。
2024-4-17 18:16
0
游客
登录 | 注册 方可回帖
返回
//