写在前面的话
在这篇文章中,我们将演示如何利用Frida脚本来绕过Android的网络安全配置,这是一种绕过网络安全配置的新技术。除此之外,我们还将演示如何在其他场景来测试该脚本,并分析脚本的运行机制。
在之前的一次Android应用程序安全审计过程中,首先我们要做的就是准备渗透测试的环境,并配置应用程序来绕过网络安全配置。由于我个人比较喜欢Frida,因此它也就成为了我的首选工具。
当时我下载了两到三个脚本,但是当我在Android 7.1.0中运行脚本时,没有一个可以成功的。这也就是为什么我想研究网络安全配置的运行机制,并且如何用Frida绕过它们。
我所做的第一件事就是生成不同的测试用例,我尝试选择了几款比较常见的:
1、OKHttp
2、HttpsURLConnection
3、WebView
接下来,我用不同的网络安全配置生成了三个应用程序:
1、一个使用了默认NSC配置的应用程序-BypassNSC
2、一个带有NSC文件(仅使用了系统证书)的应用程序-BypassNSC2
3、一个带有NSC文件的应用程序(强制证书绑定)-BypassNSC3
代码会解析并验证Android SDK中的网络安全配置,我的测试版本为24、25和26。广大用户可以点击【这里】获取我所生成的应用程序以及所使用的脚本。
脚本名如下:
network-security-config-bypass-1.js
network-security-config-bypass-2.js
network-security-config-bypass-3.js
network-security-config-bypass-cr.js
下图为每一个脚本的分析测试结果:
network-security-config-bypass-1.js
原始引用:【链接】
该脚本会修改NetworkSecurityConfig.Builder类中的getEffectiveCertificatesEntryRefs方法,该方法可以返回有效证书列表。在标准的Android配置中,它所返回的有效证书列表就是安装在目标系统中的有效证书。毫无以为,这个脚本将直接返回用户安装的证书,因此理论上来说,它可以绕过前两个应用程序的网络安全配置,但让我惊讶的是,它竟然也适用于第三种情况,也就是证书绑定配置。我们可以使用下列方法来验证绑定的证书:
android.security.net.config.NetworkSecurityTrustManager.checkPins
下面的栈跟踪记录显示了代码到checkPins函数的执行路径:
at android.security.net.config.NetworkSecurityTrustManager.checkPins(Native Method) at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:95) at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:88) at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:178) at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:596) at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:357) ...
如果补丁没有执行,那么当执行路径抵达该函数时,则会抛出异常:
Caused by: java.security.cert.CertificateException: Pin verification failed at android.security.net.config.NetworkSecurityTrustManager.checkPins(NetworkSecurityTrustManager.java:148) at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:95) at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:88) at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:178) at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:596) at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method) at com.android.org.
我们看一下这个方法的实现代码(API 25):
private void checkPins(List<X509Certificate> chain) throws CertificateException { PinSet pinSet = mNetworkSecurityConfig.getPins(); if (pinSet.pins.isEmpty() || System.currentTimeMillis() > pinSet.expirationTime || !isPinningEnforced(chain)) { return; } Set<String> pinAlgorithms = pinSet.getPinAlgorithms(); Map<String, MessageDigest> digestMap = new ArrayMap<String, MessageDigest>( pinAlgorithms.size()); for (int i = chain.size() - 1; i >= 0 ; i--) { X509Certificate cert = chain.get(i); byte[] encodedSPKI = cert.getPublicKey().getEncoded(); for (String algorithm : pinAlgorithms) { MessageDigest md = digestMap.get(algorithm); if (md == null) { try { md = MessageDigest.getInstance(algorithm); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } digestMap.put(algorithm, md); } if (pinSet.pins.contains(new Pin(algorithm, md.digest(encodedSPKI)))) { return; } } } // TODO: Throw a subclass of CertificateException which indicates a pinning failure. throw new CertificateException("Pin verification failed"); }
这个方法可以接收网站通信所返回的证书列表,它做的第一件事情就是条件检测:
1、pinset为空
2、验证时pinset已过期
3、证书绑定并非配置强制要求
如果上述条件没有一个为真,绑定验证将会被忽略。如果必须实现验证,应用程序将检查站点提供的任何证书是否与网络安全配置文件中定义的某个证书匹配,此时验证就是成功的。如果没有发生这种情况,该方法将抛出前一个stacktrace中显示的异常。
一开始,我以为这个问题存在于用来分析每一个证书的for循环中,所以我在Frida脚本中添加了下列log:
var Pin = Java.use("android.security.net.config.Pin");
Pin.$init.implementation = function (digestAlg, digest) {
var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("\nBacktrace:\n" + bt);
console.log(digestAlg);
return this.$init(digestAlg,digest);
}
它可以输出验证过程中的每一个pin,当我运行改动过的应用程序之后,我发现这并没有什么用。于是我又在log中添加了针对pinSet.getPinAlgorithms()的调用,并在for循环之前执行:
var PinSet = Java.use("android.security.net.config.PinSet");
PinSet.getPinAlgorithms.implementation = function () {
var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("\nBacktrace:\n" + bt);
return this.getPinAlgorithms();
}
这一次什么都没打印出来,于是接下来我得看看函数的条件判断是否为真,因此我在脚本中添加了下列代码:
NetworkSecurityTrustManager.checkPins.implementation = function (pins) {
var bt = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new());
console.log("\nBacktrace:\n" + bt);
pinSet = this.mNetworkSecurityConfig.value.getPins();
console.log("pinSet.pins.value.isEmpty: " +pinSet.pins.value.isEmpty());
console.log("isPinningEnforced: " +this.isPinningEnforced(pins));
console.log("pins.isEmpty: " +pins.isEmpty());
console.log(System.currentTimeMillis())
console.log(pinSet.expirationTime.value);
console.log(System.currentTimeMillis() > pinSet.expirationTime.value);
this.checkPins(pins);
}
运行应用程序之后,我拿到了下列输出:
pinSet.pins.value.isEmpty: false
isPinningEnforced: false <-- this condition is the problematic one
pins.isEmpty: false
1562031248274
9223372036854775807
false
我们可以看到,isPinningEnforced为False,此时其他所有的表达式都将为True。该方法的实现代码如下:
private boolean isPinningEnforced(List<X509Certificate> chain) throws CertificateException {
if (chain.isEmpty()) {
return false;
}
X509Certificate anchorCert = chain.get(chain.size() - 1);
TrustAnchor chainAnchor =
mNetworkSecurityConfig.findTrustAnchorBySubjectAndPublicKey(anchorCert);
if (chainAnchor == null) {
throw new CertificateException("Trusted chain does not end in a TrustAnchor");
}
return !chainAnchor.overridesPins;
}
原来,问题出在findTrustAnchorBySubjectAndPublicKey的身上,它是NetworkSecurityConfig类中的一个方法,能够返回一个chainAnchor:
public TrustAnchor findTrustAnchorBySubjectAndPublicKey(X509Certificate cert) {
for (CertificatesEntryRef ref : mCertificatesEntryRefs) {
TrustAnchor anchor = ref.findBySubjectAndPublicKey(cert);
if (anchor != null) {
return anchor;
}
}
return null;
}
它会在配置过程中对创建的CertificatesEntryRef进行迭代,并返回第一个跟SubjectAndPublicKey匹配的对象。在这个场景中,它将返回的是其中一个代理。研究完源代码后,我找到了CertificatesEntryRef类,并发现了唯一一个构造器:
public CertificatesEntryRef(CertificateSource source, boolean overridesPins) {
mSource = source;
mOverridesPins = overridesPins;
}
如果再回头看一次Frida脚本,你将会发现CertificatesEntryRef是以下列方式创建的:
NetworkSecurityConfig_Builder.getEffectiveCertificatesEntryRefs.implementation = function(){
origin = this.getEffectiveCertificatesEntryRefs()
source = UserCertificateSource.getInstance()
userCert = CertificatesEntryRef.$new(source,true) <-- sets overridesPins in true
origin.add(userCert)
return origin
}
这也就是为什么这个脚本适用于所有场景。
network-security-config-bypass-2.js
原始引用:【链接】
在这个场景下,唯一适用的就是不包含网络安全配置文件的应用程序。
我分析了一下为什么补丁不起作用,原因在于parseNetworkSecurityConfig方法:
XmlUtils.beginDocument(parser, "network-security-config");
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
//here it creates a NetworkSecurityconfig.Builder based on the xml structure.
...
}
...
NetworkSecurityConfig.Builder platformDefaultBuilder =
NetworkSecurityConfig.getDefaultBuilder(mTargetSdkVersion); <-- this is the method changed with the script
addDebugAnchorsIfNeeded(debugConfigBuilder, platformDefaultBuilder);
//baseConfigBuilder is null only if the xml network-security-config is not defined in the AndroidManifest.xml
if (baseConfigBuilder != null) {
baseConfigBuilder.setParent(platformDefaultBuilder);
addDebugAnchorsIfNeeded(debugConfigBuilder, baseConfigBuilder);
} else {
baseConfigBuilder = platformDefaultBuilder;
}
...
mDefaultConfig = baseConfigBuilder.build();
mDomainMap = configs;
}
构建方法会生成NetworkSecurityConfig实体:
public NetworkSecurityConfig build() {
boolean cleartextPermitted = getEffectiveCleartextTrafficPermitted();
boolean hstsEnforced = getEffectiveHstsEnforced();
PinSet pinSet = getEffectivePinSet();
List<CertificatesEntryRef> entryRefs = getEffectiveCertificatesEntryRefs();
return new NetworkSecurityConfig(cleartextPermitted, hstsEnforced, pinSet, entryRefs);
}
有效证书源在entryRefs变量中定义,其构造方法如下:
private List<CertificatesEntryRef> getEffectiveCertificatesEntryRefs() {
if (mCertificatesEntryRefs != null) {
return mCertificatesEntryRefs;
}
if (mParentBuilder != null) {
return mParentBuilder.getEffectiveCertificatesEntryRefs();
}
return Collections.<CertificatesEntryRef>emptyList();
}
此时,mCertificatesEntryRefs不为空,并会返回标准的SystemCertificateSource。因此,mParentBuilder永远不会被调用。
接下来,当服务器证书验证成功后,应用程序将会调用NetworkSecurityConfig.findTrustAnchorBySubjectAndPublicKey方法,该方法将会对有效证书进行过滤:
public TrustAnchor findTrustAnchorBySubjectAndPublicKey(X509Certificate cert) {
for (CertificatesEntryRef ref : mCertificatesEntryRefs) {
TrustAnchor anchor = ref.findBySubjectAndPublicKey(cert);
if (anchor != null) {
return anchor;
}
}
return null;
}
并导致栈跟踪抛出异常:
com.android.org.conscrypt.TrustManagerImpl.checkTrusted(TrustManagerImpl.java:375)
at com.android.org.conscrypt.TrustManagerImpl.getTrustedChainForServer(TrustManagerImpl.java:304)
at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:94)
at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:88)
...
network-security-config-bypass-3.js
原始引用:【链接】
该脚本适用于三个场景中的其中两个,因为补丁会在验证证书的方法中执行。但是它不适用于第三种场景,因为证书绑定是在其他方法中执行的,具体可以参考栈跟踪记录中的错误信息:
at android.security.net.config.NetworkSecurityTrustManager.checkPins(NetworkSecurityTrustManager.java:148)
at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(NetworkSecurityTrustManager.java:95)
at android.security.net.config.RootTrustManager.checkServerTrusted(RootTrustManager.java:88)
at com.android.org.conscrypt.Platform.checkServerTrusted(Platform.java:203)
at com.android.org.conscrypt.OpenSSLSocketImpl.verifyCertificateChain(OpenSSLSocketImpl.java:592)
at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake(Native Method)
at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:351)
... 25 more
network-security-config-bypass-cr.js
原始引用:【链接】
在这个场景下,修补的方法为getConfigSource,当代码对network-security-config进行解析时将会调用这个方法。我们可以看到,重写的方法会创建一个DefaultConfigSource,并在Android 23中以参数形式进行定义。
总结
我通过本文所介绍的方法实现了我的目标,也就是利用脚本来绕过Android SDK中实现的网络安全配置。在研究过程中,我对SDK、Frida和Android生态系统又有了新的认识。希望本文提供的技术能给Android安全研究人员提供帮助。
* 参考来源:neo-geo2,FB小编Alpha_h4ck编译,转载请注明来自FreeBuf.COM。