HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(一)
HTTP严格安全传输(HTTP Strict Transport Security, HSTS)chromuim实现源码分析(二)
HTTP strict transport security (HSTS) is defined in
http://tools.ietf.org/html/ietf-websec-strict-transport-sec
HTTP-based dynamic public key pinning (HPKP) is defined in
http://tools.ietf.org/html/ietf-websec-key-pinning.
深入理解需参考chromuim设计文档:
https://www.chromium.org/developers/design-documents
(中文版)https://ahangchen.gitbooks.io/chromium_doc_zh/content/zh//General_Architecture/Threading.html
chromuim在线源码:https://chromium.googlesource.com/chromium/src/+/58.0.3025.2/
------------------------------------------------分割线---------------------------------------------------------------------------------
通过观察文件名,发现涉及的文件主要是net目录下的:
Transport_security_persister.cc
Transport_security_persister.h
Transport_security_state.cc
Transport_security_state.h
Url_request_http_job.cc
在开始之前,需要从源码层面了解chromuim的网络栈部分,可参考《WebKit技术内幕》作者朱永盛的博文http://blog.csdn.net/milado_nju/article/details/9255563以及官网网络栈部分的文档http://www.chromium.org/developers/design-documents/network-stack。简单来说,发送网络请求基本都是通过URLRequest类,再根据不同协议选择不同的工厂,如HTTP为URLRequestHttpJob,由于HSTS针对HTTP,所以也只需关注URLRequestHttpJob;另一个重要的类是URLRequestContext,它包含其他完成URL请求的上下文信息,如cookie、主机解析、缓存以及HSTS信息等;许多URLRequest对象共享一个URLRequestContext。本文采用好理解的“自上而下”顺序来进行总结分析,但是实际中由于作者对chromuim完全没有认识,其实是从源码中搜索关键词看注释再查找引用一步一步摸索的。
类URLRequestHttpJob的Factory方法在创建实例前,调用了Url_request_http_job.cc中的MaybeInternallyRedirect()函数对是否要进行升级HSTS进行了判断,通过名字可以猜出chromuim是采用内部重定向的方式来实现HTTP到HTTPS升级的,这跟使用开发者工具调试时观察到的请求一致。
MaybeInternallyRedirect函数中,根据request->url()来判断该域名是否应该升级HSTS,若hsts->ShouldUpgradeToSSL返回false,就返回nullptr,不用重定向;否则,使用Replacements类,根据request->url()的协议,来进行替换为https或wss(即websocket),再返回307状态码的URLRequestRedirectJob类。因此,URLRequestHttpJob返回URLRequestRedirectJob而非正常的URLRequestHttpJob,进行重定向来升级协议。由此可见,hsts的ShouldUpgradeToSSL方法就至关重要了,是否升级https全看它的返回值了。
hsts是从request->context()->transport_security_state()取出,通过查看源码,即是URLRequest中的URLRequestContext指针类型变量context_中的TransportSecurityState*类型的transport_security_state变量。这句话有点绕,其实就是从
取出了保存HSTS信息的变量,该变量(260行)类型为TransportSecurityState*,TransportSecurityState就是实现HSTS机制的重点了。另外,源码指针的实现也呼应了开始提到的多URLRequest对一个URLRequestContext。URLRequestContext
Transport_security_state.h中可以看到TransportSecurityState的定义,它驻留在内存中追踪哪个域名启用了HSTS和PKP,并且用SetDelegate方法注册了一个代理来存储状态到硬盘中。为了解一个类,发现从其对应的单元测试文件可以得到一个整体的感性认识,单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。从单元测试文件Transport_security_state_unittest.cc中可以看出该类的简单用法,以及作者已经考虑到的一些避免HSTS被绕过的情况。比如这里就考虑了域名末尾加“.”和不加“.”在本策略中是一样的。并且,除了subdomain选项和preload外, 还有一些极端情况,如下所述。(Google C++ Testing Framework可参考这个网址http://developer.51cto.com/art/201108/285290.htm)
(1)防止拒绝服务的产生(比如添加一个域名为“.”的情况),
(2)对大小写不敏感(Google.com与GooGLe.CoM是一样的),
(3)规则冲突覆盖以更具体的为准(example.com的subdomain为true,而foo.example.com的subdomain为false,那么sub.foo.example.com的HSTS情况应为false;这点浏览器的实现情况应引起网站管理员的注意,可测试网站有没有这样的漏洞,RFC标准里我不记得提到),
(4)若设置了subdomain,不规范子域名也应使用HSTS(如2\x01.foo.example.test返回true),
(5)域名删除的情况。
下面的这个810行的测试没看懂,为何exampl1.com一会儿期望为true,一会儿为false?猜想可能是在测试单元测试功能有没有正常工作吧。
--------------------------------------------------分割线-------------------------------------------------
由单元测试和之前的源码分析以及源码中的注释可以得到,TransportSecurityState的ShouldUpgradeToSSL是判断该URL是否应该升级HTTPS的方法。查看ShouldUpgradeToSSL方法,发现是根据STSState类型变量的ShouldUpgradeToSSL()来返回布尔值的,从名称看依次检查了动态HSTS和预置的HSTS(即preload,如www.google.com已经预置在浏览器里了)。
先看类STSState,他是一个内部类,实现非常简单,注释说明它描述了HSTS的状态,属性包括过期时间、include_subdomains标识、域名等,根据host来由GetDynamicSTSState和GetStaticDomainState更新。首先来看GetDynamicSTSState,该函数根据host查询保存的信息,来对result赋值,并且特别强调了以更具体的结果为准;通过源码,其进行了host的规范化,然后用迭代器和enabled_sts_hosts_以及hash后的host(其实本地存储的HSTS信息文件中域名只有哈希后的值,为了隐私)来进行查询,并判断了查询结果中当前时间是否超过了过期时间,对结果result进行赋值为j->second,这样STSState就有值可以判断ShouldUpgradeToSSL了。ShouldUpgradeToSSL方法仅仅是比较upgrade_mode属性是否为MODE_FORCE_HTTPS。由此可见,STSStateMap类型的enabled_sts_hosts_就是本地维护HSTS信息的变量了,由此将我们引向了STSStateMap类。其实,若要发现实现细节方面的漏洞就需要详细看实现查找的算法了(CanonicalizeHost),对发现逻辑漏洞帮助不大,没有耐心的我就先略过算法细节分析。
bool TransportSecurityState::GetDynamicSTSState(const std::string& host, STSState* result) { DCHECK(CalledOnValidThread()); const std::string canonicalized_host = CanonicalizeHost(host); if (canonicalized_host.empty()) return false; base::Time current_time(base::Time::Now()); for (size_t i = 0; canonicalized_host[i]; i += canonicalized_host[i] + 1) { std::string host_sub_chunk(&canonicalized_host[i], canonicalized_host.size() - i); STSStateMap::iterator j = enabled_sts_hosts_.find(HashHost(host_sub_chunk)); if (j == enabled_sts_hosts_.end()) continue; // If the entry is invalid, drop it. if (current_time > j->second.expiry) { enabled_sts_hosts_.erase(j); DirtyNotify(); continue; } // If this is the most specific STS match, add it to the result. Note: a STS // entry at a more specific domain overrides a less specific domain whether // or not |include_subdomains| is set. if (current_time <= j->second.expiry) { if (i == 0 || j->second.include_subdomains) { *result = j->second; result->domain = DNSDomainToString(host_sub_chunk); return true; } break; } } return false; }
STSStateMap并不神秘,其实就是map<std::string, STSState>。那么具体是哪些函数对这个信息进行维护呢,通过对enabled_sts_hosts_查找引用,共12处,分布在EnableSTSHost、DeleteDynamicDataForHost、ClearDynamicData、DeleteAllDynamicDataSince、AddOrUpdateEnabledSTSHosts。
EnableSTSHost首先对host进行规范化,然后根据state.ShouldUpgradeToSSL()是否应该升级https来决定是存入信息还是删除信息,存入的时候可以看到,保存的不是host,而是哈希后的host;该方法中已经使用了ShouldUpgradeToSSL,可见state内已经有信息,这一步只是进行存储的操作,还需要查看引用是谁调用EnableSTSHost传入了state。通过查找引用,发现是AddHSTSInternal方法,决定ShouldUpgradeToSSL返回值的upgrade_mode来自于该方法的参数,还需要继续寻找调用者。(感觉这俩方法合并效率会更高些)AddHSTSHeader根据解析HSTS头的ParseHSTSHeader函数返回的max_age变量InSeconds()==0来判断是否应该强制升级HTTPS,这种时间等于0的做法容易产生问题,负数、溢出是否影响?判断有无这种漏洞就再次需要详细看实现算法细节了。
URLRequestHttpJob::ProcessStrictTransportSecurityHeader调用了AddHSTSHeader,实现中可以看到,浏览器仅接受HTTPS且没有证书错误的HSTS,从而避免恶意服务器拒绝服务其他域名;域名是IP地址也不会接受HSTS;有多个HSTS头则只接受第一个。注意到传入AddHSTSHeader作为host参数的是request_info_.url.host(),而这些变量不是通过参数传入而是类自身的变量,所以没必要再继续向上追。(当然缺乏经验的我肯定会犯错,向上追了好几步。ProcessStrictTransportSecurityHeader()被URLRequestHttpJob::NotifyHeadersComplete()调用,而调用NotifyHeadersComplete()的地方查找调用看似很多,好像处理头部时都会调用,只要注意限制在URLRequestHttpJob类,就发现其实只有一个调用者,SaveCookiesAndNotifyHeadersComplete,再网上追查两步就到了URLRequestHttpJob类的回调机制。但由于是检查HSTS头部时检查的ssl证书等信息,并且不存在其他路径到添加添加HSTS信息的函数,所以攻击者也难以通过别的通道来拒绝服务其他域名;本文只关心HSTS带来的问题,故没有必要再向上追寻,要寻找拒绝服务攻击可能性,关键就在于传入AddHSTSHeader的值了)
如果这个request_info_.url变量和ssl_info变量出问题:规范化后域名canonicalized_host哈希值碰撞(采用的SHA256几乎不可破)、和当前域名对应错误(若这个有问题那么cookie什么的就可以全乱套了,感觉出问题几率应该不大)、规范化后的域名和其他域名能一致(好多数据结构要看);则攻击者就可以操作其他域名HSTS信息。URLRequestHttpJob中request_info_.url是URLRequestHttpJob::Start()中赋值request_info_.url = request_->url();request_继承自父类URLRequestJob,指向了创建这个job的URLRequest。
到此为止,我们对HSTS实现的基本流程有了一定了解,但这仅仅是对管理enabled_sts_hosts_的EnableSTSHost函数追踪所学习到的,还有DeleteDynamicDataForHost、ClearDynamicData、DeleteAllDynamicDataSince、AddOrUpdateEnabledSTSHosts还有待继续学习。最后总结出如下两个图:
By Ascii0x03
转载请注明出处:http://www.cnblogs.com/ascii0x03/p/6375825.html