61

I stumbled upon this code:

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

 }

and I wonder how is it even possible to compile this piece of code. My reasoning is:

According to MSDN String.Empty is read-only therefore changing it should be impossible and compiling should end with "A static readonly field cannot be assigned to" or similar error.

I thought Base Class Library assemblies are somehow protected and signed and whatnot to prevent exactly this kind of attack. Next time someone may change System.Security.Cryptography or another critical class.

I thought Base Class Library assemblies are compiled by NGEN after .NET installation therefore changing fields of String class should require advanced hacking and be much harder.

And yet this code compiles and works. Can somebody please explain what is wrong with my reasoning?

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
Krycklik
  • 513
  • 4
  • 6
  • I can understand how that compiles, nothing there is wrong for compile time. Would love to check the source of the String class now but I don't have time... – Andre Luus Jun 09 '11 at 14:02
  • @Andre: The `Empty` field does have the `initonly` modifier. – Ben Voigt Jun 09 '11 at 14:04
  • I'd vote for this to be patched by Microsoft for this specific property - it really shouldn't be possible to change "Empty". – Andre Luus Jun 09 '11 at 14:07
  • @Andre That would be really ugly code in the reflection. Checking if you are currently setting string.Empty hard coded, yuck – Oskar Kjellin Jun 09 '11 at 14:09
  • 1
    @Andre: And what about `IntPtr.Zero`? Or `Int32.MaxValue`? Maybe what you're *really* suggesting is that `initonly` in `System.dll` should never be bypassed by reflection. – Ben Voigt Jun 09 '11 at 14:13
  • 1
    Yes absolutely. I'm not suggesting a special case for String.Empty. Perhaps checking `initonly` is sufficient or perhaps it'd be cutting too deep; we can't really tell. Point is, the CLR team should fix it. – Andre Luus Jun 09 '11 at 14:19
  • 22
    @Andre: It cannot be fixed now because people rely on fully trusted code being able to rehydrate readonly fields from persisted state using Reflection. There's no reason to "fix" it because it isn't broken in the first place. If you don't want code to change these fields, *don't fully trust code that changes those fields.* – Eric Lippert Jun 09 '11 at 14:27
  • 1
    Fair enough @Eric, but you're telling me it is perfectly acceptable for fully trusted code to change values that have special meanings? I'm sorry but I can't agree on that. Being able to prevent it using code access security really has nothing to do with it. – Andre Luus Jun 10 '11 at 08:09
  • 4
    @Andre: Whether it is *acceptable* or not is irrelevant; it is *possible*. I have said this about a dozen times now but it seems to not be sinking in: *full trust means full trust*. Full trust code can obtain a pointer to the executing machine code of the runtime and rewrite it with different code for the security system! Is that a nice thing to do? No. But *there is no way to prevent it* and therefore there is no way to prevent *anything* because the security system is what enforces these rules. As Raymond Chen says, it's a game of walls and ladders. – Eric Lippert Jun 10 '11 at 14:12
  • 2
    @Andre: We are not going to say "well, hey guys, don't worry if you have trusted some hostile code because at least you know it can't change String.Empty". Why bother putting up a wall that prevents that when hostile full trust code has a ladder that is ten times taller than that wall? – Eric Lippert Jun 10 '11 at 14:14
  • 1
    @Eric, thanks for taking the time to argue with me on this. I can understand how from the C#/CLR team's perspective changing this isn't necessary. If security is important, as you said, don't fully trust any 3rd party code. That is the only realistic way for you to protect our code. I think we've developed a false sense of security in concepts such as 'private', 'internal' etc. – Andre Luus Jun 10 '11 at 14:36
  • 6
    @Andre: Indeed, accessibility constraints are for *programmers*. They're there so that you can document your intentions for your benign coworkers and have tools that do a reasonably good job of ensuring that your invariants are met. They're there also so that metadata is self-documenting, and tells your customers what parts of a program they can rely on as being tested and stable, and what parts are internal implementation details. Those sorts of tasks are their job; if you have strong security requirements that you need to enforce, you'll have to use other mechanisms. – Eric Lippert Jun 10 '11 at 16:15

