当请求反射API覆盖System.String.Empty时会产生什么影响?

61

我偶然发现了这段代码:

static void Main()
{
    typeof(string).GetField("Empty").SetValue(null, "evil");//from DailyWTF

    Console.WriteLine(String.Empty);//check

    //how does it behave?
    if ("evil" == String.Empty) Console.WriteLine("equal"); 

    //output: 
    //evil 
    //equal

 }

我很惊讶这段代码怎么可能编译成功。我的推理是:

根据MSDN的说明,String.Empty是只读的,因此更改它应该是不可能的,并且编译应该以“无法将静态只读字段分配给”或类似错误结束。

我认为基类库程序集受到某种保护和签名措施,以防止恰好发生这种攻击。下次有人可能会更改System.Security.Cryptography或另一个关键类。

我认为基类库程序集在.NET安装后由NGEN编译,因此更改String类的字段应该需要高级黑客技术,而且更难实现。

然而,这段代码编译并运行了。请有人可以解释一下我的推理哪里出了问题吗?


22
@Andre: 现在无法修复此问题,因为人们依赖于完全可信的代码能够使用反射从持久化状态重新填充只读字段。没有理由“修复”它,因为它一开始就没有问题。如果您不希望代码更改这些字段,请不要完全信任更改这些字段的代码 - Eric Lippert
1
很好,@Eric,但你告诉我完全可信的代码更改具有特殊含义的值是完全可以接受的?对不起,但我不能同意这一点。使用代码访问安全性来防止它确实与此无关。 - Andre Luus
5
@Andre:无论是可接受的还是不可接受的都不重要;这是可能的。我已经说了大约十几次,但似乎没有人理解:完全信任意味着完全信任。完全信任代码可以获取指向运行时执行机器代码的指针,并为安全系统重新编写它!那是一件好事吗?不是。但是,没有办法阻止它,因此也没有办法阻止任何事情,因为安全系统正是强制执行这些规则的。如Raymond Chen所说,这是一个墙与梯子的游戏。 - Eric Lippert
3
@Andre:我们不会说“好吧,伙计们,如果你信任了一些恶意代码,也别担心,因为至少你知道它不能改变String.Empty”。为什么要设置一个防止这种情况发生的墙呢?当恶意完全信任的代码有比那堵墙高十倍的梯子时,还有什么意义呢? - Eric Lippert
7
@Andre:确实,可访问性约束是为了程序员而设立的。它们存在是为了让您记录您的意图以便您的同事能够理解,并且提供一些工具使得程序的约束条件得到合理的满足。同时,它们也会自我记录元数据并告诉您的客户哪些部分可以被视为经过测试和稳定的、哪些是内部实现细节。这些都是它们的职责。如果您需要强制执行安全要求,则必须使用其他机制。 - Eric Lippert
显示剩余7条评论
5个回答

52
一个静态只读字段是不能被赋值的。 你并没有对它进行赋值。你正在调用System.Reflection 命名空间中的公共函数。编译器没有理由对此抱怨。
此外,typeof(string).GetField("Empty")可以使用用户输入的变量,因此在所有情况下编译器都无法确定GetField的参数是否最终为 "Empty"
我认为你想让Reflection看到该字段被标记为initonly,并在运行时引发错误。我可以理解你的期望,但是即使写入到initonly字段也具有某些应用于白盒测试的功能。
NGEN没有影响的原因是你没有修改任何代码,只是数据。数据与.NET一样存储在内存中,就像其他语言一样。本地程序可能会使用只读内存段来存储字符串常量之类的东西,但是指向字符串的指针通常仍然是可写的,这就是这里发生的事情。
请注意,您的代码必须在完全信任的情况下运行才能以这种可疑的方式使用反射。此外,更改仅影响一个程序,这不是任何安全漏洞(如果您在进程内运行恶意代码并具有完全信任,则设计决策是安全问题,而不是反射)。
进一步注意,mscorlib.dll中的initonly字段的值是.NET 运行时的全局不变量。 在破坏它们之后,您甚至无法可靠地测试不变式是否被破坏,因为检查当前值的System.String.Empty的代码也已经破坏了其不变性。开始违反系统不变量,就再也不能依赖任何东西。
通过在.NET规范中指定这些值,编译器可以实现大量的性能优化。 其中一个简单的优化是:
s == System.String.Empty

