安全研究人员的工程修养
本文的目标读者是开发过一些脚本和个人项目,想进一步了解工程开发的安全研究人员。
关于工程开发
相对于我们安全研究员的个人开发,工程开发的主要特点在于:
- 需要高质量的协作手段。 一般来说,工程开发参与的开发者人数较多(>3),协作效率对工程开发的效率和质量有着直接影响。
- 项目规模较大。 工程开发的目标项目通常代码规模较大,复杂度高。
- 迭代的高质量的交付目标。 工程开发的交付目标通常是需要持续迭代的,且每次迭代的交付目标通常对质量要求较高。这对流程的规范性、代码的正确性、可维护性有着较高的要求。
这就要求工程开发需要:
- 一套高效协作的方法,如明确的代码规范、做好开发者文档建设、规范的代码提交流程等。
- 一个合适的软件架构和合理的代码结构,从而实现大量代码也可以做到井然有序,互不干扰。
- 明确开发、测试、交付的迭代流程(Devops),并定期偿还技术债。如应用敏捷开发思想等。必要时还考虑引入诸如CI/CD等工具。此外,我们还需要根据项目特点选择合适的语言。
基于上文所述的工程开发的这些特点和需要,接下来的文章将会详细阐释协作、软件架构和代码结构、交付与迭代流程、编程语言选择这四个方面。
协作
要实现高质量的协作,我们首先需要一个需求管理和任务分配的方法,以对明确需求和开发责任。在开发者在开发过程中,我们需要一个明确的代码规范,以确保代码的在团队内部的可读性。必要时,我们也需要建设开发者文档,以方便开发者之间的沟通。我们也需要明确代码提交的流程,以减少代码合并的冲突和提高代码的可追溯性。
需求管理与任务分配
当一个项目具有较多开发者时,我们可能需要明确每个具体的开发需求以及负责人。一种方式是通过 issue 来明确需求,并将 issue 赋予给某个指定的负责人负责开发,这样团队内每个人都知道都在做什么。
代码规范
代码规范的主要作用是提高代码在开发团队内的可读性、提高代码的可维护性。虽然代码的具体规范因人而异,但仍然有一些普适性的代码规范可以列举:
- 合理的命名。
- 函数和类遵循单一抽象原则。
- 在需要改进的地方标记 todo。
- 为动态类型语言所有的定义编写 Type Hint,以方便应用类型检查(Type Check)。
def some_interface(arg1: Type1, arg2: Type2) -> Type3:
some implementation
- 在接口代码中添加描述接口功能和参数详情的注释。
- 在不好理解的代码中编写注释解释其功能。
def tricky_function():
# this code do xxxx things to achieve xxx goal
some tricky code
- 正确打印 log 和抛出异常。
推荐阅读:
State-of-the-Art Shitcode Principles(强烈推荐)
开发者文档
编写开发者文档是为了减轻开发者的工作量。
这句话可能会违反大家对文档的固有印象,相信大部分人都会认为编写文档是一项占用大量精力且无趣的工作。是的,如果一个项目的开发团队小于3人,大多数开发相关的内容都可以开会对齐,编写文档的收益确实不高。
但我们试想一种场景,开发者团队具有5个人,每个人都负责一部分模块并向其他人提供接口。假如开发者在开始时都不编写文档,那他们可能随时都会被其他开发者询问其负责模块的相关的一些问题。当一个问题被询问多次的时候,那他可能就倾向于将这个问题、以及其他可能会被问到的问题统一写成文档。继续试想,某一天,这个开发团队加入了一个新的开发者,那这位新开发者需要向老开发者询问大量的问题才能够上手开发,为了避免这样的再次情况发生,开发者们以及项目负责人也会倾向于做好文档建设。
写开发者文档的需求正是这样产生的。写文档不是乱写,也不是为写而写,而是将需要同步给他人的东西像一个布告栏一样放置到文档中,以减少被问询的频率,从而使得开发者直接能够高效协作开发。如果项目开发过程中中并不存在这种情形,那文档也并不是必须的。
这就是为什么我在本节开头说编写文档是为了减轻开发者的工作量。
推荐资料:
Book, The Pragmatic Programmer
代码提交流程 (Git WorkFlow)
作为版本管理工具,Git 已经深入人心。所谓 Git Flow 实际上就是如何使用 Git 提交代码的规范。一个合适的 Git Flow 可以在多人协作的场景下有效减少代码合并的冲突、提高代码的可追溯性。
推荐阅读:
Gitflow Workflow
软件架构和代码结构
一个合适的软件架构和合理的代码结构会极大的增加代码的质量和可迭代性。接下来我们会一一阐释如何选择一个合适的软件架构以及合理的代码结构。
软件架构
首先,这里的软件架构不是讨论如何定义函数、如何定义类,这些属于代码结构的范畴。软件架构是一个宏观上的概念。它的一些正例是 MVC(一种单体分层架构)、微服务(一种分布式领域隔离的架构)。一些反例是工厂模式、生产者消费者模式。
一个适合的软件架构可以有效将开发者组织起来,降低不同团队的开发者之间的耦合,提升同一团队开发者的内聚。如果开发团队是按照诸如“前端”、“后台”、“数据库”这样以技术方向进行组织,那你可能比较适合分层架构这种以技术方向为导向的架构,如分层架构。如果开发团队是按照诸如“逆向”、“数据分析”、“数据收集”进行组织,那服务架构这种基于领域进行隔离的架构更为适合。
“Software architecture is about comprise.”
我是非常同意这一句话的。没有最好的架构,只有最适合的架构。
如果你选择了单体架构(如分层架构、插件架构),那你可能需要对程序的可用性、可扩展性、可恢复性等架构特性做一些妥协。如果你选择了基于服务的分布式架构,你可能需要对一致性、效率、可测试性做一些妥协。
因此,在选择自己的架构之前,首先需要明确自己项目对架构有哪些需求(如可用性、可扩展性等),明确项目是基于技术方向隔离还是基于领域隔离。明确了这些东西之后,再去选择最合适的架构。
推荐资料:
Book, Fundamentals of Software Architecture
代码结构
设计代码结构的目标是通过抽象实现模块化(即高内聚,低耦合)。
然而,抽象是有代价的,抽象的代码的复杂性必然会变高。举个例子,假设说程序的业务逻辑层需要对数据库进行增删改查,最简单的实现就是直接使用相关 API 进行增删改查。但是如果我们要支持多种不同数据库,那我们可能就需要对数据库的接口进行抽象,实现一个专门的工厂或者应用依赖注入来创建具体的数据库访问实例。显然,这降低了数据库访问与业务代码的耦合,却也增加了复杂性。
代码结构设计的一个经典反例就是“过早抽象”,即在项目开发初期就绞尽脑汁思考一个“最合适的”代码结构。“过早抽象”会极大的增加项目启动的成本。况且,项目需求通常是动态的,这些经过深思熟虑的“过早抽象”大概率也都是不合适的。
代码结构设计的最佳实践是在代码抽象与代码复杂度之间寻找一个平衡,既大致实现了模块化的目标,又没有令代码变得特别复杂。同时,又能够在需求和设计发生变动的时候能快速的对代码架构进行更改。
记住,需求是持续变动的,代码结构也是持续变动的,“过早抽象”是没有意义的。
推荐资料:
Book, Fundamentals of Software Architecture
迭代与交付
为了尽可能的确保高质量的迭代和交付,我们可能会需要一套名为 Devops 的流程来对应用生命周期进行管理。在需求不明确或者需求时常变动的情形,我们可能还需要应用敏捷开发思想。此外,当项目规模较大,系统较为复杂时,我们也可以通过部署 CI/CD 使得项目的集成和交付更加平滑。
⚠️ Warning:本部分的所有内容都需要秉持着根据实际情况仅必要时采用的原则。如果一个项目的需求比较固定,那应用敏捷开发是没有意义的。如果项目的规模较小,应用 CI/CD 也是一个巨大的浪费。
Devops
相信 Devops 这个名词对于不少安全研究员既熟悉,又陌生。熟悉可能是因为在安全研究过程中,这个词大概率会遇到,因为它是安全技术一个非常重要的应用场景(Devsecops)。陌生,大概是因为平时做安全研究很少有需要参与到 Devops 中来,因此缺少这方面的实践经验。
那到底是什么呢?我觉得它的字面已经表达的很形象了:一种紧密的结合开发(Development)与(Operation)的项目迭代流程。
它明确了整个项目需求收集、开发、测试、交付、运维、反馈的一整套流程。使得项目的交付质量、以及可维护性得到了极大的提高。
试想一种场景,一个团队合作开发一个二进制分析的项目,二进制分析想要做好投入可不小,在开发之前,我们毫无疑问是要明白需要我们为什么要开发这个项目,他要解决怎样的问题。明确了这些需求之后,我们才能开始进行架构设计和开发。
在代码开发完成后,我们也必须要明确代码在合并之前要通过哪些测试(如代码风格测试、静态 bug 扫描、单元测试、Code Review、集成测试等)。如果不明确这些测试,那代码的质量将无法得到保证,代码很容易成为屎山,变得难以维护。
在交付、运维这个周期里,我们需要通过 log 聚合、运行环境监控监控以维持对软件的可观测性,以及时发现 bug(比如 log 中打印了 error,发现来异常,这里面其实还有如何让一个 bug 可复现的问题)。我们也需要持续收集用户反馈(虽然有时候用户就是我们自己)。
最后,我们根据收集到的信息和用户反馈,帮助我们明确下一个迭代周期的需求,反思需要还的技术债,并进行下一轮的迭代和改进。
通过以上例子,我们可以明白,Devops 不是一个单纯的口号,而是确确实实能够提升软件质量和可维护性的一套流程。
推荐资料:
What is devops
敏捷开发
敏捷开发是目前最为常用的软件迭代思想之一。这里提敏捷开发主要是想强调敏捷开发不是快速开发。敏捷开发的主要思想通过缩短 Devops 的周期,减少每个周期所做的改动来适应需求的快速变动和迭代。
不少人把敏捷开发简单的理解为快速开发,认为敏捷开发出来的代码质量不行,这其实对敏捷开发的一种误解。
在敏捷开发过程中,开发者仍需要经历 Devops 的各个流程,而开发只是这个流程中的一个步骤。因此,敏捷开发其实是可以提高代码质量的,因为它的迭代周期更短,能够更及时的复盘、反思,并清偿技术债。
推荐资料:
What is agile development
CI/CD
持续集成/持续交付(CI/CD)是缩减软件的集成和交付周期,最终甚至可以将集成和交付的周期缩短至 commit level。这样做的主要好处是可以让集成和交付更加平滑,有效避免了软件集成和交付日的 bug 喷发。
集成和交付本身是有成本的,要想做的持续的集成和交付,就必须降低集成和交付的成本。因此,CI/CD 通常跟自动化联系在一起,这也是 CI/CD 在项目中的通常表现为一个类似于:
“静态代码检查 -> build -> 单元测试 -> 集成 -> 集成测试”的自动化 Pipeline 的原因。
语言选择
语言没有好坏,只有是否适合。我个人比较推荐三种语言:Python、Kotlin、Rust。这三个语言基本可以胜任安全研究过程中大多数开发需求。
总体来说:
- Python 属于万能语言,适合几乎所有执行效率不敏感且业务逻辑不是特别复杂的情形;
- Kotlin 是现代版本的 Java,继承了 Java 的生态,适合复杂业务逻辑的开发;
- Rust 具有媲美 C++ 的效率以及强大的各类 Checker,适合用于开发性能敏感型、以及 Bug 敏感型项目(如 fuzzing)。
接下来,我会介绍我理解的各个语言的优缺点,并结合这些优缺点具体阐释其适合的场景。
Python
优点:
- 生态成熟,有丰富的 lib,可以快速完成各种需求的开发。
- 语法简单,便于上手,开发效率高。
- 解释执行,无需编译,便于代码的快速修改和动态 debug。
缺点:
- 类型系统较弱,难以构建相对好用的类型检查能力。在编写较为复杂的项目时,静态查 bug 的体验较差。
- 由于解释执行外加 GIL 等因素的叠加影响,Python 的运行效率较低。
- 包管理系统目前比较初级。
总体来说,Python 比较万能,可以在大多数场景中进行使用,尤其适合在需求比较多样、开发效率要求较高的场景使用。不过,开发时建议标记 type hint,以方便使用 type checking 消除 bugs。不过,由于 Python 的动态类型、较弱的类型系统以及较为自由的语法,使得其开发可维护的高复杂性的项目的难度较高。
Kotlin
优点:
- 兼容 Java,可以直接使用 Java 的生态,可以与 Java 混编,支持 Java 代码一键转换为 Kotlin。
- 属于比较现代的语言,具有 Null Free、函数式编程等相关支持。拥有丰富的语法糖(如属性访问等),编码体验极佳。
- 具有较为成熟的包管理系统。
- 类型安全,可以在编译时利用类型检查消除大多数 bugs。
缺点:
- 编译型语言,每次修改代码都需要重新编译,对动态调试效率有一定影响。
- 第三方 IDE 和编辑器的对支持较差,开发重度依赖官方 IDE IntelliJ,IntelliJ 有很多的 analyses 要跑,非常吃配置(有时候 M1 的 Mac 跑起来都卡)。
由于 Kotlin 可以直接使用 Java 的生态,且属于较为现代的语言,因此它是 Java 的很好的一个替代品,基本解决了 Java 缺少现代语法糖,以及各种 Null Pointer Exception 等痛点。Kotlin 很适合应用在原来 Java 适合应用的场景,如复杂业务逻辑的开发等。
Rust
优点:
- Native,执行效率可以媲美 C++。
- 编译器的 Type Check、Borrow Check 将大多数的代码 bug 在编译时消灭,只有极少数的 bug 会被留到运行时触发。
- 属于比较现代的语言,基本革除了 C++/C 语法上的一些弊病,具有 Null Free、函数式编程等相关支持。
- 具有较为成熟的包管理系统。
缺点:
- 学习曲线较为陡峭,上手较慢,会的人也比较少。
- 语言较为年轻,生态目前还不够完善。
- borrow check 等 checking 会使得一些代码没办法写,这个时候只能 unsafe 的后门编写。
Rust 是一门较为年轻的语言,很好了甩除了 C++/C 的历史包袱。因此可以用来作为 C++ 的替代品来开发一些对效率要求较高的代码,如 Fuzzer 等。