中间人攻击与证书绑定:保护你的应用

网络安全领域,中间人攻击是一种常见的攻击手段,攻击者通过拦截、查看甚至修改应用与服务器之间的加密通信来获取敏感信息,如密码和应用私有信息。尽管这种攻击方式相对容易防范,但许多应用并未采取相应的保护措施。

以个人为例,拥有一个Ring门铃,并在Windows上安装了Ring(UWP)应用,以便可以(除了其他事项外)确保发出的警报包被邮局取走。以下是应用与服务器之间的一次HTTPS会话示例:

如果在请求https://api.ring.com/clients_api/profile?时将"bypass_account_verification"的值修改为True,会发生什么呢?在FiddlerScript部分,可以轻松实现这种操作,这在补充节目Code Hour中有展示。

如果正在开发应用,面临的中间人攻击风险并不限于那些愿意安装Fiddler根证书以隐藏所有HTTPS窥探错误的好奇开发者。考虑这个令人恐惧且表达清晰的Stack Overflow回答:

任何在客户端和服务器之间的人都可以对HTTPS进行中间人攻击。如果认为这不太可能或罕见,考虑一下有商业产品系统地解密、扫描并重新加密所有通过互联网网关的SSL流量。它们通过向客户端发送一个即时创建的SSL证书,该证书的详细信息复制自“真实”的SSL证书,但使用不同的证书链签名。如果这个链以浏览器信任的任何CA结尾,这个MITM对用户来说将是隐形的。

对于应用开发者来说,被低估的解决方案是:证书绑定。

证书绑定,或称为公钥绑定,是限制应用程序愿意与之通信的服务器的过程,主要是为了消除中间人攻击。如果上述Ring应用实现了证书绑定,那么当Fiddler拦截并重新签名传输中的所有HTTPS请求时,它们将收到错误。Windows个人银行应用就是这样做的,如果在启动时检测到签名证书不是它应该是的(即使它是完全受信任的),它会给出错误:“很抱歉,无法完成请求。请再试一次”。

在.NET中实现证书绑定通常相当容易。它通常涉及ServicePointManager的ServerCertificateVerificationCallback方法。它看起来像这样:

public static async void Main(string[] args) { // Set callback (delegate) ServicePointManager.ServerCertificateValidationCallback = PinPublicKey; WebRequest request = WebRequest.Create("https://..."); WebResponse response = await request.GetResponseAsync(); // ... } private static bool PinPublicKey(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { if (certificate == null || chain == null) return false; if (sslPolicyErrors != SslPolicyErrors.None) return false; // Verify against known public key within the certificate String pk = certificate.GetPublicKeyString(); return pk.Equals(PUB_KEY); }

这适用于AppDomain中的所有请求(顺便说一句,这对库提供者来说不好,但对常规应用开发者来说很方便)。也可以通过设置HttpClient的ServerCertificateCustomValidationCallback方法,逐个请求地进行操作(如下例所示)。

无论哪种方式,注意GetPublicKeyString()方法。这是一个非常有用的提取公钥的方法,以便可以将其与已知值进行比较。正如OWASP在Pinning Cheat Sheet中描述的,这比绑定整个证书更安全,因为它避免了服务器轮换其证书时的问题。

这在Xamarin和.NET Core中效果很好。不幸的是,在通用Windows平台(UWP)应用中没有ServicePointManager。此外,将不会得到X509Certificate对象,所以获取公钥更难。而且这个话题几乎没有文档,所以以下部分代表了花费了相当多的时间来摸索。

正如Windows应用团队博客所描述的,UWP中有

两个HttpClient:

在托管的UWP应用中实现HTTP客户端角色最常用和推荐的两个API是System.Net.Http.HttpClient和Windows.Web.Http.HttpClient。这些API应该优先于旧的、不推荐的API,如WebClient和HttpWebRequest(尽管HttpWebRequest的一小部分在UWP中可用于向后兼容性)。

如果因为想要使用前面提到的ServerCertificateCustomValidationCallback方法而想要使用System.Net.Http.HttpClient,那么当尝试编写以下代码时,会有一个不愉快的惊喜:

HttpMessageHandler handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = OnCertificateValidate }; var httpClient = new System.Net.Http.HttpClient(handler);