并且

(s != null) && (s.Length == 0)

两者等效,但后者速度更快(相对而言)。

此外,编译器可以确定

if (int.Parse(s) > int.MaxValue)

如果条件判断始终为假,就会生成一个无条件跳转到else块(它仍然需要调用Int32.Parse以具有相同的异常行为,但可以删除比较操作)。

System.String.Empty 在BCL实现中也被广泛使用。如果你覆盖了它,可能会发生各种疯狂的事情,包括泄漏到程序外的损坏(例如,你可能会写入使用字符串操作构建的文件名...当字符串损坏时,你可能会覆盖错误的文件)。


而且这种行为在.NET版本之间很容易不同。通常情况下,当发现新的优化机会时,它们不会回溯到先前版本的JIT编译器中(即使它们被回溯到了,也可能存在在实现回溯之前安装的版本)。特别是,String.Empty相关的优化在.NET 2.x和Mono以及.NET 4.5+之间明显不同。


5
如果你有“另一个软件部分”,并且不信任开发者,请使用部分信任加载该子模块。你正在谈论给一个邪恶的开发者无限制的权限,不要这样做。使用.NET的“代码访问安全性”来仅授予执行任务所需的权限。 - Ben Voigt
5
如果你不信任你的开发者,游戏已经结束了。必须提及 XKCD 的参考链接:http://www.xkcd.com/898/ - Piskvor left the building
5
@Krycklik说,.NET安全系统并没有任何设计来完全防御恶意的完全信任的开发人员。该安全系统旨在保护用户免受从互联网下载"部分可信代码"的恶意开发人员编写的代码。如果您有恶意开发人员可以编写代码并让用户以完全信任的方式运行它,则需要制定某些流程来检测和起诉他们;.NET运行时表示完全信任就是完全信任。 - Eric Lippert
3
@Krycklik:由于您似乎担心来自不同代码提供者的代码相互攻击,您应该知道另一件事。在.NET 4安全模型中,允许具有相同信任级别的部分受信任代码相互操作其内部。也就是说,如果您加载了部分受信任的Water.DLL和部分受信任的Wine.DLL以及完全受信任的Whiskey.DLL,则Water和Wine可以操纵彼此的内部属性。Whiskey可以操纵所有三个DLL的内部属性。Water和Wine无法操纵Whiskey。 - Eric Lippert
1
@Krycklik:你可能想要阅读Shawn Farkas的博客并在Channel 9上观看他的视频。他谈论了很多关于这些安全模型设计问题的内容。 - Eric Lippert
显示剩余8条评论

42
代码编译通过是因为代码的每一行都是合法的C#代码。您认为哪一行应该成为语法错误?那里没有一行代码将值分配给只读字段。有一行代码调用反射中的方法,该方法会将值分配给只读字段,但已经编译了,并且最终破坏安全性的东西甚至不是用C#编写的,而是用C++编写的。它是运行时引擎本身的一部分。
代码成功运行是因为“完全信任”意味着“完全信任”。您在完全信任的环境中运行代码,因为完全信任意味着“完全信任”,所以运行时假定您知道当您做这种愚蠢和危险的事情时,您在做什么。
如果您尝试在部分受信任的环境中运行代码,则会看到Reflection抛出一个“您不被允许这样做”的异常。
是的,程序集已签名等等。如果您运行完全受信任的代码,则可以随便操作这些程序集。这就是完全信任的含义。部分受信任的代码无法执行此操作,但完全受信任的代码可以执行您可以执行的任何操作。请仅授予您实际信任的代码完全信任,以免其代表您执行疯狂的操作。

