开发应用程序时,必须使应用程序尽可能安全,尤其是在处理敏感的用户信息时。要确定应用程序安全性的薄弱环节,最好由移动安全专家对其进行渗透测试。
在本文的示例中,我们将使用OkHttp,因为它是一个完善的库,并且是Android社区中最受欢迎的库。另外,在这篇文章中,我们将集中讨论网络,特别是TLS协议,以及在连接到特定web服务时如何使应用程序尽可能安全的技巧。
TLS连接
如今,大多数应用程序都与某种Web服务进行通信。因此,拥有一个安全的连接是非常重要的。
最坏的情况是,应用程序使用HTTP连接,而没有传输层安全(TLS)协议。它将不可避免地失败,因为你的数据将以纯文本的形式传输。因此,非常容易跟踪。TLS是加密通信的核心部分,它使HTTPS调用安全且经过身份验证。
尽管它基于SSL 3.0,但你会经常听到人们将SSL用作TLS的同义词,其实这并不是100%正确的,因为SSL(TLS的前身)在2015年被IETF弃用。
实际上,从2020年初开始,大多数大公司(Mozzila,Google,Microsoft,Apple)。
都将TLS 1.2作为互联网的新的最低标准。
TLS在Android OS中的应用
Android从API级别16开始支持TLS 1.2,并且从级别21开始默认启用。不幸的是,这也不是100%正确的。
是的,你可以依赖API级别21/22及更高版本的TLS 1.2。但是,你不能指望API级别为16到19。有一篇由Ankush Gupta撰写的精彩文章,其中更详细地描述了此问题,因此,如果你想了解更多有关如何以及为什么的信息,请点此查看。
了解了Android操作系统必须提供的功能,以及使用至少TLS 1.2实现安全连接的目标之后,我们将继续进行讨论。
准备渗透测试的准备阶段
在本节中,我们将讨论两个特定的示例。第一种示例是minSdk在API级别16到19之间。另一种示例是,你的minSdk是API级别21或更高。
MinSdk在API级别16和19之间
如果要在Android 5之前的版本上启用TLS 1.2,则必须扩展SSLSocketFactory并创建自己的实现。该实现非常简单明了,看起来像这样:
class TlsSocketFactory constructor(private val socketFactory: SSLSocketFactory) : SSLSocketFactory() { override fun getDefaultCipherSuites(): Array { return socketFactory.defaultCipherSuites } override fun getSupportedCipherSuites(): Array { return socketFactory.supportedCipherSuites } override fun createSocket(socket: Socket?, host: String?, port: Int, autoClose: Boolean): Socket { return socketFactory.createSocket(socket, host, port, autoClose).enableTls() } override fun createSocket(host: String?, port: Int): Socket { return socketFactory.createSocket(host, port).enableTls() } override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int): Socket { return socketFactory.createSocket(host, port, localHost, localPort).enableTls() } override fun createSocket(host: InetAddress?, port: Int): Socket { return socketFactory.createSocket(host, port).enableTls() } override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int): Socket { return socketFactory.createSocket(address, port, localAddress, localPort).enableTls() } private fun Socket.enableTls(): Socket { if (this is SSLSocket) enabledProtocols += TlsVersion.TLS_1_2.javaName() return this }}
此实现包含我们自己的socketFactory对象,我们将其用作委托并简单地更新已启用的协议。
现在我们有了一个定制的SSLSocketFactory实现,我们必须告诉OkHttp通过SSLSocketFactory builder参数使用它:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { val sslContext = SSLContext.getInstance(TlsVersion.TLS_1_2.javaName()) sslContext.init(null, *yourTrustManagers*, null) sslSocketFactory(TlsSocketFactory(sslContext.socketFactory), *yourTrustManager*) }
yourTrustManagers代表一组TrustManager实例,如果你不知道如何创建自己的信任管理器,或者不需要自定义实现,则可以随时加载默认的管理器。在下一节中,你将学习如何加载默认的信任管理器,在这一节中,我们将讨论证书。
请注意,上述TlsSocketFactory实现仅将指定的协议设置为默认协议。它不涉及未在设备上安装协议的情况。要涵盖该实例,你将必须使用Google Play服务中的ProviderInstaller。
另外,请记住,你可以调用installIfNeeded(context)或installIfNeededAsync(context),而且必须在创建TlsSocketFactory之前调用。
对于更具体的实现,你可以在以下要点上检查示例实现:installIfNeeded,installIfNeededAsync。
MinSdk API级别21
在这种情况下,你至少应使用OkHttp 3.13.x ,它仅适用于Android 5及以上的版本。如果你已经在使用该版本的Android,则可以使用该版本的库,因为这个版本的库已经将TLS 1.2作为所有HTTPS调用的默认版本。
如果你使用的是较旧版本的OkHttp库,并且由于任何原因而无法对其进行更新,则必须按照上述API级别16和19之间的MinSdk部分中所述的步骤进行操作。
顺便说一句,如果你对OkHttp中的TLS配置的历史感兴趣,这是一本非常好的文章。
既然你的应用程序使用了更安全的TLS协议版本,那么了解如何成功验证TLS连接是很重要的。
验证TLS连接
在验证TLS连接的过程中,有两部分是必不可少的:
1. 证书验证(证书绑定);
2. 主机名验证。
在以下各段中,我们将详细介绍每一部分。
证书验证(证书绑定)
本节将重点介绍证书绑定及其实现方法,此外,我们将简要介绍证书验证的完成方式。
为了使TLS连接按预期工作,客户端必须有一种方法来验证服务器上使用的证书是受信任的和有效的。在我们的示例中,客户端是Android应用程序。CA是可以颁发受信任的限时证书的实体。CA是有资格颁发受信任的、有时间限制的证书的对象。CA颁发的证书是一个可验证的小数据文件,其中包含身份凭证,以帮助网站、人员和设备代表其真实的在线身份。
但是,我们的Android设备如何区分CA颁发的证书和那些所谓的自签名证书?借助一系列CA(设备已存储在Android系统级别上),从4.2版开始,Android包含100多个CA,并且在每个发行版中都会对其进行更新。
要以编程方式查看Android设备中的所有证书,可以加载默认的TrustManager。这样,你将看到与系统级证书相对应的所有文件路径,以及用户安装的证书。这些证书也称为根证书,要检索默认的TrustManager,可以使用以下代码:
private fun getDefaultTrustManager(): Array? { val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) trustManagerFactory.init(null as KeyStore?) val trustManagers = trustManagerFactory.trustManagers if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) { throw IllegalStateException("Unexpected default trust managers:" + Arrays.toString(trustManagers)) } return trustManagers}
当Android设备尝试与服务建立安全连接时,它会经历称为TLS握手的过程。在建立连接的过程中,服务器可以发送整个证书链,设备必须使用其信任的CA组进行验证。
证书链也称为信任链,它基本上是从终端用户(在我们的示例中为Android设备)到根证书的验证和确认的链接路径。
链中的证书由根证书,零或多个中间证书和叶证书组成。
请查看下面的图片,以澄清这种混淆:
如你所见,链中的证书是相互依赖的。这意味着即使证书的CA之一受到破坏,也可以利用信任链。由于使用默认的TrustManager,受损的CA可用于颁发证书,Android设备将自动信任该证书。
为了进一步保护你的应用程序不受伪造证书的影响,你可以使用一种称为证书绑定的技术。证书绑定是针对MITM(即中间人)攻击的一种额外防御机制,在该机制中,开发人员实现了一个自定义信任管理器,该管理器仅包含可以验证应用程序正在与之通信的Web服务器的证书,这意味着你的应用程序将不信任任何其他系统信任的CA或用户安装的证书。
证书绑定是渗透测试中最常见的测试示例之一,因此以下段我们将详细说明如何为它准备一个应用程序。
准备阶段
在本节中,我们将向介绍如何为自签名证书和CA颁发的证书成功实现证书绑定。要绑定证书,第一步是从证书链中确定要绑定的证书。
由于应用程序首先与该证书进行交互,因此通常建议绑定叶子证书,因为攻击面很小。如果该证书无法通过验证,则该应用将无法使用。
自签名证书
绑定自签名证书是你仅应在测试环境中执行的操作,要将证书绑定在Android应用中,首先要做的是获取证书,这可以通过终端设备的OpenSSL轻松完成:
$ openssl s_client -connect example.com:443 -showcerts
上面的命令将返回指定网站的整个证书链的列表,如果要将证书保存在本地文件中或通过浏览器下载证书,请点击此处了解如何进行操作。
获得所需的证书后,下一步是创建将包含所获取的证书的自定义TrustManager。可以按以下步骤完成:
private fun createCustomTrustManager(context: Context): Array? { val keyStore = KeyStore.getInstance("BKS").apply { load(null, null) } val certificateFactory = CertificateFactory.getInstance(CERT_TYPE) val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) context.resources.openRawResource(R.raw.leaf_cert_expires_1_1_2077).use { keyStore.setCertificateEntry("MyLeafCert", certificateFactory.generateCertificate(it)) } trustManagerFactory.init(keyStore) return trustManagerFactory.trustManagers}
此代码从原始资源文件夹中加载leaf_cert_expires_1_1_2_2.pem文件,并将其设置为MyLeafCert别名下的空密钥库中的证书条目。在密钥库中设置所需的证书后,请使用相同的密钥库来初始化trustManager。
一切准备就绪,就可以通过sslSocketFactory构建器函数将TrustManager的实例传递给OkHttp了,如上面的TLS连接部分所述。这种方法有时称为“硬证书绑定”,因为我们将绑定的证书捆绑在我们的应用程序中,并且每次证书更改或更新时,我们都必须上传包含新证书的应用程序的新版本。
由受信任的CA签署的证书
先前的代码示例可以以相同的方式用于受信任的CA签名的证书。如果你希望在所有情况下都实现一个方法,则这是一种好方法。但是,OkHttp使用CertificatePinner类确实提供了一种用于绑定非自签名证书的机制。有关此类的更多信息,请参见此处。
CertificatePinner采用另一种绑定方式,即使用证书的主题加密公共密钥进行验证。与之前描述的方法(你需要将绑定的证书与应用程序捆绑在一起)不同,公钥绑定只需要证书公布的base64 SHA-256哈希值,这是因为哈希可以安全地作为纯字符串存储在你的应用中应用程序而不会引起安全问题。
实现更加简单:
const val MY_LEAF_CERT_EXPIRES_1_1_2077 = "sha256/A23dA8l41A46gjAcAiAA32AAA8juvAnvvlk85kub6A5="private fun buildCertificatePinner(): CertificatePinner { return CertificatePinner.Builder() .add("yourHostname.com", MY_LEAF_CERT_EXPIRES_1_1_2077) .build()}// Setting up OkHttp with the CertificatePinner objectOkHttpClient.Builder() .certificatePinner(buildCertificatePinner()) .build()
这是一个非常简单直观的方法,除了更容易实现之外,这种方法还有另一个值得注意的优点。如果证书过期,服务器只需要更新证书,而不需要更改证书的加密公钥。
这实际上意味着如果证书得到更新,你将不需要更新应用程序,此方法的缺点之一是它不支持自签名证书。
另外,在上述实现中,你可能会注意到了yourHostname.com字符串。该值用于主机名验证,我们将在下一部分中讨论。
最后,请记住,这只是一个简单的证书绑定介绍,只是足够了解基本并准备你的应用程序渗透测试。为了更容易地实现和维护,应该与应用程序正在通信的web服务的管理员就证书绑定达成一致意见,以确定它是否适合。
如果你决定使用证书绑定,请确保对整个过程有更好的理解。
主机名验证
最后,我们介绍验证TLS连接的第二个关键部分,即主机名验证。
开发人员常犯的一个错误是设置一个宽松的主机名验证器,或者更糟,接受所有的主机名。这基本上意味着,攻击者可以使用受感染的CA颁发有效的证书,为其选择任何域名,然后执行MITM攻击。
如果使用OkHttp,则HostnameVerifier的默认实现足以验证你与主机的连接。不过,我们将向你展示几个示例,说明如何实现HostnameVerifier,以更好地了解其工作原理。
准备阶段
如果你没有使用CertificatePinner进行主机名验证,那么可以使用Java的HostnameVerifier,因为它是进行主机名验证的基本接口。这个接口由OkHttp支持,你只需将它传递给hostnameVerifier构建器函数即可。
在TLS握手期间,验证机制可以回调此接口的实现者,以确定是否应该允许此连接。具体的验证可以使用OkHostnameVerifier来完成,尽管在使用HttpsURLConnection.getDefaultHostnameVerifier()的一些实现中会遇到问题。
在后台,它仍然使用OkHostnameVerifier,但是来自同一个类的内部Android版本。
我们准备使用OkHostnameVerifier.INSTANCE.verify检查示例实现:
hostnameVerifier { _, session -> OkHostnameVerifier.INSTANCE.verify("yourHostname.com", session)}
上面的代码将检查输入的主机名yourHostname.com是否包含在当前SSLSession的证书内。只有这样,验证才能成功。如果你想信任一个完整的子域,你可以使用通配符,但是你必须像以下这样使用verifyHostname方法:
hostnameVerifier { hostname, _ -> OkHostnameVerifier.INSTANCE.verifyHostname(hostname, "*.yourHostname.com")}
实现结果将取决于你的环境和应用程序连接到的所有服务,如果你想进一步了解此方法支持的通配符规则,请点此查阅。
总结
至此,你已经对TLS连接有了更好的理解,并知道如何为下一次渗透测试准备你的应用程序。注意TLS连接的两个关键部分,证书和主机名验证。如果这两个验证中的一个被破坏,则整个TLS连接都将无效,并且该应用程序很容易成为MITTM攻击的目标。
不过在本文中,我们没有介绍Android网络安全配置,这是一个强大的工具,可以让应用程序在不修改应用程序代码的情况下自定义其网络安全设置。
不幸的是,它仅在Android 7以上的版本中才可用,但是如果你可以在minSdk API级别24及以上操作,或者可以根据sdk级别有两个单独的绑定实现,那么这种方法应该是首选来处理你的网络配置的方法。
本文翻译自:https://infinum.com/the-capsized-eight/how-to-prepare-your-android-app-for-a-pentest如若转载,请注明原文地址: