2017 年 3 月

第 32 卷,第 3 期

Visual Studio - 使用 Visual Studio 对源代码文件进行哈希处理以确保文件完整性

作者 Mike Lai | 2017 年 3 月

对所有编译的软件语言来说,将人类可读代码转换成计算机可读代码都是一项软件保障挑战: 用户如何有信心相信在其计算机上运行的软件程序是根据开发者创建的同一源代码文件生成的呢? 这不一定,即使源代码文件经过行业专家评审,也不例外,因为可能出现开放源代码软件的情况。软件保障的核心是信任经过评审的源代码文件是生成可执行文件的相同源代码文件。 

在编译和链接过程中,使用特定编程语言(C#、C++、Objective C、Java 等)编写的一组源代码文件被转换成二进制可执行文件,以供在特定体系结构(如 x86、x64、ARM)的计算机上运行。但这种转换可能不具有决定性作用。两组不同的源代码文件可能被转换成两组位完全相同的可执行文件。有时,这是有意而为之。源代码文件内空格或文本注释不一致不得影响编译器生成的二进制代码。另一方面,同一组源代码文件也可能会因不同的编译过程而被转换成不同的可执行文件。无论属于上述哪种情况,问题都在于确定性,即无法确定拥有的文件是否就是所需的文件。

为了解决这个问题,不妨在编译过程中使用 Visual Studio 编译器对源代码文件进行哈希处理。将编译器生成的哈希值与经过检查的源代码文件生成的哈希值进行匹配,可以验证可执行代码是否的确是由特定的源代码文件生成而来。这显然会让用户很受益(实际上,如果其他编译器的供应商也采用了类似方法,那么用户会进一步受益)。本文介绍了用于选择哈希算法的新 Visual Studio 开关、此类哈希可能适用的应用场景,以及如何使用 Visual Studio 生成源代码哈希值。

在编译过程中生成强哈希值

程序数据库 (PDB) 文件是一个单独数据文件,存储用于调试二进制可执行文件的信息。Microsoft 最近将其各种编译器文件哈希运算(如 PDB 文件中嵌入的源哈希值)更新为使用强加密算法。   

本机代码编译器:Visual Studio 2015 本机 C/C++ 编译器 cl.exe 随附一个新开关 /ZH:{MD5|SHA_256},用于为编译器选择其他哈希算法,从而对源代码文件进行哈希处理。默认开关为 MD5,虽然已知其更容易导致冲突,但仍采用默认开关,因为从计算层面来讲它的哈希值生成成本更低。使用新的开关,编译器可以实现密码强度高于 MD5 的 SHA-256 选项。

如果源代码文件的 SHA-256 哈希值与二进制可执行文件的 PDB 文件中存储的 SHA-256 哈希值一致,就可以确定可执行文件是由相同的源代码文件编译而成,这样所有利益干系人便可以对二进制可执行文件有信心。实际上,二进制可执行文件的 PDB 文件中存储的一组 SHA-256 哈希值全都成为二进制可执行文件的“生成证明”中的标识符,因为这些标识符由“生成”二进制可执行文件的编译器进行注册。  

使用调试接口访问 SDK (bit.ly/2gBqKDo),可以轻松创建简单的工具,如调试信息转储程序 cvdump.exe(可从 bit.ly/2hAUhyy 中获取此程序及其源代码)。可以使用 cvdump.exe 的 -sf 开关查看模块(使用本地生成计算机中的完整路径名称)及其 MD5 或 SHA-256 哈希值的列表,如图 1 中的命令窗口所示。

使用 cvdump.exe 查看模块及其哈希值
图 1:使用 cvdump.exe 查看模块及其哈希值

使用旧版 cvdump.exe 查看同一 PDB 文件时,我看到的文字是“0x3”,而不是“SHA_256”。“0x3”值是“SHA_256”的枚举值,更新后的 cvdump.exe 知道如何进行解析。它也是调试接口访问 SDK 的 IDiaSourceFile::get_checksumType 方法返回的同一枚举值。

托管代码编译器:默认情况下,Visual Studio 2015 托管代码 C# 编译器 csc.exe 使用 SHA-1 加密算法计算源文件校验和哈希值,以存储在 PDB 文件中。然而,csc.exe 现在支持使用新的可选“/checksumalgorithm”开关来指定 SHA-256 算法。若要切换到 SHA-256 算法,请使用此选项编译当前目录中的所有 C# 文件,然后将调试信息(包括源文件列表和 SHA-256 哈希值)放入 PDB文件中:

csc /checksumalgorithm:SHA256 /debug+ *.cs

可从 github.com/dotnet/roslyn 中获取属于 .NET 编译器平台 (Roslyn) 开放源代码项目的 csc.exe。有关对文件中 SHA-256 源文件调试校验和算法命令行选择器的支持,请访问 bit.ly/2hd3rF3

Visual Studio 2015 csc.exe 只与 Microsoft .NET Framework 4 或更高版本的可执行文件兼容。另一个用于生成低于版本 4 的可执行文件的 Visual Studio 2015 .NET Framework 编译器不支持 /checksumalgorithm 开关。

托管代码 PDB 文件存储数据的方式不同于本机代码 PDB 文件。可使用 Microsoft DiaSymReader 互操作接口和实用工具来读取托管代码 PDB 文件,而不是使用调试接口访问 SDK。可从 bit.ly/2hrLZJb 中以 NuGet 包的形式获取 Microsoft DiaSymReader。   

Roslyn 项目包括 pdb2xml.exe 实用工具,可从 bit.ly/2h2h596 中获取此工具及其源。此实用工具以 XML格式显示 PDB 的内容。例如,图 2 中的各段列出了用于编译托管代码可执行文件的 C# 源代码文件。  

以 XML 格式显示托管代码 PDB
图 2:以 XML 格式显示托管代码 PDB

checkSumAlgorithmId 字段中的“8829d00f-11b8-4213-878b-770e8597ac16”GUID 表明,校验和字段中的值是名称字段中引用的文件的 SHA-256 哈希值。可移植 PDB 格式规范 v0.1 (bit.ly/2hVYfEX) 中定义了此 GUID。  

编译器对 SHA-256 的支持

以下 Visual Studio 2015 编译器支持对源代码文件进行 SHA-256 哈希处理:      

  • cl.exe /ZH:SHA_256
  • ml.exe /ZH:SHA_256
  • ml64.exe /ZH:SHA_256
  • armasm.exe -gh:SHA_256
  • armasm64.exe -gh:SHA_256
  • csc.exe /checksumalgorithm:SHA256

可在 Visual Studio 2015 的“VS2015 开发者命令提示符”命令窗口中创建这些编译器。

不面向 Windows 平台的编译器通常不使用 PDB 文件存储其调试信息。这些编译器通常在编译期间同时生成两个可执行文件,一个是未删除源信息的可执行文件,另一个是已删除源信息的可执行文件 (bit.ly/2hIfvx6)。所有调试信息都存储在未删除源信息的可执行文件中,而已删除源信息的可执行文件则不包含任何详细的调试信息。未删除源信息的可执行文件可能适合存储可执行文件的已处理源代码文件的 SHA-256 哈希值。我们正打算联系其他这些编译器的创建者,确定最适合其编译器的方法,以便使用这些编译器的非 Windows 软件(如 Office for Android、Office for iOS 或 Office for Mac)可以和 Windows 软件一样受益。

用例应用场景   

现在,我们来看一下源文件哈希值可能适用的一些应用场景。       

检索可移植可执行 (PE) 二进制文件的已编入索引源文件:Ssindex.cmd 脚本 (bit.ly/2haI0D6) 是一种实用工具,可用于生成签入源控件的(已编入索引)源文件列表,以及每个文件的版本信息,以供存储在 PDB 文件中。如果 PDB 文件包含此版本控制信息,可以结合使用 srctool 实用工具 (bit.ly/2hs3WXY) 及其 -h 选项来显示信息。由于已编入索引的源文件也将其哈希值嵌入 PDB 文件,因此这些哈希值可用于在检索期间验证源文件,如知识库文章 3195907 (bit.ly/2hs8q0u)“How To Retrieve Indexed Source Files of a Portable Executable Binary File”(如何检索可移植可执行二进制文件的已编入索引源文件)中所述。 具体来说,如果哈希值不一致,则表明 PE/PDB 对生成期间或源控件系统中的某个环节可能出现了问题。这可能有必要执行进一步调查。相比之下,如果哈希值一致,则充分表明检索到的已编入索引源文件是用于编译 PE/PDB 对。        

匹配源文件静态分析器生成的哈希值:现在,使用自动工具来评估软件质量是常事,就像 Microsoft 安全开发生命周期 (SDL) 针对实现阶段建议的一样 (bit.ly/­29qEfVd)。具体来说,源文件静态分析器用于扫描目标源代码文件,以评估软件质量的许多不同方面。这些静态分析器通常在扫描目标源代码文件后立即生成相应的实时结果。在静态分析器扫描各个源代码文件时,也是生成每个在扫描源代码文件的强哈希值 (SHA-256) 的绝佳机会。实际上,bit.ly/2ibkbwz 中开放源代码项目提出了静态分析结果交换格式 (SARIF),这种格式提供了静态分析结果中的特定位置,以供静态分析器生成扫描的目标源代码文件及其 SHA-256 哈希值。 

以 PE 文件为例,假设可获得以下内容:

  1. 由编译器生成的相应 PDB 文件中的编译源文件哈希列表。
  2. 由静态分析器生成的相应静态分析结果中的扫描源文件哈希列表。

在此应用场景中,可以评审并验证这两个文件哈希列表是否匹配。如果匹配,表明静态分析器已扫描源文件来评估某方面的质量,无需重新扫描源文件。以前没有文件哈希列表,可能就需要重新扫描,以确保静态分析器进行了正确的评估。  

在软件更新或修补程序开发过程中更快速地执行健全性检查:如果需要发布软件更新来修复源文件静态分析器在已发布产品中发现的质量问题,静态分析器应报告待定更新程序的源代码文件中不存在发现的质量问题。这个报告至少将确认更新程序能否有效解决原始质量问题。也就是说,它将验证软件更新的预期用途。如果需要,你或安全评审员可以执行下列步骤来实施快速验证: 

  1. 确认原始静态分析器报告是否发现相关质量问题。
  2. 确认原始静态分析器报告是否包括存在质量问题的源文件的哈希值。
  3. 将原始静态分析器报告中发现的文件哈希值与已发布产品版本的源文件的哈希值进行匹配。
  4. 使用同一静态分析器扫描更新程序的源代码文件,生成更新后的静态分析器报告。
  5. 确认更新程序的静态分析器报告中是否不存在之前发现的质量问题。
  6. 将更新后的静态分析器报告中的文件哈希值与更新程序的源文件的哈希值进行匹配。

在执行这些验证步骤期间,无需访问原始发布产品或更新程序的实际源代码文件。 

构造两个软件版本之间的源代码增量:评审一组完整的源代码可能需要一些时间。然而,在某些情况下,不一定非要在更改源代码后评审全部源代码。因此,可能要求只评审源代码增量。这样的要求当然合理,因为重复分析上次评审后没有变化的所有部分并不合理。      

以前没有源代码文件的密码强度高的哈希值,很难精确构造增量子集。即使你有增量子集可以提供,行业专家也可能对你能否准确创建增量子集没有什么信心。但现在情况已不再如此。借助源代码文件的密码强度高的哈希值,可以执行下列步骤来创建增量子集:

  1. 获取原始产品版本的所有源代码文件的哈希值池(例如:池 X)。
  2. 精确复制文件目录(例如:目录 A),其中包含后续产品版本的源代码登记,将根据其构造增量子集。
  3. 准备用于仅保留增量文件子集的最终文件目标文件夹(例如:目录 B)。
  4. 整理目录 A 中的所有文件:
  5.         a.如果文件的哈希值与池 X 中的哈希值一致,什么也不做,匹配下一个文件。
  6.         b.如果文件的哈希值与池 X 中的哈希值不一致,将文件复制到目录 B 中,然后匹配下一个文件。
  7. 确认目录 B 中所有文件的哈希值与后续产品版本的源文件的相应哈希值一致。  
  8. 让目录 B 的内容成为后续产品版本的增量源文件子集。     

生成哈希值

现在,我们来了解一下如何使用 Visual Studio 编译器对文件进行哈希处理。为此,我将以联机 Visual Studio 文档 (bit.ly/2haPupF) 中的“Hello, World”应用程序创建应用场景为例:

  1. 介绍在输出 PDB 文件中在何处可以找到编译的源文件的哈希值
  2. 使用 certutil 工具 (bit.ly/2hIrnPR) 计算源文件哈希值,与 PDB 文件中的哈希值进行匹配。       

首先,我在 Visual Studio 2015\Projects 文件夹中新建了一个 Win32HelloWorld 应用程序项目。在这个 Win32HelloWorld 项目中,只有一个 C++ 源文件 Win32HelloWorld.cpp,如图 3 所示。

Win32HelloWorld.cpp
图 3:Win32HelloWorld.cpp

如你所见,Win32HelloWorld.cpp 包含用于显示“Hello”文字的主函数。

生成 Win32HelloWorld 项目后,我在 Visual Studio 2015\Projects\W32HelloWorld\x64\Debug 文件夹中生成了 W32HelloWorld.exe 和 W32HelloWorld.pdb 文件。

对 W32Hello­World.pdb 文件结合使用 cvdump 工具和 -sf 选项在输出中显示 Win32HelloWorld.cpp 文件及其 MD5 哈希值,如图 4 所示。

显示 Win32HelloWorld.cpp 及其 MD5 哈希值的 cvdump 输出
图 4:显示 Win32HelloWorld.cpp 及其 MD5 哈希值的 cvdump 输出

此哈希值是 MD5,因为 MD5 是 Visual Studio 2015 编译器 cl.exe 的默认算法。若要将源文件哈希算法切换成 SHA-256,我需要向 cl.exe 提供 /ZH:SHA_256 选项。为此,我可以在 Win32HelloWorld 项目“属性”页上的“其他选项”框中添加“/ZH:SHA_256”,如图 5 所示。

将源文件哈希算法切换成 SHA-256
图 5:将源文件哈希算法切换成 SHA-256

在 Visual Studio 中重建后,我在 Visual Studio 2015\Projects\W32HelloWorld\x64\Debug 文件夹中生成了 W32HelloWorld.exe 和 W32HelloWorld.pdb 的新 PE/PDB 对。现在,对新的 W32HelloWorld.pdb 文件结合使用 cvdump 工具和 -sf 选项在输出中显示 Win32HelloWorld.cpp 文件及其 SHA-256 哈希值,如图 6 所示。

显示 Win32HelloWorld.cpp 及其 SHA-256 哈希值的 cvdump 输出
图 6:显示 Win32HelloWorld.cpp 及其 SHA-256 哈希值的 cvdump 输出

现在,我可以返回到 Visual Studio 2015\Projects\W32HelloWorld\W32HelloWorld 文件夹中的 W32HelloWorld.cpp 文件,查看它的 SHA-256 哈希值。对于 SHA-256,对 Win32HelloWorld.cpp 文件结合使用 certutil 工具和 -hashfile 谓词生成 SHA-256 哈希值,如图 7 所示。

使用 Certutil 生成 SHA-256 哈希值
图 7:使用 Certutil 生成 SHA-256 哈希值

很显然,此哈希值与 W32Hello­World.pdb 文件中记录的 SHA-256 值一致。这充分表明 W32HelloWorld.exe 应用程序确实是按预期由 Win32HelloWorld.cpp 文件编译而成。

若要详细了解适用于本机代码和托管代码 PE/PDB 文件对的相关公共工具,请参阅知识库文章 3195907 (bit.ly/2hs8q0u)“How To Retrieve Indexed Source Files of a Portable Executable Binary File”(如何检索可移植可执行二进制文件的已编入索引源文件)。

总结

我希望通过本文你可以了解到,更紧密地关联源代码文件和用其编译的 PE 文件可能会带来的一些好处。可以在编译过程中使用最强的可用哈希算法 SHA-256 让编译器对源代码文件进行哈希处理,从而更紧密地关联两者。实际上,编译器生成的源代码文件的实际哈希值成为了用于编译可执行文件的源代码文件的唯一标识符。

了解这些唯一标识符的值后,就可以在不同软件开发生命周期计划中使用它们跟踪、处理和控制与特定可执行文件有着紧密联系的源代码文件,从而让最终用户对可执行文件更有信心。


Mike Lai 刚刚迎来他在 Microsoft 工作的第 20 个年头。他很感谢 Microsoft 能够提供各种机会来推动许多产品在功能及工程方面取得进步。他想要感谢可信任计算部门现任管理层能够耐心等待他的思想变成熟并逐步融入已发布的产品,另外还要感谢他们支持加入信息和通信技术安全标准组织。

衷心感谢以下 Microsoft 技术专家对本文的审阅: Scott Field、Mike Grimm、Sue Hotelling、Ariel Netz、Richard Ward 和 Roy Williams