본문 바로가기

툴-팁

[번역] SID 보고 알아내기

반응형
If you see SID, tell him
SID 보고 알아내기


Published Monday, March 17, 2008 7:29 PM
2008년 3월 17일
번역 2010년 12월 7일

As a small addendum to my previous blog on the subject of authenticating users, and checking for administrator privileges, under Windows 2000, XP and Vista, I should add this little note. It turns out, thanks to the sort of heavily industrious testing that's par for the course here at Red Gate, that LogonUser / SSPI has a habit under certain circumstances of accepting invalid logins. When a computer is not on a domain, but a workgroup, credentials validation seems to take a different route with respect to validating the existence of the domain to which the login belongs. Specifically, if it can find a local account matching the username and password from the (username,password,domain) tuple, then if not on a domain, the domain part is often summarily ignored and authentication says "yes, that's fine, this user is acceptable".

윈도우 2000, XP, 비스타에서 사용자 인증과 관리자 권한 체크에 괜해 썼던 포스팅에 대한 보충으로 이 글을 쓴다. 열심히 테스트해 본 결과 LogonUser / SSPI 가 어떤 경우에 유효하지 않은 로그인을 받아들인다는 걸 알게됐다. 컴퓨터가 도메인에 있지 않고, 워크그룹에 있으면, 다른 방식으로 사용자 확인을 한다는 것을 알았다. 자세히 말하자면, (사용자명, 암호, 도메인) 튜플에서 사용자명과 암호가 맞고 도메인은 안 맞는 로컬 계정이 있으면, 도메인은 종종 무시되고, 인증은 성공한다.

I suspect it does this for some good reason. Exactly what that reason is I couldn't say, though I wouldn't be surprised to find that it was something in the area of permitting users to easily access machines on a workgroup provided their usernames and passwords match.

이렇게 하는 합리적인 이유가 있는 것 같다. 정확한 이유는 모르겠지만, 사용자명과 암호가 맞으면 워크그룹 머신에 쉽게 접근하도록 하기 위한 것이라면 난 놀라지 않은 것이다.

This is all very fine and splendid until one comes to use the login for other purposes. The CreateService API, for example, doesn't find it as amusing as the authentication APIs with regard to accepting invalid domains.

So in order to make the previous authentication code robust, we have to do some more legwork. After succeeding with the LogonUser / SSPI APIs, we need to manually verify the correctness of the domain before treating the user provided credentials as correct.

Once again this solution involved a pinch of Google and a dash of experimentation. On the way I learned a little more about SIDs, which I discussed previously. The string format of a SID, it turns out, is a very literal interpretation of the "black box" contents of the SID data structure. It's described in some detail on MSDN, but briefly each SID is composed as follows:

SID의 문자열 형식은 SID 데이터 구조체를 구체적으로 써 놓은 것이다. MSDN에 설명되어 있지만, 간단히 말하면 SID는 다음과 같다.

    S-<revision>-<identifier authority>-<first sub authority>-<second sub authority>[-<third sub authority> ... ]

    S-<리비전>-<식별자 권한>-<첫번째 서브 권한>-<두번째 서브 권한>[-<세번째 서브 권한> ... ]

All SIDs to date are revision 1, so we always start "S-1-...". The identifier authority tells us where the SID was originally issued, in broad terms. The subsequent sub authorities are also known as relative identifiers, or RIDs.

현재의 모든 SID 리비전은 1이다. 그래서 우린 언제나 "S-1-..."로 시작하는 SID만 보게 된다. 식별자 권한은 SID가 최초에 어디서 발행되었는지를 알려준다. 다음에 오는 서브 권한들은 상대 식별자인 RID이다.

Now a user's SID looks like the following:

자 사용자의 SID는 아래와 같이 생겼다:

    S-1-5-xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-nnnn

S-1-5 means "SID version 1, issued by "NT Authority" (the originator of pretty much all user, computer and domain SIDs). It turns out that "xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx" is the standard format for the RID of a computer or domain. "nnnn" is a number indicating which user we're dealing with on that domain.

S-1-5 는 "SID 버전 1, "NT Authority"(대부분의 사용자, 컴퓨터, 도메인 SID의 근원) 발행"이란 뜻이다. "xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx"가 컴퓨터나 도메인 RID의 표준 형식인 것으로 보인다. "nnnn"은 도메인에서 어떤 사용자를 말하는지를 나타낸다.

Domains and computers themselves have valid SIDs. This is understandable, given that a SID is pretty universal as the identifier used to identify something for security purposes. Can you guess what a computer or domain SID looks like?

