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