UWP会给这个回应:

System.PlatformNotSupportedException: The value ' System.Func`5[System.Net.Http.HttpRequestMessage, System.Security.Cryptography.X509Certificates.X509Certificate2, System.Security.Cryptography.X509Certificates.X509Chain,System.Net.Security.SslPolicyErrors, System.Boolean]' is not supported for property ' ServerCertificateCustomValidationCallback' .

即使使用Paul Betts的出色ModernHttpClient也无法解决这个问题。发现的唯一解决方案是使用Windows.Web.Http.HttpClient和ServerCustomValidationRequested事件,如下所示:

using (var filter = new HttpBaseProtocolFilter()) { // todo: probably remove this in production, avoids overly aggressive cache filter.CacheControl.ReadBehavior = HttpCacheReadBehavior.NoCache; filter.ServerCustomValidationRequested += FilterOnServerCustomValidationRequested; var httpClient = new Windows.Web.Http.HttpClient(filter); var result = await httpClient.GetStringAsync(new Uri(url)); // always unsubscribe to be safe filter.ServerCustomValidationRequested -= FilterOnServerCustomValidationRequested; }

注意CacheControl方法。当请求在Fiddler中停止显示时,以为自己疯了。原来Windows.Web.Http.HttpClient的缓存非常激进,与System.Net.Http.HttpClient不同,它不会对它之前见过的URL再次发起请求,它只会返回之前的结果。

拼图的最后一块是FilterOnServerCustomValidationRequested方法以及如何在没有X509Certificate的好处下提取证书的公钥:

private void FilterOnServerCustomValidationRequested(HttpBaseProtocolFilter sender, HttpServerCustomValidationRequestedEventArgs args) { if (!IsCertificateValid(args.RequestMessage, args.ServerCertificate, args.ServerCertificateErrors)) { args.Reject(); } } private bool IsCertificateValid(Windows.Web.Http.HttpRequestMessage httpRequestMessage, Certificate cert, IReadOnlyList sslPolicyErrors) { // disallow self-signed certificates or certificates with errors if (sslPolicyErrors.Count > 0) { return false; } // by default reject any requests that don't use ssl or match up to our known base url if (!RequestRequiresCheck(httpRequestMessage.RequestUri)) return false; var certificateSubject = cert?.Subject; bool subjectMatches = certificateSubject == CertificateCommonName; var certificatePublicKeyString = GetPublicKey(cert); bool publicKeyMatches = certificatePublicKeyString == CertificatePublicKey; return subjectMatches && publicKeyMatches; } private static string GetPublicKey(Certificate cert) { var certArray = cert?.GetCertificateBlob().ToArray(); var x509Certificate2 = new X509Certificate2(certArray); var certificatePublicKey = x509Certificate2.GetPublicKey(); var certificatePublicKeyString = Convert.ToBase64String(certificatePublicKey); return certificatePublicKeyString; } private bool RequestRequiresCheck(Uri uri) { return uri.IsAbsoluteUri && uri.AbsoluteUri.StartsWith("https://", StringComparison.CurrentCultureIgnoreCase) && uri.AbsoluteUri.StartsWith(HttpsBaseUrl, StringComparison.CurrentCultureIgnoreCase); }

可能有更便宜的GetPublicKey()方法版本,它涉及索引到类型数组,但上述方法对来说看起来相当干净。唯一的可能问题是可能需要根据UWP版本引用Microsoft的System.Security.Cryptography.X509Certificates nuget包。

可以在在构建的Siren of Shame UWP应用的Maintenance项目中看到最终版本,以及可能的现成CertificatePinningHttpClientFactory。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485