도메인과 컴퓨터도 유효한 SID를 갖는다. SID가 식별자로서 보안을 위해 무언가를 식별하기 위해 굉장히 광범위하게 쓰인다는 것을 생각하면 이해되는 부분이다. 컴퓨터나 도메인의 SID가 어떻게 생겼는지 짐작해 볼 수 있을까?

    S-1-5-xx-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx

That's right - we just lop the last sub-authority off the end.

맞다. 우린 그저 서브 권한의 마지막 부분만 날려버리면 된다.

What are the magic numbers in the "xx-xxxxxxxxx-...." portion? Well, they're unique to the computer or domain in question. A computer's SID is generated when Windows is installed, and stay the same for its lifetime. (This has actually led to issues in companies which literally clone machines for deployment purposes; they'd end up with identical SIDs, leading to confusion between users and other fun. SysInternals provide an handy utility to change a computer's SID in this sort of situation.)  A domain's SID is generated when the domain is set up. I suspect that it is the computer SID of the first (chronologically) domain controller on the domain (the primary domain controller in pre Windows 2000 terms, before domain controllers started taking joint and several liability for domain security, particularly assigning SIDs) but I haven't got any evidence for that.  Certainly, in the good old days, the primary and backup domain controllers had to have the same computer ID, which would seem to fit.

그럼 "xx-xxxxxxxxx-...." 부분의 번호는 무슨 값일까? 음, 그건 컴퓨터나 도메인에 따라 유일한 값이다. 컴퓨터의 SID는 윈도우가 설치되면서 생성되고, 끝까지 동일하게 유지된다. (이 때문에 운영체제를 복사해서 사용하는 회사에서 문제가 발생하기도 한다. 왜냐면 모두 같은 SID를 갖기 때문이다. 시스인터널즈는 이런 경우에 컴퓨터의 SID를 바꿔줄 수 있는 툴을 제공한다.) 도메인의 SID는 도메인이 설정될 때 만들어진다. 내가 짐작하기로는 도메인의 (시간적으로) 첫번째 도메인 컨트롤러 컴퓨터의 SID일 거라고 생각하는데, 확실한 건 아니다. 확실하게 예전에는 프라이머리와 백업 도메인 컨트롤러는 같은 컴퓨터 ID를 가져야 했다.

The upshot of this discussion is that once a user is logged in it's quite easy to determine whether the account in question is a user account or a domain account, provided one has the SIDs for the domain or computer. One simply lops off the last "-nnnn" portion of the SID, and then can compare SIDs either the nice way via the EqualSID() API or, if you're feeling hacky, by strcmp'ing the SID strings.

이 논의에서 중요한 것은. 사용자가 로그인되었을 때, 도메인이나 컴퓨터의 SID를 알고 있다면, 계정이 사용자 계정인지 도메인 계정인지를 쉽게 결정할 수 있다는 것이다. SID의 마지막 "-nnnn" 부분만 날려버리고 SID를 EqualSID API나 strcmp를 이용해서 비교할 수 있다.

When looking at this problem initially, my plan was to do just that. Initially I couldn't find an easy way to get hold of a computer or domain SID. I then tripped over the psgetsid tool from SysInternals.

This tool is capable of converting account, machine and Windows domain names to SIDs, and SIDs to names, on local or remote computers. A quick dumpbin revealed how it does it:

    C:\> vcvars32.bat
    Setting environment for using Microsoft Visual Studio 2005 x86 tools.

    C:\> dumpbin d:\data\sysinternals\psgetsid.exe /imports | more
    Microsoft (R) COFF/PE Dumper Version 8.00.50727.762
    Copyright (C) Microsoft Corporation.  All rights reserved.

    Dump of file d:\data\sysinternals\psgetsid.exe

    File Type: EXECUTABLE IMAGE

      Section contains the following imports:

      ...
        ADVAPI32.dll
                    40A000 Import Address Table
                    40A7C4 Import Name Table
                         0 time date stamp
                         0 Index of first forwarder reference

                      140 IsValidSid
                      116 GetSidIdentifierAuthority
                      119 GetSidSubAuthorityCount
                      118 GetSidSubAuthority
                       1D AllocateAndInitializeSid
                       AF DeleteService
                       42 ControlService
                      1AD OpenSCManagerA
                      1AF OpenServiceA
                      249 StartServiceA
                      1C3 QueryServiceStatus
                       64 CreateServiceA
                       3E CloseServiceHandle
                      149 LookupAccountSidA
                      147 LookupAccountNameA

