26

TL;DR Can the user token contained in a WindowsIdentity's Token property (say, someIdentity.Token) be spoofed such that:

var validated = new WindowsIdentity(someIdentity.Token);

...will return an instance that claims to represent a user that has not, in fact, been authenticated, and yet has IsAuthenticated set true, valid .Name and .User properties, etc.?

Below I put a few boundaries on this; it's presumably impossible to completely spoof-proof.


The full story:

In this answer, Damien_The_Unbeliever cleverly demonstrated that some code of mine could be tricked into believing it had a valid authenticated user in a WindowsIdentity instance when it didn't. Long story short, my code was assuming that if Thread.CurrentPrincipal.Identity was an instance of WindowsIdentity and IsAuthorized was true, that it represented an authenticated user and I could rely on the SID in .User:

WindowsIdentity identity = Thread.CurrentPrincipal == null
    ? null
    : Thread.CurrentPrincipal.Identity as WindowsIdentity;

if (identity != null && identity.IsAuthenticated && !identity.IsAnonymous) {
    // ...use and trust the SID in identity.User, the
    // username in identity.Name, etc....
}

(There's a reason this code is using the thread rather than WindowsIdentity.GetCurrent().)

His code to trick that (slightly modified):

var ident = WindowsIdentity.GetCurrent();
Thread.CurrentPrincipal = new WindowsPrincipal(ident);
var fakeSid = new SecurityIdentifier("S-1-3-0"/* E.g., some SID you want to trick me into believing is the real user */);
typeof(WindowsIdentity).GetField("m_user", BindingFlags.Instance | BindingFlags.NonPublic)
    .SetValue(ident, fakeSid);

And sure enough, if you do that then call my code above, my code gets fooled. Kudos Damien.

So in true arms race fashion, here's my revised code that catches the spoof and denies it:

WindowsIdentity identity = Thread.CurrentPrincipal == null
    ? null
    : Thread.CurrentPrincipal.Identity as WindowsIdentity;

if (identity != null && identity.IsAuthenticated && !identity.IsAnonymous) {
    var validated = new WindowsIdentity(identity.Token);
    if (!validated.User.Equals(identity.User) || !validated.IsAuthenticated || validated.IsAnonymous) {
        // Something fishy is going on, don't trust it
    } else {
        // Good! Use the validated one
        identity = validated;
        // ...use and trust the SID in identity.User, the
        // username in identity.Name, etc....
    }
}

As you can see, that takes the Token from the provided identity and creates a new WindowsIdentity instance using that token. If the SIDs of the identities match, we continue, trusting the validated one. (The documentation for WindowsIdentity(IntPtr token) says that the initial value of IsAuthenticated will be false, but that's simply wrong in my tests assuming I've created it with a valid user token.)

The only way I can see that that could be tricked would be with a spoofed user token that nevertheless passes the validations Windows does with it. That seems unlikely to me. But then, this is an area of ignorance for me.


Boundaries:

I should note that I'm just shooting for a reasonable degree of single sign-on security here, doing my due dilience. If a malicious app has successfully started intercepting system calls / compromised Windows itself, well, there's just not a lot I'm going to be able to do about that. As Damien pointed out in comments on that other question, he could probably build a host container that completely ignored strong naming (and thus could give me a completely fake WindowsIdentity type). Fair enough. Perfection kills. I just don't want to leave doors open like the one Damien kindly demonstrated. If I released a system and it were hacked in the field that easily, I'd be pretty darned embarrassed about it. :-)

Community
  • 1
  • 1
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 4
    Well beyond my ability to answer, but great question! Excited to see the answers that are given :) – levelonehuman Nov 06 '15 at 19:02
  • @levelonehuman My thoughts exactly! – David Behler Nov 06 '15 at 19:18
  • 2
    "Skeet don't fail me now!" ;-) – T.J. Crowder Nov 06 '15 at 19:22
  • 1
    What is your attacker? Someone who edits memory locations of your process with a debugger? If that happens You have lost already. If in your process the data of other users is already there a simple memory dump will be your worst enemy. – Alois Kraus Nov 06 '15 at 19:43
  • @AloisKraus: No, this is a DLL accesses a software service based on the Windows user. So for instance, Damien's attack would be code run in an executable linking with the DLL and then callilng my API. You're quite right that the kind of attack you're talking about is out of my control, I just don't want to leave door open to hacks like the one he demonstrated. – T.J. Crowder Nov 06 '15 at 19:52

1 Answers1

8

To demonstrate this is doable, let's use a cool Visual Studio addon named "Microsoft Fakes" (the name itself means a lot...).

Fakes itself is tied to test features of Visual Studio, but it will prove the point. You can follow the standard tutorials to setup a project, and add a fakes assembly for System (in fact mscorlib + system)

This is your code in a library project (I've used exception everywhere because it's easier in testing condition...).

namespace ClassLibrary1
{
    public class Class1
    {
        public static void MyCheck()
        {
            WindowsIdentity identity = Thread.CurrentPrincipal == null
                ? null
                : Thread.CurrentPrincipal.Identity as WindowsIdentity;

            if (identity != null && identity.IsAuthenticated && !identity.IsAnonymous)
            {
                var validated = new WindowsIdentity(identity.Token);
                if (!validated.User.Equals(identity.User) || !validated.IsAuthenticated || validated.IsAnonymous)
                    throw new Exception("Something fishy is going on, don't trust it");
                else
                    throw new Exception("Good! Use the validated one. name is:" + validated.Name);
            }
            else
                throw new Exception("not in");
        }
    }
}

This is the testing code in the test project:

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            using (ShimsContext.Create())
            {
                System.Security.Principal.Fakes.ShimWindowsIdentity.AllInstances.NameGet = (i) =>
                {
                    return "Simon the hacker";
                };

                WindowsIdentity wi = WindowsIdentity.GetCurrent(); // this is the real one "Simon".
                Thread.CurrentPrincipal = new WindowsPrincipal(wi);

                Class1.MyCheck();
            }
        }
    }
}

This is the project layout in Visual Studio:

enter image description here

Also make sure you modify the mscorlib.fakes file that was automatically generated like this:

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true" TargetFrameworkVersion="v4.6">
  <Assembly Name="mscorlib" />
  <ShimGeneration>
    <Clear />
    <Add Namespace="System.Security.Principal" />
  </ShimGeneration>
</Fakes>

It means I want the whole System.Security.Principal namespace to be shimed and I suggest you use framework 4.6 for both projects and add the corresponding TargetFrameworkVersion attribute.

Now, when you run the test, this is what you'll see:

enter image description here

Ok, in your specific scenario, I may not be able to use fakes, but the underlying technology it relies on just reroutes all APIs (it's lower than .NET in fact, it's called Detours) I believe and allows all these hackery.

To sum up: if it runs on my machine, I can hack it (unless I don't have physical access to my machine).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • Thanks. I think this falls under *"As Damien pointed out in comments on that other question, he could probably build a host container that completely ignored strong naming (and thus could give me a completely fake WindowsIdentity type). Fair enough. Perfection kills."* But it's distrubing (if not entirely surprising) that it's that easy. Based on Damien's comment I was thinking I might authenticate the token using unmanaged Windows calls -- but that just pushes the problem back a layer, the calling process could intercept *them*, too. Or just edit my assembly's bytecode to bypass the check. – T.J. Crowder Nov 08 '15 at 07:42
  • So given *"if it runs on my machine, I can hack it"*, in your very well-informed, experienced professional opinion, where do I draw the line? The system in question is "normal" but will be running in the context of a mix of users where some users really ought not see some information in it whilst others can. We're not talking classified material here (if we were, of course, it would be on a classified machine and network), but still. Go ahead and use the Identity I was given? Do my miniscule check in the question? Unmanaged code? It *can* be tricked, I'm just trying to find the line... :-) – T.J. Crowder Nov 08 '15 at 07:59
  • 1
    Ha that's is the real question. What I'd do (and actually did) is write a small piece of C/C++ using the Windows API. It still can be hacked but blows all .NET guys (and youngsters who think a guid is created by goblins hidden in the machine). In this C++ code, you can check the process strong name also. – Simon Mourier Nov 08 '15 at 08:07
  • :-) That was indeed the one step further I had in mind, unmanaged code directly calling the Windows API. Still hackable, just making the bar (slightly) higher. With apologies, is the starting point there the user token (`.Token` in the code in the question)? – T.J. Crowder Nov 08 '15 at 08:16
  • sorry, not sure what you mean by that? (ps: in C++ code, you want to detect Fakes also - I didn't research how to do it) – Simon Mourier Nov 08 '15 at 08:22
  • Let's say my API accepts a `WindowsIdentity` if you want us to do SSO and I want to do reasonable validation of that. (Or perhaps it should be accepting something else?) I know *nothing* of the Windows API around authentication, but user tokens seem to be the key thing. Would I pass my "small bit of C/C++ code" the user token as the starting point of its checks? – T.J. Crowder Nov 08 '15 at 08:27
  • It really depends on what your program does I guess. The thing is you want to put something vital in that c++ thing, something that can't be bypassed (ie don't return a boolean or anything else that we can fake from .NET). Microsoft Fakes is a good playground/bench for what can be done from .NET. – Simon Mourier Nov 08 '15 at 16:56
  • :-) I don't think I'm being clear. I was asking what I would pass to the C/C++ code to allow it to check that the user was authenticated. I figured `.Token` because that's the unmanaged user token. But I'll do my research and if necessary post a question about that, it's pretty off-topic from where we started. *Merci pour toute l'aide*, Simon ! – T.J. Crowder Nov 08 '15 at 17:40