如何在重新构建C#应用程序时始终生成字节级别相同的.exe文件?

20

首先,让我简单介绍一下为什么我提出这个问题:

我目前在从事严格受监管的行业,因此我们的代码受到官方测试机构的严密审核。这些测试机构希望能够构建代码并生成一个完全相同的 .exe 或 .dll 文件(显然不更改任何代码!)。他们检查所创建的可执行文件的 MD5 和 SHA1 值以确保其相同。

迄今为止,我主要使用 C++ 进行编程,在进行一些项目设置调整后,我设法使项目每次重建时都具有相同的 MD5/SHA1 值。现在我正在使用 C# 进行一个项目,但在重新构建后很难使 MD5 值匹配。我知道在文件的 PE 头中有 "时间戳",它们已经被清零为 0。我还知道 .exe 的 GUID,它也已经被清零为“00 00 00...”等。但是文件仍然无法匹配。

我使用 CFF Explorer 查看和编辑 PE 头以删除时间和日期戳。使用二进制比较工具后,.exe 中只有 2 个字节块是不同的(两者都非常小)。

一个不一致的块出现在二进制代码之前,该代码在 ASCII 中详细描述了 *Project*\obj\Release\xxx.pdb 文件的路径。

编辑:现在已知这是 *.pdb 文件的 GUID,但我仍然不知道是否可以修改它而不会导致任何错误!?

另一个块出现在函数名称中间,例如(典型部分)AssemblyName.GetName.Version.get_Version.System.IO.Ports.SerialPort.Parity.Byte.<PrivateImplementationDetails>{ 然后是不同的代码块:

4A134ACE-D6A0-461B-A47C-3A4232D90816

接着是:

"}.ValueType.__StaticArrayInitTypeSize=7.$$method0x60000ab-1.RuntimeFieldHandle.InitializeArray`... 等等。

欢迎提出任何想法或建议!

6个回答

5
更新:Roslyn似乎有一个/feature:deterministic编译器标志,用于可重现构建,尽管它还没有完全工作

您可以通过禁用PDB生成来消除调试GUID。如果无法实现,则将GUID设置为零也是可以的 - 只有调试器会查看该部分(您将无法再调试程序集,但它应该仍然可以正常运行)。

PrivateImplementationDetails稍微有点困难 - 这些是编译器为某些语言结构(数组初始化程序,使用字符串的switch语句等)生成的内部辅助类。因为它们仅在内部使用,所以类名并不真正重要,因此您可以将连续编号分配给它们。

我会通过浏览#Strings元数据流并将所有形式为“<PrivateImplementationDetails> {GUID}”的字符串替换为“<PrivateImplementationDetails> {连续编号,填充到与GUID相同的长度}”来完成此操作。

#Strings元数据流只是元数据使用的字符串列表,以UTF-8编码并用\0分隔;因此,一旦知道可执行文件中#Strings流的位置,查找和替换名称应该很容易。

很遗憾,“元数据流头”包含这些信息的位置在文件格式中相当深。您需要从NT Optional Header开始,找到指向CLI Runtime Header的指针,使用PE部分表将其解析为文件位置(它是一个RVA,但您需要文件内的位置),然后转到元数据根并读取流头。

好的,如果通过禁用PDB生成(或将其清除为全0)可以摆脱GUID,那么问题1就解决了。问题2似乎要难得多。你是说我必须通过IL并更改其中的值吗?还是直接访问已编译的*.exe并手动设置字节? - Siyfion
由于我在资源注入工具上的工作,我会选择*.exe打补丁的解决方案。进行ILDASM/ILASM往返以替换类名也是可能的。 - Daniel
你有没有在不久的将来发布这个补丁工具的机会呢? ;) - Siyfion
嗨,丹尼尔,我只是想知道你的小工具有没有什么新消息,它可能会让这个过程更容易吗? - Siyfion
没有好消息。我没有时间继续编写那个工具。目前我只有一个概念验证,有时会生成损坏的程序集。 - Daniel

2
我不确定这一点,只是想到一个可能性:您是否使用任何匿名类型,编译器可能会在后台生成名称,每次编译器运行时名称可能会不同?这只是我想到的一种可能性。可能需要请教Jon Skeet ;-)
更新:您还可以使用反编译工具Reflector进行比较和反汇编addins

不,应用程序中没有使用任何匿名类型,虽然这是一个好想法!;) - Siyfion
关于Reflector的比较,不幸的是,选择用于比较的工具并不是我,必须是完全匹配的MD5。 - Siyfion

2
关于PDB GUID问题,如果您指定在Release版本编译时不生成PDB文件,那么二进制文件是否仍包含PDB文件系统GUID?
要禁用PDB生成:
1. 在“解决方案资源管理器”中右键单击项目,选择“属性”。 2. 从左侧菜单中选择“生成”。 3. 确保配置选项为“Release”(您仍需要进行调试)。 4. 单击右下角的“高级”按钮。 5. 在“输出/调试信息”下,选择“无”。
如果您是从控制台构建,请使用/debug-获得相同的结果。

我目前只是为了评估而使用Visual C# Express,您知道在这个版本中是否可以关闭*.pdb生成吗? - Siyfion
你可以,我会添加指示,因为该选项有点隐藏。 - Justin R.

0

你说过经过一些项目调整,你能够让C++应用程序重复编译成相同的SHA1/MD5值。我和你处于同样的境地,我们所在的行业需要与第三方测试实验室合作,确保可执行文件可以被重复构建。

在研究如何在VS2005中实现这一点时,我看到了你在这里发布的帖子。你能否分享一下你所做的项目调整,以便让C++应用程序能够一致地生成相同的SHA1/MD5值?这将对我自己以及其他有类似需求的人非常有帮助。


1
当然,虽然这是我脑海中的想法!在发布模式下执行以下操作:
  • 禁用清单文件的生成(解决方案属性->链接器->清单文件)
或者
  • 更改清单设置(解决方案属性->清单工具->输入和输出),以便将“嵌入清单”设置为“否”。
还要确保所有调试信息都关闭了发布版本。然后,您只需要从PE文件头中删除TimeAndDateStamp即可。(尝试谷歌搜索“CFF Explorer”)
- Siyfion
哎呀,手动操作文件头?这简直是等着发生人为错误的灾难啊。你知道有没有命令行实用程序可以自动化、可靠和可重复地完成这项工作吗? - Tom

0
使用ildasm.exe完全反汇编两个程序并比较IL。然后,您可以使用基于文本的方法“清理”代码,并(可预测地)重新编译它。

0

看一下这个问题的答案,特别是第三个答案提供的外部链接。

编辑:

我实际上想链接到这篇文章。


这是一个用于比较二进制文件的差异工具的链接。 - Vinay Sajip
我实际上找不到任何可用的 Dumpbin.exe 版本,但除此之外,似乎唯一的区别应该是日期和时间(我已将其清除为 0)、GUID(我已将其清除为 00 00...等等)、程序集版本(应该相同?)以及强散列(如果其他所有内容都相同,则应该相同!)。所以我认为下一步是使用 Ildasm.exe 尝试弄清楚 MSIL 代码是否有任何不同! - Siyfion
抱歉造成困惑。我编辑了我的帖子,指向正确的文章。请在那里查找更多信息。 - Frank Bollack
啊哈,好的,从那个链接来看,似乎其中一个不同的块是 *.pdb 文件的 GUID。虽然我仍然找不到一种将其设置为特定值的方法?另一个差异,我还在研究中。 - Siyfion

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接