5 Answers5

52

A static readonly field cannot be assigned to

You're not assigning to it. You're calling public functions in the System.Reflection namespace. No reason for the compiler to complain about that.

Besides, typeof(string).GetField("Empty") could use variables entered in by the user instead, there's no sure way for the compiler to tell in all cases whether the argument to GetField will end up being "Empty".

I think you're wanting Reflection to see that the field is marked initonly and throw an error at runtime. I can see why you would expect that, yet for white-box testing, even writing to initonly fields has some application.

The reason NGEN has no effect is that you're not modifying any code here, only data. Data is stored in memory with .NET just as with any other language. Native programs may use readonly memory sections for things like string constants, but the pointer to the string is generally still writable and that is what is happening here.

Note that your code must be running with full-trust to use reflection in this questionable way. Also, the change only affect one program, this isn't any sort of a security vulnerability as you seem to think (if you're running malicious code inside your process with full trust, that design decision is the security problem, not reflection).


Further note that the values of initonly fields inside mscorlib.dll are global invariants of the .NET runtime. After breaking them, you can't even reliably test whether the invariant was broken, because the code to inspect the current value of System.String.Empty has also broken, because you've violated its invariants. Start violating system invariants and nothing can be relied on.

By specifying these values inside the .NET specifications, it enables the compiler to implement a whole bunch of performance optimizations. Just a simple one is that

s == System.String.Empty

and

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

are equivalent, but the latter is much faster (relatively speaking).

Also the compiler can determine that

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

is never true, and generate an unconditional jump to the else block (it still has to call Int32.Parse to have the same exception behavior, but the comparison can be removed).

System.String.Empty is also used extensively inside BCL implementations. If you overwrite it, all sorts of crazy things can happen, including damage that leaks outside your program (for example you might write to a file whose name is built using string manipulation... when string breaks, you might overwrite the wrong file)


And the behavior might easily differ between .NET versions. Normally when new optimization opportunities are found, they don't get backported to previous versions of the JIT compiler (and even if they were, there could be installations from before the backport was implemented). In particular. String.Empty-related optimizations are observably different between .NET 2.x and Mono and .NET 4.5+.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Thanks, exactly what I was looking for. Ad security. My problem is not software->machine attack, my problem is developer->codebase attack. Let say some company has already tested or even verified piece of code. Rogue developer of another part of software uses reflection in some obscure method to cripple main algorithm. Source code of StockTradingBotCore (or whatever) doesn't change, it looks just the same, except it makes trading decision mistakes from time to time. How can you detect this and find the rat? Nothing changed. – Krycklik Jun 09 '11 at 16:18
  • 5
    @Krycklik: If you've got "another part of software" and don't trust the developer, load that submodule using partial trust. You're talking about giving an evil developer unlimited rights, don't do that. Use .NET "Code Access Security" to give out only the rights needed to perform the task. – Ben Voigt Jun 09 '11 at 17:01
  • 5
    @Krycklik: If you don't trust your developers, the game is already over. Obligatory XKCD reference: http://www.xkcd.com/898/ – Piskvor left the building Jun 09 '11 at 17:07
  • @BenVoigt: You have to look earn the critic's badge at some point! :P List of suspects [here](http://stackoverflow.com/badges/7/critic) – Trufa Jun 09 '11 at 18:20
  • 5
    @Krycklik: the .NET security system is not designed to defend in *any way whatsoever* against hostile fully trusted developers. The security system is designed to protect *users* from *hostile developers who wrote code the users then downloaded off the internet*; that is, "partially trusted" code scenarios. If you have hostile developers who can write code and get users to run it with full trust then you're going to have to come up with some process to detect and prosecute them; the .NET runtime says that full trust is full trust. – Eric Lippert Jun 09 '11 at 20:17
  • @Trufa: I guess... although finding an answer that obviously deserves a downvote really isn't that difficult. Anyway, I'm a lot less concerned about a -1/+29 ratio than a -1/+2... the latter might indicate a real problem with my answer. – Ben Voigt Jun 09 '11 at 20:53
  • 3
    @Krycklik: Since it sounds like you are concerned about code from different code providers attacking each other, you should be aware of another thing. In the .NET 4 security model, *partially trusted code that is all of the same trust level is allowed to party on each other's internals*. That is, if you load partially trusted Water.DLL and partially trusted Wine.DLL and fully trusted Whiskey.DLL, then Water and Wine can mess with each other's internal properties. Whiskey can mess with the internal properties of all three. Water and Wine can't mess with Whiskey. – Eric Lippert Jun 09 '11 at 21:29
  • 1
    @Krycklik: You might want to read Shawn Farkas's blog and watch his videos on Channel 9. He talks a lot about these sorts of security model design issues. – Eric Lippert Jun 09 '11 at 21:29
  • @Eric Thanks for the blog/videos. Much better than wiki/msdn. Until now I thought official MS code=untouchable, that is why I was surprised how easy it is. – Krycklik Jun 09 '11 at 22:49
  • @Krycklik: Again, this is data, not code, which is why the runtime allows it be to overwritten through reflection. I don't believe that reflection allows replacing code, even with DynamicMethod (unless the code was originally written as a DynamicMethod). – Ben Voigt Jun 09 '11 at 22:52
  • 1
    @Krycklik: Look at it this way. Fully trusted code could go onto the internet, pull down a custom-built version of the .NET runtime where "String.Empty" was equal to any old thing, install it, and then restart the program in the new runtime. If you could do that, fully trusted code could do it too. There's nothing magical about Microsoft code that makes it immune to attack from full trust code; full trust code can do anything the user can do, and that includes installing bogus versions of the runtime. Just tweaking a private field is *nothing* compared to what hostil full trust code can do! – Eric Lippert Jun 10 '11 at 05:54
  • 1
    @Eric: Fully trusted doesn't (hopefully) mean OS privilege escalation. I assume that .NET runtime installation requires administrator privileges, and that xcopy install of .NET isn't possible (at least on systems where the OS loader recognizes MSIL assemblies). If administrator privilege is involved, then might as well go for the ultimate boogey-man: *fully trusted code could install Linux**! – Ben Voigt Jun 10 '11 at 13:55
  • @Ben: Right, full trust does not mean admin privileges. But you can download and run an awful lot of code without admin privileges. My example was not intended to be a realistic example of an attack but rather to indicate the sort of power that full trust code has: everything you can do, fully trusted code can do on your behalf. – Eric Lippert Jun 10 '11 at 14:25
42

The code compiles because every line of the code is perfectly legal C#. What specific line do you think should be a syntax error? There is no line of code there that assigns to a readonly field. There's a line of code that calls a method in Reflection that assigns to a readonly field, but that's already compiled, and ultimately the thing that breaks security in there wasn't even written in C#, it was written in C++. It's part of the runtime engine itself.

The code runs successfully because full trust means full trust. You are running your code in a full trust environment, and since full trust means full trust, the runtime is assuming that you know what you're doing when you do this stupid dangerous thing.

If you try running your code in a partially trusted environment then you'll see that Reflection throws a "you're not allowed to do that" exception.

