我偶然发现了这个代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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或其他关键类。
我认为基类库程序集是由ngen在.NET安装之后编译的,因此更改字符串类的字段需要高级黑客攻击,而且要困难得多。
但这段代码可以编译并运行。有人能解释一下我的推理有什么问题吗?
- 我能理解它是如何编译的,编译时没有错。现在想检查一下字符串类的源代码,但我没有时间……
- @安德烈:Empty字段有initonly修饰符。
- 我投票赞成微软为这个特定的财产修补这个漏洞——它真的不应该改变为"空的"。
- @安德烈,这将是反射中非常难看的代码。正在检查当前是否正在设置字符串。空的硬编码,糟糕。
- @安德烈:那埃多克斯1〔2〕呢?或是Int32.MaxValue?也许你真正建议的是,在System.dll中的initonly永远不应该被反射所绕过。
- 是的,当然。我不是建议给string.empty一个特殊的例子。也许检查EDOCX1[1]就足够了,或者它切割得太深,我们不能真正分辨。关键是,CLR团队应该修复它。
- @安德烈:现在无法修复,因为人们依赖完全可信的代码,能够使用反射将只读字段从持久状态中重新水化。没有理由"修理"它,因为它一开始没有损坏。如果不希望代码更改这些字段,请不要完全信任更改这些字段的代码。
- 很公平@eric,但您告诉我完全可信的代码更改具有特殊含义的值是完全可以接受的?对不起,我不能同意。能够使用代码访问安全性来阻止它,实际上与此无关。
- @安德烈:这是否可以接受是无关紧要的,是有可能的。我已经说过十几次了,但似乎并没有陷入困境:完全信任意味着完全信任。完全信任代码可以获得指向运行时执行机器代码的指针,并为安全系统用不同的代码重写它!这是件好事吗?没有。但是没有办法阻止它,因此没有办法阻止任何事情,因为安全系统是执行这些规则的。正如陈瑞蒙所说,这是一个由墙和梯子组成的游戏。
- @安德烈:我们不会说"嘿,伙计们,别担心你们是否信任了一些恶意代码,因为至少你们知道它不能改变字符串。空的"。当敌对的完全信任代码有一个比墙高十倍的梯子时,为什么还要竖起一堵墙来防止呢?
- @埃里克,谢谢你花时间和我争论这个问题。我可以从C/clr团队的角度理解,改变这一点是不必要的。如果安全很重要,如您所说,不要完全信任任何第三方代码。这是您保护我们代码的唯一现实方法。我认为我们在诸如"私有"、"内部"等概念中产生了一种错误的安全感。
- @安德烈:确实,可访问性约束是针对程序员的。它们就在那里,这样你就可以记录下你对你的友好同事的意图,并且有一些工具可以相当好地确保你的不变量得到满足。它们的存在也使得元数据是自记录的,并告诉您的客户他们可以依赖程序的哪些部分来进行测试和稳定,以及哪些部分是内部实现细节。这些类型的任务是他们的工作;如果您有强大的安全需求需要强制执行,则必须使用其他机制。
A static readonly field cannot be assigned to
你没有分配给它。您在System.Reflection名称空间中调用公共函数。编译器没有理由对此抱怨。
另外,typeof(string).GetField("Empty")可以使用用户输入的变量来代替,编译器在所有情况下都无法确定GetField的参数最终是否会是"Empty"的。
我认为您希望Reflection看到字段标记为initonly,并在运行时抛出错误。我可以理解为什么您会期望,但是对于白盒测试,甚至写入initonly字段也有一些应用程序。
ngen不起作用的原因是您没有在这里修改任何代码,只修改数据。与任何其他语言一样,数据使用.NET存储在内存中。本机程序可以对字符串常量之类的东西使用只读内存段,但指向字符串的指针通常仍然是可写的,这就是这里发生的事情。
请注意,您的代码必须以完全信任的方式运行,才能以这种可疑的方式使用反射。此外,更改只影响一个程序,这并不像您想象的那样是一个安全漏洞(如果您在流程内以完全信任的方式运行恶意代码,那么设计决策就是安全问题,而不是反射)。
进一步注意,mscorlib.dll中initonly字段的值是.NET运行时的全局不变量。打破它们之后,甚至无法可靠地测试不变量是否被破坏,因为检查system.string.empty当前值的代码也被破坏了,因为您违反了它的不变量。开始违反系统不变量,什么都不能依赖。
通过在.NET规范中指定这些值,编译器可以实现一系列性能优化。简单一点就是
1
| s == System.String.Empty |
和
1
| (s != null) && (s.Length == 0) |
是等效的,但后者要快得多(相对而言)。
编译器也可以确定
1
| if (int.Parse(s) > int.MaxValue) |
从不为真,并生成到else块的无条件跳转(它仍然必须调用Int32.Parse以具有相同的异常行为,但可以删除比较)。
System.String.Empty在BCL实现中也被广泛使用。如果覆盖它,可能会发生各种疯狂的事情,包括程序外部泄漏的损坏(例如,您可能会写入一个文件,该文件的名称是使用字符串操作构建的…当字符串中断时,您可能会覆盖错误的文件)
而且.NET版本之间的行为可能很容易有所不同。通常,当发现新的优化机会时,它们不会被反向移植到以前版本的JIT编译器(即使是这样,也可能在实现反向移植之前进行安装)。特别地。与String.Empty相关的优化在.NET 2.x和mono以及.NET 4.5+之间存在显著差异。
- 投反对票的原因?
- 谢谢,正是我要找的。广告安全性。我的问题不是软件->机器攻击,我的问题是开发人员->代码库攻击。假设一些公司已经测试甚至验证了一段代码。软件另一部分的流氓开发人员利用一些模糊方法中的反射来削弱主算法。StockTradingBotcore(或其他什么)的源代码没有改变,它看起来是一样的,只是有时会犯交易决策错误。你怎么能发现这个并找到老鼠?没有改变。
- @Krycklik:如果你有"软件的另一部分",并且不信任开发人员,那么使用部分信任加载子模块。你说的是给一个邪恶的开发者无限的权利,不要这样做。使用.NET"代码访问安全性"只授予执行该任务所需的权限。
- @如果你不信任你的开发者,游戏已经结束了。强制性xkcd参考:xkcd.com/898
- @本沃伊特:你必须看起来在某个时候赢得评论家的徽章!:p此处的嫌疑人列表
- @Krycklik:NET安全系统的设计并不是为了以任何方式防御恶意的完全信任的开发人员。安全系统的设计是为了保护用户免受恶意开发人员的攻击,这些开发人员编写代码,然后将用户从互联网上下载下来;也就是说,"部分受信任"的代码场景。如果有敌对的开发人员可以编写代码并让用户以完全信任的方式运行代码,那么您必须想出一些过程来检测和起诉他们;.NET运行时说完全信任就是完全信任。
- @特鲁法:我想……尽管找到一个明显值得否决的答案并不难。不管怎样,我对-1/+29比-1/+2更不关心…后者可能表明我的回答存在真正的问题。
- @Krycklik:因为听起来你很担心来自不同代码提供者的代码相互攻击,所以你应该知道另一件事。在.NET 4安全模型中,允许具有相同信任级别的部分受信任代码参与彼此的内部。也就是说,如果加载部分受信任的water.dll和部分受信任的wine.dll以及完全受信任的wiskey.dll,那么水和酒可能会干扰彼此的内部属性。威士忌会破坏这三种酒的内在特性。水和酒不能和威士忌混在一起。
- @你可能想读肖恩·法卡斯的博客,在9频道看他的视频。他经常谈到这些安全模型设计问题。
- @Eric感谢你的博客/视频。比wiki/msdn好多了。直到现在,我还认为官方的MS代码是不可触及的,这就是为什么我惊讶于它如此简单的原因。
- @再次强调,这是数据,而不是代码,这就是为什么运行时允许通过反射覆盖它。我不认为反射允许用dynamicmethod替换代码(除非代码最初是作为dynamicmethod编写的)。
- @克赖克利克:这样看。完全受信任的代码可以进入Internet,下拉一个自定义的.NET运行时版本,其中"string.empty"等于任何旧的东西,安装它,然后在新的运行时重新启动程序。如果可以这样做,完全可信的代码也可以这样做。微软的代码没有什么神奇的地方可以让它不受完全信任代码的攻击;完全信任代码可以做用户能做的任何事情,包括安装虚假的运行时版本。与hostil完全信任代码所能做的相比,仅仅调整一个私有字段是没有用的!
- @埃里克:完全信任并不(希望)意味着操作系统权限升级。我假设.NET运行时安装需要管理员权限,而.NET的xcopy安装不可能(至少在OS加载器识别MSIL程序集的系统上)。如果涉及到管理员特权,那么最好选择最棒的人:完全可信的代码可以安装Linux*!
- @本:对,完全信任并不意味着管理特权。但是你可以下载并运行大量没有管理员权限的代码。我的示例并不是要成为一个现实的攻击示例,而是要指出完全信任代码所具有的那种能力:您可以做的一切,完全信任的代码都可以代表您做。
- + 1 - Thanks Ben
代码编译是因为代码的每一行都是完全合法的。您认为哪一行是语法错误?那里没有分配给只读字段的代码行。有一行代码调用一个反射的方法,它分配给只读字段,但已经编译好了,最终破坏了安全性的东西甚至没有用C语言编写,它是用C++编写的。它是运行时引擎本身的一部分。
代码运行成功,因为完全信任意味着完全信任。您在完全信任的环境中运行代码,由于完全信任意味着完全信任,因此运行时假设您在执行这种愚蠢的危险操作时知道自己在做什么。
如果您尝试在部分受信任的环境中运行代码,那么您将看到反射抛出了一个"不允许您这样做"异常。
是的,集会已经签署了什么。如果您运行的是完全信任的代码,那么当然,它们可以随心所欲地使用这些程序集。这就是完全信任的含义。部分受信任的代码不能做到这一点,但是完全受信任的代码可以做任何你能做的事情。只有完全信任你真正信任的代码,才不会代表你做疯狂的事情。
- 我反对说这种"破坏安全",看陈瑞蒙的优秀"它涉及到在这个密封舱口的另一边"文章。其他一切我都同意100%。
- @本:我认为我们的观点是一样的,只是措辞不同。
- 是的,我认为你在回答的最后很清楚地说明了这一点…但是我所说的特定句子有一个微妙的C++断言安全性,C不。你在某些评论中使用的不变量的术语比较好。
反射可以让你违反物理定律做任何事情。甚至可以设置私有成员的值。
反射不遵循规则,您可以在msdn上阅读。
另一个例子:我可以使用反射更改C中的私有只读字段吗?
如果您在Web应用程序上,则可以设置应用程序的信任级别。
1
| level="[Full|High|Medium|Low|Minimal]" |
这些是信任级别的限制,与msdn一致,在Medium Trust中,您限制反射访问。
编辑:不要运行完全信任以外的Web应用程序,这是ASP.NET团队的直接建议。为保护应用程序,请为每个网站创建一个应用程序池。
此外,不建议使用反射来进行任何事情。它有正确的使用地点和时间。
- 反射绕过(可访问性)的规则和反射强制执行的规则(类型安全)。为什么initonly应该在第一类中是一个好问题。
- 谢谢,这解释了问题1。安全问题呢?
- @本:破坏可访问性规则或"initonly"规则的代码最坏情况下会导致程序中的语义错误,例如,违反程序不变量和异常崩溃。破坏类型安全性的代码实际上会使运行时本身崩溃。运行时的正确操作取决于它运行的代码是类型安全的。如果不是这样,那么您可以将一个指针大小的整数存储在一个指向垃圾的对象字段中,当它取消引用它时,运行时将崩溃。
- @Krycklik:没有安全问题,因为您有一个完全信任代码攻击其他完全信任代码。谁在乎?攻击代码都是完全可信的;如果它是恶意的,那么它可能已经伤害了用户,不管用户想要什么。如果你有恶意的完全可信的代码,那么它已经赢了。只有当部分信任的代码能够像这样制造恶作剧时,才存在安全问题,而不能。
这里有一点以前没人提到过:这段代码在不同的.NET实现/平台上会导致不同的行为。实际上,在Mono上,它什么也不返回:参见IDeone(Mono2.8),我的本地Mono2.6.7(Linux)生成相同的"输出"。
我还没有看过低级代码,但是我想它是特定于编译器的,正如prashant p或运行时环境所提到的那样。
更新
在Windows(MS Dotnet 4)上运行Mono编译的exe可生成
在Linux上运行Windows编译的exe是不可能的(dotnet 4…),所以我用dotnet 2重新编译(它在Windows上仍然表示邪恶和平等)。没有输出。当然,从第一个WriteLine开始,必须至少有"
",事实上,它就在那里。我将输出通过管道传输到一个文件,并启动hexeditor查看单个字符0x0A。
长话短说:它似乎是特定于运行时环境的。
- 在mono上,String.Empty仍然被覆盖。代码的一个微小变化显示了这一点:ideone.com/64rwi
- @ben voigt:inded,此代码在两个RTE上生成相同的输出。还有一个问题,为什么原始代码的行为不同。
- 我想在某些情况下,由于性能原因,JIT优化器可能会使用String.Empty这种特殊情况,而不是实际从字段中读取。例如,s == String.Empty可以用s.Length == 0代替,速度更快。通过使代码稍微复杂一点,我击败了JIT的模式识别,避免了特殊的处理。但这只是假设。
只读仅强制
在编译器级别
因此
可能在当前低于级别时发生更改