Google Integrity 一直以绝对可信闻名,基于此大量的银行、金融类APP都高度依赖 Integrity 服务来验证客户端的请求是否可信。但它真的完全可靠吗?怀揣着这个疑问,我深度分析了 Integrity Token 的生成过程与 Key Attestation 的原理。得出的结论是不可靠。
在官方的文档中,想要获取一个 Token 有两种请求方式。分别为传统与标准请求。
传统请求的示例代码为:
标准请求的示例代码为:
这两种请求方式的区别仅仅是标准请求使用了缓存,高频调用的响应速度更快。从业务安全的角度来说是完全一致的。因此,我们以传统请求 API 为例,来
分析完整的生成过程。
首先我们通过 Maven 拿到 Integrity 的 jar 包。然后使用 Jadx 打开定位到 requestIntegrityToken 函数:

发起请求时携带了一个 IntegrityTokenRequest 参数,里面主要包含了获取 Token 的必要参数:nonce、cloudProjectNumber。
进入到 this.a.c 函数:

组装 Binder 跨进程调用参数:

发起调用:

最终由 IIntegrityService 做为服务端处理来自客户端的请求,至此我们正式进入 Google Play 源码分析。


由于篇幅的关系,我这里不展开 c 以及一系列子函数的调用。我只大概讲一下它的流程:
在收集的所有信息中,最为人津津乐道的便是 Tee 签名的证书链。这是由绝对安全的硬件空间生成的设备描述信息,按理说是绝对可信的。因为,一旦你更改了返回的证书链,你没办法重签。不重签后端就不认这个证书链,直接判定为伪造。你想要重签,就必须拿到私钥,可是私钥放到 Tee 中,不可导出不可读取。因此,形成了完美闭环。
这套完美的防御体系,看似无懈可击。但是当 Tee 私钥泄漏了之后呢?这条防御链条中最重要的一环,将土崩瓦解。
Google Play 发起 Key Attestation 的代码为:

首先指定别名、密钥生成算法、挑战值(防重放)等信息构建一个 KeyGenParameterSpec
通过 KeyPairGenerator 指定 AndroidKeyStore 为具体的处理服务
generateKeyPair 生成派生密钥对
getCertificateChain 拿到最终的认证证书链
发送给服务端验证
这些调用的背后实际上发生了什么呢?generateKeyPair 具体的流程如下:

这一步调用完以后 ,Tee 返回一个 X.509 证书链。这个证书链就是 Key Attestation 验证的核心。一般情况下至少包含三个证书:
根据官方文档描述,Leaf Certificat 附带的信息至少包含如下两项:
拿到证书链之后,便组包发给了 Google 后端进行校验。那么在后端是如何校验的呢?依赖的便是在上文中指定的签名算法:EC(secp256r1),实际上使用的算法是:ECDSA(椭圆曲线数字签名算法)secp256r1为指定的椭圆曲线参数。算法具体的签名与验证过程:

根据图中的验证过程可知:私钥只签名不参与验证,公钥只验证不参与签名。签名值与公钥分别附加在证书的 Certificate.signatureValue Certificate.TBSCertificate.subjectPublicKeyInfo 中。因为每张证书都由上张证书签名,所以 Google 拿到三张证书依次进行校验:
当校验到 Root Certificate 时,由于没有上级证书。这时拿 Root Certificate 去数据库匹配,如果这张 Root Certificate 是由厂商提交过备案的,则证明整条链条可信。接下来再继续校验 AttestationApplicationId、RootOfTrust 等信息。其中 RootOfTrust 是获得 MEETS_DEVICE_INTEGRITY 也就是二绿的关键。这里面携带了设备是否解锁的标识:deviceLocked。并且整个 RootOfTrust 状态是由 Tee 维护的,因此不具备伪造性。
但是当我们拥有一台有 Root 权限并且没有解锁 Bootloader 设备的时候呢?那么这台设备就具备就具备以下两个发起中间人攻击的条件:
设备A:Y700 - 拥有 Root 权限 - 三绿
设备B:Pixel6 - 拥有 Root 权限 - 一绿
Google Play 版本均保持一致(过 AttestationApplicationId 签名校验)
我们使用如下 Frida 脚本启动 Pixel 的 Google Play:
如下脚本启动 Y700 的 Google Play:
然后惊奇的发现:

| 设备 |
是否具有Root 权限 |
是否通过 Google Integrity 验证 |
| Y700 |
是 |
是 |
| Pixel 6 |
是 |
否 |
import com.google.android.gms.tasks.Task; ...
String nonce = ...
IntegrityManager integrityManager =
IntegrityManagerFactory.create(getApplicationContext());
Task<IntegrityTokenResponse> integrityTokenResponse =
integrityManager
.requestIntegrityToken(
IntegrityTokenRequest.builder().setNonce(nonce).build());
import com.google.android.gms.tasks.Task;
// Create an instance of a manager.
StandardIntegrityManager standardIntegrityManager =
IntegrityManagerFactory.createStandard(applicationContext);
StandardIntegrityTokenProvider integrityTokenProvider;
long cloudProjectNumber = ...;
// Prepare integrity token. Can be called once in a while to keep internal
// state fresh.
standardIntegrityManager.prepareIntegrityToken(
PrepareIntegrityTokenRequest.builder()
.setCloudProjectNumber(cloudProjectNumber)
.build())
.addOnSuccessListener(tokenProvider -> {
integrityTokenProvider = tokenProvider;
})
.addOnFailureListener(exception -> handleError(exception));