And yes, the assemblies are signed and whatnot. If you're running fully-trust code, then sure, they can screw around with those assemblies as much as they want. That's what full trust means. Partially trusted code doesn't get to do that but fully trusted code can do anything you can do. Only grant full trust to code you actually trust to not do crazy things on your behalf.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 3
    I object to saying this "breaks security", see [Raymond Chen's excellent "It rather involved being on the other side of this airtight hatchway" articles](http://www.google.com/search?q=oldnewthing+airtight). Everything else I agree with 100%. – Ben Voigt Jun 09 '11 at 14:12
  • @Ben: I think we're making the same point, just with different wording. – Eric Lippert Jun 09 '11 at 14:45
  • Yes, I think you make that pretty clear toward the end of your answer... but the particular sentence I'm talking about has a subtle undertone of *C++ breaks security, C# doesn't*. The terminology of *invariants* you used in some of your comments is better. – Ben Voigt Jun 09 '11 at 14:47
20

Reflection allows you to defy laws of physics do anything. You can even set the value of private members.

Reflection does not follow rules, you can read about it on MSDN.

Another example: Can I change a private readonly field in C# using reflection?


If you are on a web application you can set the trust level of your application.

level="[Full|High|Medium|Low|Minimal]"

These are the restriction of the trust level, accondingly to MSDN, at medium trust you restrict reflection access.

Edit: DO NOT run a web application other than Full Trust, this is a direct recommendation from ASP.NET team. To protect your application create one App Pool for each website.


Also, it is not recommended to go wild using reflection for everything. It has the right place and time to be used.

Community
  • 1
  • 1
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
  • 2
    There are rules that reflection bypasses (accessibility) and rules that reflection enforces (type safety). Why `initonly` should be in the first category is a good question. – Ben Voigt Jun 09 '11 at 14:02
  • Thanks, this explains problem 1. What about the security concerns? – Krycklik Jun 09 '11 at 14:13
  • 1
    @Ben: code which breaks accessibility rules or the "initonly" rules can at worst lead to semantic errors in the program that, say, violates program invariants and crashes with an exception. Code which breaks type safety can actually crash *the runtime itself*. The proper operation of the runtime is predicated on the code it runs being typesafe. If its not then you can store a pointer-sized integer in an object field that points off to garbage and the runtime will crash when it dereferences it. – Eric Lippert Jun 09 '11 at 14:16
  • 16
    @Krycklik: There's no security concern because you have a *full trust* code attacking other *full trust* code. Who cares? The attacking code is all fully trusted; if it were hostile, it could already hurt the user however it wants. If you have hostile fully trusted code then *it has already won.* There is only a security concern if *partially trusted* code can pull shenanigans like this, and it cannot. – Eric Lippert Jun 09 '11 at 14:19
5

A point nobody mentioned here before: This piece of code causes different behavior on different .net implementations/platforms. Actually on Mono it returns nothing at all: see IDE One (Mono 2.8), my local Mono 2.6.7 (linux) produces the same "output".

I haven't yet looked at the low level code, but I suppose it's compiler specific just as mentioned by PRASHANT P or the runtime environment.

UPDATE

Running the Mono compiled exe on Windows (MS Dotnet 4) produces

evil 
equal

Running the Windows compiled exe on linux was not possible (Dotnet 4...) so I recompiled with Dotnet 2 (it still says evil and equal on Windows). There is "no" output. Of course there must be at least the "\n" from the first WriteLine, in fact it is there. I piped the output to a file and started up my hexeditor to look at a single character 0x0A.

To cut a long story short: It seems to be runtime environment specific.

Community
  • 1
  • 1
mbx
  • 6,292
  • 6
  • 58
  • 91
  • 1
    On mono, `String.Empty` is still overwritten. A slight change to your code shows this: http://ideone.com/64RWi – Ben Voigt Jun 10 '11 at 14:06
  • @Ben Voigt: inded, this code produces the same output on both RTEs. There stil is the question, why the original code behaves different. – mbx Jun 10 '11 at 15:14
  • 4
    I guess the JIT optimizer may special-case `String.Empty` in certain cases for performance reasons, and not actually read from the field. For example, `s == String.Empty` may be replaced by `s.Length == 0`, which is faster. By making the code just a little more complex, I've defeated the JIT's pattern recognition and avoided the special handling. But this is just supposition. – Ben Voigt Jun 10 '11 at 17:48
0

readonly is only enforce

at COMPILER level

therefore

may be changed at current beneath level

PRASHANT P
  • 1,527
  • 1
  • 16
  • 28