LookupAccountSid and LookupAccountName are the important APIs here. LookupAccountSid takes a SID and returns its name. LookupAccountName takes a name and returns its SID. In both cases invalid names/SIDs are picked up.

This last property made my job even simpler. All I need to know is whether the domain part of the user supplied (username,password,domain) is in some sense valid. So all I need to do is ti call LookupAccountName to fetch the SID for that domain or computer. If it fails, then I reject the login.

To get this to work, I added the following method to my security classes (adapted, as always, from the internet via cut and paste, followed by some tidying and/or bug fixing):

            public static void GetSidForAccountOrDomain(string strAccountName,
                 out string accountSid, out string strDomainName,
                 out short AccountType)
            {
                int lSidSize;
                int lDomainNameSize;
                IntPtr Sid = IntPtr.Zero;
                string strServer = null;

                // First get the required buffer sizes for SID and domain name.
                if (!NativeSecurityApis.LookupAccountName(
                                    strServer,
                                    strAccountName,
                                    Sid,
                                    ref lSidSize,
                                    null,
                                    ref lDomainNameSize,
                                    ref AccountType))
                {
                    if (Marshal.GetLastWin32Error() == NativeSecurityApis.ERROR_INSUFFICIENT_BUFFER)
                    {
                        // Allocate the buffers with actual sizes that are required
                        // for SID and domain name.
                        strName = new StringBuilder(lDomainNameSize);
                        Sid = Marshal.AllocHGlobal(lSidSize);
                        if (!NativeSecurityApis.LookupAccountName(
                                  strServer,
                                  strAccountName,
                                  Sid,
                                  ref lSidSize,
                                  strName,
                                  ref lDomainNameSize,
                                  ref AccountType))
                            throw new Win32Exception(); // last error
                    }
                    else
                        throw new Win32Exception(); // last error
                }
                else
                    throw new InvalidOperationException("Expected LookupAccountName to fail given no buffers"); // shouldn't get here

                strDomainName = strName.ToString();

                IntPtr pString;
                if (!NativeSecurityApis.ConvertSidToStringSid(Sid, out pString))
                    throw new Win32Exception(); // last error

                accountSid = Marshal.PtrToStringAuto(pString);
                NativeSecurityApis.LocalFree(pString);

                //Console.WriteLine("Domain Name: {0}", strDomainName);
                //Console.WriteLine("Account Sid: {0}", sidText);
                Marshal.FreeHGlobal(Sid);
            }
        }

Then, a more than elementary wrapper to ensure that a given domain is valid:

            private static void VerifyDomain(IntPtr hToken, string domain)
            {
                if (string.IsNullOrEmpty(domain) || domain == ".")
                    return; // don't attempt to verify null or local (BUILTIN\.) domain

                // attempt to look up the SID of the "domain", be it an actual domain
                // or a computer. If we succeed, then it's valid and LogonUser() should
                // pick
                try
                {
                    string domainSid;
                    string canonicalDomainName;
                    short accountType;
                    GetSidForAccountOrDomain(domain, out domainSid, out canonicalDomainName, out accountType);
                }
                catch (Exception ex)
                {
                    throw new LogonException(string.Format("Cannot verify domain {0} is valid.", domain), ex);
                }
            }

 Then I simply modified the IsUserAdmin() function described in my previous entry, so it now reads as follows:

        public static bool IsUserAdmin(string userName, string domain, string password)
        {
            IntPtr hToken;
            if (NativeSecurityApis.LogonUser(userName, domain, password, NativeSecurityApis.LOGON32_LOGON_INTERACTIVE, NativeSecurityApis.LOGON32_PROVIDER_DEFAULT, out hToken))
            {
                try
                {
                    VerifyDomain(hToken, domain);

                    return IsUserAdmin(hToken);
                }
                finally
                {
                    NativeSecurityApis.CloseHandle(hToken);
                }
            }
            else
            {
                try
                {
                    WindowsPrincipal principal = Win32SSPI.LogonUser(userName, domain, password);

                    IntPtr hPrincipalToken = (principal.Identity as WindowsIdentity).Token;
                    VerifyDomain(hPrincipalToken, domain);

                    return IsUserAdmin(principal);
                }
                catch (Exception ex)
                {
                    throw new LogonException(ex.Message, ex);
                }
            }
        }
 

So that was pretty straightforward. At least, relative to the prior steps I describe in my previous blog entries. So far the C# code to authenticate a user, and check that they are an administrator, is still sub 1000 lines including whitespace, comments and API import definitions. Cheap at the price?

728x90