3
我反对说这“破坏了安全性”的说法,请参考雷蒙德·陈的优秀文章“它涉及到处于密封舱门的另一侧”。其他方面我完全同意。 - Ben Voigt
@Ben:我认为我们表达的是同样的观点,只是措辞不同。 - Eric Lippert
是的,我认为你在回答的最后已经表达得很清楚了......但我所说的那个特定句子中有一种微妙的含义,即“C++破坏安全性,而C#则不会”。你在一些评论中使用的“不变量”术语更好。 - Ben Voigt

20

反射让你能够做任何事情,甚至可以设置私有成员的值。

反射不遵循规则,你可以在MSDN上了解它。

另一个例子:我能使用反射在C#中更改私有只读字段吗?


如果你在使用Web应用程序,你可以设置应用程序的信任级别。
level="[Full|High|Medium|Low|Minimal]"

这是信任级别的限制,根据MSDN的规定,在中等信任级别下,您将限制反射访问。

编辑:请勿在除完全信任之外的Web应用程序中运行,这是ASP.NET团队的直接建议。为了保护您的应用程序,请为每个网站创建一个App Pool。


此外,不建议在所有情况下都放任使用反射。它有适当的使用场合和时间。


2
有一些反射可以绕过的规则(可访问性),也有一些反射需要遵守的规则(类型安全)。为什么“initonly”应该属于第一类规则是一个很好的问题。 - Ben Voigt
谢谢,这解释了问题1。那么安全方面的顾虑呢? - Krycklik
1
@Ben:违反无障碍规则或“initonly”规则的代码最坏的情况下可能会导致程序中的语义错误,例如违反程序不变量并因异常而崩溃。违反类型安全性的代码实际上可以使运行时本身崩溃。运行时的适当操作是基于其运行的代码是类型安全的。如果不是,则可以在对象字段中存储指针大小的整数,该整数指向垃圾,并且当运行时对其进行解引用时将崩溃。 - Eric Lippert
16
没有安全问题,因为你有一段“完全信任”的代码攻击另一段“完全信任”的代码。谁在乎呢?攻击的代码都是完全可信的;如果它是敌对的,它已经可以随心所欲地伤害用户了。如果你有敌对的完全可信代码,那么它已经获胜了。只有当“部分可信”的代码可以像这样耍花招时才会存在安全问题,但它不能。 - Eric Lippert

5
在这里没有人提到的一个问题是:这段代码会在不同的.NET实现/平台上产生不同的行为。实际上,在Mono上它根本不返回任何东西:请参阅IDE One (Mono 2.8),我的本地Mono 2.6.7(Linux)也产生了相同的“输出”。
我还没有看过底层代码,但我认为它与编译器或运行时环境一样是特定于编译器的,就像PRASHANT P所提到的那样。 更新 在Windows上(使用MS Dotnet 4),运行Mono编译的exe产生以下结果:
evil 
equal

在Linux上运行Windows编译的exe文件是不可能的(Dotnet 4...),因此我重新编译了Dotnet 2版本(在Windows上仍然显示为 evil equal )。没有任何输出。当然,至少应该有第一个WriteLine"\n"。实际上,它就在那里。我将输出导入到一个文件中,并启动我的十六进制编辑器来查看单个字符0x0A
简而言之:它似乎是与运行时环境相关的。

1
在mono上,“String.Empty”仍然被覆盖。稍微更改您的代码即可显示此内容:http://ideone.com/64RWi - Ben Voigt
@Ben Voigt:确实,这段代码在两个运行时环境上产生相同的输出。仍然有一个问题,为什么原始代码表现不同。 - mbx
4
我猜测JIT优化器在某些情况下可能会出于性能原因特殊处理String.Empty,而不是实际从字段中读取。例如,s == String.Empty可以被替换为s.Length == 0,这样更快。通过稍微复杂化代码,我打败了JIT的模式识别并避免了特殊处理。但这只是猜测。 - Ben Voigt

0

readonly 只在编译器级别强制执行

因此

可能会在当前底层级别进行更改


3
你所说的编译器是指哪一个?C# 编译器还是 JIT 编译器? - svick

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