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),
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)