[Win32] [C++] CreateProcessAsUser – #3 ソース

小出しにしてきたプログラムの全貌です。ファイルは 5 つに分けました。
2 月のプログラムに、幾つかの機能を加えてあります。1000 行近くまで膨らんでしまった・・・。

  • main.cpp – エントリ ポイント、引数のパース
  • logue.cpp – メイン ルーチン
  • priv.cpp – 特権関係
  • dacl.cpp – DACL 関係
  • logue.h – 共通ヘッダー

(2014/12/29 追記)
HTML として貼り付けているだけと読みづらいので (検索でヒットしやすいというメリットはありますが・・)、ソースを GitHub に置きました。Makefile も作りましたので、Visual Studio のコマンド プロンプトから nmake コマンドをパラメーターなしで実行するだけでビルドできます。

https://github.com/msmania/logue

main.cpp

//
// main.cpp
//

#include <iostream>
#include <stdio.h>
#include <locale.h>
#include <Windows.h>

#include "logue.h"

using namespace std;

/*

Usage: logue -runas <user> <password> <command>
       logue -priv Enum
       logue -priv Add <privilege>
       logue -priv Check <privilege>
       logue -priv Enable <privilege>
       logue -priv Disable <privilege>

Example:
    logue domain\user password "c:\windows\system32\notepad.exe c:\temp\temp.txt"
    logue -priv check SeSecurityPrivilege

Privilege: http://msdn.microsoft.com/en-us/library/bb530716.aspx

*/

void ShowUsage() {
    wcout << L"\nUsage: logue -runas <user> <password> <command>" << endl;
    wcout << L"       logue -priv All" << endl;
    wcout << L"       logue -priv Enum" << endl;
    wcout << L"       logue -priv Add <privilege>" << endl;
    wcout << L"       logue -priv Check <privilege>" << endl;
    wcout << L"       logue -priv Enable <privilege>" << endl;
    wcout << L"       logue -priv Disable <privilege>" << endl << endl;
    wcout << L"Example:" << endl;
    wcout << L"    logue -runas domain\\user password \"c:\\windows\\system32\\notepad.exe c:\\temp\\temp.txt\"" << endl;
    wcout << L"    logue -priv check SeSecurityPrivilege" << endl << endl;
    wcout << L"Privilege:
http://msdn.microsoft.com/en-us/library/bb530716.aspx" << endl << endl;
}

#define MAX_COMMAND 16

static wchar_t upperstr[MAX_COMMAND+1];
const wchar_t *ToUpper(const wchar_t *s) {
    for ( int i=0 ; i<MAX_COMMAND+1 ; ++i ) {
        upperstr[i]= toupper(s[i]);
        if ( s[i]==0 )
            return upperstr;
    }
    upperstr[MAX_COMMAND]= 0;
    return upperstr;
}

int wmain(int argc, wchar_t *argv[]) {
    _wsetlocale(LC_ALL, L"");
   
    if ( argc<3 ) {
        ShowUsage();
        return ERROR_INVALID_PARAMETER;
    }

    LPCWSTR Command= ToUpper(argv[1]);
    if ( wcscmp(Command, L"-RUNAS")==0 ) {
        if ( argc<5 ) {
            ShowUsage();
            return ERROR_INVALID_PARAMETER;
        }

        RunAs(argv[2], argv[3], argv[4]);
    }
    else if ( wcscmp(Command, L"-PRIV")==0 ) {
        HANDLE Token= NULL;
        if ( !OpenProcessToken(GetCurrentProcess(),
                               TOKEN_ALL_ACCESS , &Token) ) {
            wprintf(L"OpenProcessToken failed – 0x%08x\n", GetLastError());
            return 0;
        }

        Command= ToUpper(argv[2]);
        if ( wcscmp(Command, L"ENUM")==0 )
            EnumPrivileges(Token, FALSE);
        else if ( wcscmp(Command, L"ALL")==0 )
            EnumPrivileges(Token, TRUE);
        else if ( argc>=4 && wcscmp(Command, L"ADD")==0 )
            AddPrivilege(Token, argv[3]);
        else if ( argc>=4 && wcscmp(Command, L"CHECK")==0 ) {
            LONG Ret= 0;
            if ( CheckPrivilege(Token, argv[3], &Ret) )
                wprintf(L"%s is %s.\n", argv[3],
                    Ret>0 ? L"ENABLED" :
                    Ret<0 ? L"NOT ASSIGNED" : L"DISABLED");
        }
        else if ( argc>=4 && wcscmp(Command, L"ENABLE")==0 ) {
            if ( EnablePrivilege(Token, argv[3], TRUE) )
                EnumPrivileges(Token, FALSE);
        }
        else if ( argc>=4 && wcscmp(Command, L"DISABLE")==0 ) {
            if ( EnablePrivilege(Token, argv[3], FALSE) )
                EnumPrivileges(Token, FALSE);
        }
        else {
            wprintf(L"Bad command – %s\n", argv[1]);
            return ERROR_BAD_COMMAND;
        }

        if ( Token ) CloseHandle(Token);
    }
    else {
        ShowUsage();
        wprintf(L"Unknown command – %s\n", argv[1]);
        return ERROR_INVALID_PARAMETER;
    }

    return 0;
}

logue.cpp

//
// logue.cpp
//
// base sample:
//
http://msdn.microsoft.com/en-us/library/aa379608(v=vs.85).aspx
//

#include <windows.h>
#include <stdio.h>

#include "logue.h"

VOID RunAs(LPWSTR inUser, LPWSTR inPW, LPWSTR inCommand) {
    HANDLE CallerToken= NULL;
    HANDLE CalleeToken= NULL;
    HWINSTA WinstaOld= NULL;
    HWINSTA Winsta0= NULL;
    HDESK Desktop= NULL;
    PSID LogonSid= NULL;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    LONG PrivCheck= 0;
   
    if ( !OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS,
            &CallerToken) ) {
        wprintf(L"OpenProcessToken failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }
   
    CheckPrivilege(CallerToken, SE_INCREASE_QUOTA_NAME, &PrivCheck);
    if ( PrivCheck<0 )
        wprintf(L"CreateProcessAsUser requires %s.  Check the user’s privileges.\n", SE_INCREASE_QUOTA_NAME);

    CheckPrivilege(CallerToken, SE_ASSIGNPRIMARYTOKEN_NAME, &PrivCheck);
    if ( PrivCheck<0 )
        wprintf(L"CreateProcessAsUser requires %s.  Check the user’s privileges.\n", SE_ASSIGNPRIMARYTOKEN_NAME);
   
    if ( !LogonUser(inUser, NULL, inPW, LOGON32_LOGON_INTERACTIVE,
            LOGON32_PROVIDER_DEFAULT, &CalleeToken) ) {
        wprintf(L"LogonUser failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

#ifdef _GUI
    Winsta0= OpenWindowStation(L"winsta0", FALSE, READ_CONTROL|WRITE_DAC);
    if ( !Winsta0 ) {
        wprintf(L"OpenWindowStation failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

    WinstaOld= GetProcessWindowStation();
    if ( !SetProcessWindowStation(Winsta0) ) {
        wprintf(L"SetProcessWindowStation failed – 0x%08x\n",
                GetLastError());
        goto Cleanup;
    }
    Desktop= OpenDesktop(L"default", 0, FALSE,
        READ_CONTROL|WRITE_DAC|DESKTOP_WRITEOBJECTS|DESKTOP_READOBJECTS);
    SetProcessWindowStation(WinstaOld);
    if ( !Desktop ) {
        wprintf(L"OpenDesktop failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }
   
    if ( !GetLogonSidFromToken(CalleeToken, &LogonSid) )
        goto Cleanup;
   
#ifdef _TRACING
    wprintf(L"PID      : 0x%x\n", GetCurrentProcessId());
    wprintf(L"HWINSTA  : 0x%x\n", Winsta0);
    wprintf(L"HDESK    : 0x%x\n", Desktop);
    wprintf(L"Logon SID: %p\n", LogonSid);
    wprintf(L"—–\n");
    getwchar();
#endif

    if ( !AddAceToWindowStation(Winsta0, LogonSid) ) {
        wprintf(L"AddAceToWindowStation failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

    if ( !AddAceToDesktop(Desktop, LogonSid) ) {
        wprintf(L"AddAceToDesktop failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }
#endif
   
    ZeroMemory(&si, sizeof(STARTUPINFO));
    si.cb= sizeof(STARTUPINFO);

#ifdef _GUI
    si.lpDesktop= L"winsta0\\default";
#else
    si.lpDesktop= L"";
#endif
   
    if ( !ImpersonateLoggedOnUser(CalleeToken) ) {
        wprintf(L"ImpersonateLoggedOnUser failed – 0x%08x\n",
                GetLastError());
        goto Cleanup;
    }

    if ( !CreateProcessAsUser(CalleeToken, NULL, inCommand, NULL, NULL,
                              FALSE, 0, NULL, NULL, &si, &pi) ) {
        wprintf(L"CreateProcessAsUser failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }
   
    WaitForSingleObject(pi.hProcess, INFINITE);
   
    RevertToSelf();

#ifdef _GUI
    RemoveAccessAllowedAcesBasedSID(Winsta0, LogonSid);
    RemoveAccessAllowedAcesBasedSID(Desktop, LogonSid);
#endif
   
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);

Cleanup:
    if ( LogonSid ) HeapFree(GetProcessHeap(), 0, LogonSid);
    if ( Winsta0 ) CloseWindowStation(Winsta0);
    if ( Desktop ) CloseDesktop(Desktop);
    if ( CalleeToken ) CloseHandle(CalleeToken);
    if ( CallerToken ) CloseHandle(CallerToken);
}

priv.cpp

//
// priv.cpp
//

#include <windows.h>
#include <NTSecAPI.h>
#include <stdio.h>
#include <strsafe.h>

#include "logue.h"

#define MAX_PRIVNAME 32
#define MAX_PRIVSCAN 256

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth

struct PRIVILAGENAME_MAPPING {
    WCHAR SymbolName[MAX_PRIVNAME];
    WCHAR PrivilegeName[MAX_PRIVNAME];
};

const PRIVILAGENAME_MAPPING PrivilegeNameMapping[]= {
    { L"SE_CREATE_TOKEN_NAME", SE_CREATE_TOKEN_NAME },
    { L"SE_ASSIGNPRIMARYTOKEN_NAME", SE_ASSIGNPRIMARYTOKEN_NAME },
    { L"SE_LOCK_MEMORY_NAME", SE_LOCK_MEMORY_NAME },
    { L"SE_INCREASE_QUOTA_NAME", SE_INCREASE_QUOTA_NAME },
    { L"SE_UNSOLICITED_INPUT_NAME", SE_UNSOLICITED_INPUT_NAME }, // no LUID?
    { L"SE_MACHINE_ACCOUNT_NAME", SE_MACHINE_ACCOUNT_NAME },
    { L"SE_TCB_NAME", SE_TCB_NAME },
    { L"SE_SECURITY_NAME", SE_SECURITY_NAME },
    { L"SE_TAKE_OWNERSHIP_NAME", SE_TAKE_OWNERSHIP_NAME },
    { L"SE_LOAD_DRIVER_NAME", SE_LOAD_DRIVER_NAME },
    { L"SE_SYSTEM_PROFILE_NAME", SE_SYSTEM_PROFILE_NAME },
    { L"SE_SYSTEMTIME_NAME", SE_SYSTEMTIME_NAME },
    { L"SE_PROF_SINGLE_PROCESS_NAME", SE_PROF_SINGLE_PROCESS_NAME },
    { L"SE_INC_BASE_PRIORITY_NAME", SE_INC_BASE_PRIORITY_NAME },
    { L"SE_CREATE_PAGEFILE_NAME", SE_CREATE_PAGEFILE_NAME },
    { L"SE_CREATE_PERMANENT_NAME", SE_CREATE_PERMANENT_NAME },
    { L"SE_BACKUP_NAME", SE_BACKUP_NAME },
    { L"SE_RESTORE_NAME", SE_RESTORE_NAME },
    { L"SE_SHUTDOWN_NAME", SE_SHUTDOWN_NAME },
    { L"SE_DEBUG_NAME", SE_DEBUG_NAME },
    { L"SE_AUDIT_NAME", SE_AUDIT_NAME },
    { L"SE_SYSTEM_ENVIRONMENT_NAME", SE_SYSTEM_ENVIRONMENT_NAME },
    { L"SE_CHANGE_NOTIFY_NAME", SE_CHANGE_NOTIFY_NAME },
    { L"SE_REMOTE_SHUTDOWN_NAME", SE_REMOTE_SHUTDOWN_NAME },
    { L"SE_UNDOCK_NAME", SE_UNDOCK_NAME },
    { L"SE_SYNC_AGENT_NAME", SE_SYNC_AGENT_NAME },
    { L"SE_ENABLE_DELEGATION_NAME", SE_ENABLE_DELEGATION_NAME },
    { L"SE_MANAGE_VOLUME_NAME", SE_MANAGE_VOLUME_NAME },
    { L"SE_IMPERSONATE_NAME", SE_IMPERSONATE_NAME },
    { L"SE_CREATE_GLOBAL_NAME", SE_CREATE_GLOBAL_NAME },
    { L"SE_TRUSTED_CREDMAN_ACCESS_NAME", SE_TRUSTED_CREDMAN_ACCESS_NAME },
    { L"SE_RELABEL_NAME", SE_RELABEL_NAME },
    { L"SE_INC_WORKING_SET_NAME", SE_INC_WORKING_SET_NAME },
    { L"SE_TIME_ZONE_NAME", SE_TIME_ZONE_NAME },
    { L"SE_CREATE_SYMBOLIC_LINK_NAME", SE_CREATE_SYMBOLIC_LINK_NAME },
    { L"", L"" }
};

BOOL LookupPrivilegeName(LPCWSTR SystemName,
                        CONST PLUID Luid, LPCWSTR *SymbolName,
                        LPWSTR PrivilegeName, LPDWORD PrivilegeNameLength,
                        LPWSTR DisplayName, LPDWORD DisplayNameLength,
                        BOOL NoErrMsg) {
    BOOL Ret= FALSE;
    DWORD LanguageId;
    int Index= -1;

    Ret= LookupPrivilegeName(NULL, Luid, PrivilegeName, PrivilegeNameLength);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeName failed – 0x%08x\n",
                    GetLastError());
        goto cleanup;
    }

    Ret= LookupPrivilegeDisplayName(NULL,
        PrivilegeName, DisplayName, DisplayNameLength, &LanguageId);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeDisplayName failed – 0x%08x\n",
                    GetLastError());
        goto cleanup;
    }

    Ret= FALSE;
    const PRIVILAGENAME_MAPPING *p=PrivilegeNameMapping;
    for ( Index=0 ; p->SymbolName[0]!=0 ; ++p, ++Index ) {
        if ( wcscmp(PrivilegeName, p->PrivilegeName)==0 ) {
            Ret= TRUE;
            break;
        }
    }

    if ( Ret )
        *SymbolName= PrivilegeNameMapping[Index].SymbolName;
    else if ( NoErrMsg )
        wprintf(L"%s not found\n", PrivilegeName);

cleanup:
    return Ret;
}

BOOL LookupPrivilegeValueEx(LPCWSTR SystemName, LPCWSTR Name, PLUID Luid) {
    BOOL Ret= LookupPrivilegeValue(SystemName, Name, Luid);
    if ( !Ret && GetLastError()==ERROR_NO_SUCH_PRIVILEGE ) {
        const PRIVILAGENAME_MAPPING *p;
        for ( p=PrivilegeNameMapping ; p->SymbolName[0]!=0 ; ++p ) {
            if ( wcscmp(Name, p->SymbolName)==0 )
                return LookupPrivilegeValue(SystemName,
                       p->PrivilegeName, Luid);
        }
        SetLastError(ERROR_NO_SUCH_PRIVILEGE);
        Ret= FALSE;
    }
    return Ret;
}

VOID EnumPrivileges(HANDLE Token, BOOL All) {
    BOOL Ret= FALSE;
    DWORD TokenLength= 0;
    PTOKEN_PRIVILEGES TokenPriv= NULL;
    DWORD PrivilegeNameLength= 256;
    DWORD DisplayNameLength= 256;
    PWCHAR PrivilegeName= NULL;
    PWCHAR DisplayName= NULL;
    LPCWCHAR SymbolName= NULL;
   
    // LUID = Locally Unique Identifier
    wprintf(L"——————————————————————————————————-\n");
    wprintf(L"   LUID                Symbol                           PrivilegeName                    DisplayName\n");
    wprintf(L"——————————————————————————————————-\n");

    if ( !All ) {
        if ( !GetTokenInformation(Token, TokenPrivileges,
                NULL, 0, &TokenLength) &&
                GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
            wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n",
                    GetLastError());
            goto cleanup;
        }

        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(),
                    0, TokenLength);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }

        if ( !GetTokenInformation(Token, TokenPrivileges, TokenPriv,
                  TokenLength, &TokenLength) ) {
            wprintf(L"GetTokenInformation failed – 0x%08x\n",
                    GetLastError());
            goto cleanup;
        }

    }
    else {
        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), 0,
            sizeof(DWORD)+sizeof(LUID_AND_ATTRIBUTES)*MAX_PRIVSCAN);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }
       
        TokenPriv->PrivilegeCount= MAX_PRIVSCAN;
        for (  LONGLONG i=0 ; i<MAX_PRIVSCAN ; ++i ) {
            TokenPriv->Privileges[i].Luid= *(PLUID)&i;
            TokenPriv->Privileges[i].Attributes= 0;
        }
    }
   
    for ( DWORD i=0 ; i<TokenPriv->PrivilegeCount ; ++i ) {
        do {
            if ( PrivilegeName ) delete [] PrivilegeName;
            if ( DisplayName ) delete [] DisplayName;

            PrivilegeName= new WCHAR[PrivilegeNameLength];
            DisplayName= new WCHAR[DisplayNameLength];

            Ret= LookupPrivilegeName(NULL,
                &TokenPriv->Privileges[i].Luid, &SymbolName,
                    PrivilegeName, &PrivilegeNameLength,
                    DisplayName, &DisplayNameLength,
                    All);
        } while( !Ret && GetLastError()==ERROR_INSUFFICIENT_BUFFER );

        if ( Ret ) {
            WCHAR Mark= 0;
            if ( All ) {
                LONG l= 0;
                CheckPrivilege(Token, PrivilegeName, &l);
                Mark= l==0 ? Mark= ‘X’ :
                    l>0 ? Mark= ‘O’ : ‘-‘;
            }
            else {
                Mark= TokenPriv->Privileges[i].Attributes&SE_PRIVILEGE_ENABLED ? L’O’ : L’X’;
            }

            wprintf(L" %c 0x%08x`%08x %-32s %-32s %s\n", Mark,
                TokenPriv->Privileges[i].Luid.HighPart,
                TokenPriv->Privileges[i].Luid.LowPart,
                SymbolName,
                PrivilegeName,
                DisplayName);
        }
    }

cleanup:
    if ( PrivilegeName ) delete [] PrivilegeName;
    if ( DisplayName ) delete [] DisplayName;
    if ( TokenPriv ) HeapFree(GetProcessHeap(), 0, TokenPriv);
}

// http://msdn.microsoft.com/en-us/library/ms722492(v=VS.85) InitLsaString
//
http://msdn.microsoft.com/en-us/library/ms721874(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/ms721863(v=vs.85).aspx
BOOL AddPrivilege(HANDLE Token, LPCWSTR PrivilegeName) {
    NTSTATUS Ret= 0;
    LSA_OBJECT_ATTRIBUTES ObjectAttributes;
    LSA_HANDLE PolicyHandle= NULL;
    PSID Sid= NULL;
    LSA_UNICODE_STRING Privilege[1];
    size_t PrivNameLength= 0;
    PTOKEN_USER CurrentUserSid= NULL;
    DWORD CurrentUserSidLength= 0;

    // get current user SID from the token
    if ( !GetTokenInformation(Token, TokenUser, NULL, 0,
            &CurrentUserSidLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n",
                GetLastError());
        goto cleanup;
    }

    CurrentUserSid= (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0,
                    CurrentUserSidLength);
    if ( !CurrentUserSid ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    if ( !GetTokenInformation(Token, TokenUser, CurrentUserSid,
            CurrentUserSidLength, &CurrentUserSidLength) ) {
        wprintf(L"GetTokenInformation failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }
   
    PrivNameLength= StringCchLength(PrivilegeName,
        MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);
   
    ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes));
    Ret= LsaOpenPolicy(NULL, &ObjectAttributes, POLICY_ALL_ACCESS,
                       &PolicyHandle);
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaOpenPolicy failed – 0x%08x\n",
                LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    StringCchLength(PrivilegeName, MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);

    Ret= LsaAddAccountRights(PolicyHandle,
        CurrentUserSid->User.Sid, Privilege, 1);;
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaAddAccountRights failed – 0x%08x\n",
            LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    wprintf(L"Privilege ‘%s’ was assigned successfully.\n", PrivilegeName);
    wprintf(L"To apply it to the token, re-log on the system.\n");

cleanup:
    if ( PolicyHandle ) LsaClose(PolicyHandle);   
    if ( CurrentUserSid ) HeapFree(GetProcessHeap(), 0, CurrentUserSid);

    return Ret==STATUS_SUCCESS;
}

// >0 Enabled
// =0 Disabled
// <0 Not assigned

BOOL CheckPrivilege(HANDLE Token, LPCWSTR PrivilegeName, LPLONG Privileged) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, PrivilegeName, &luid) ){
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }

    PRIVILEGE_SET PrivilegeSet;
    PrivilegeSet.Control= 0;
    PrivilegeSet.PrivilegeCount= 1;
    PrivilegeSet.Privilege[0].Luid= luid;
    PrivilegeSet.Privilege[0].Attributes= 0; // not used

    BOOL Check;
    if ( !PrivilegeCheck(Token, &PrivilegeSet, &Check) ) {
        wprintf(L"PrivilegeCheck failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( Check )
        *Privileged= 1;
    else {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount= 1;
        tp.Privileges[0].Luid= luid;
        tp.Privileges[0].Attributes= 0;

        if ( !AdjustTokenPrivileges(Token, FALSE, &tp,
                sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
            wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n",
                    GetLastError());
            return FALSE;
        }

        *Privileged= (GetLastError()==ERROR_NOT_ALL_ASSIGNED) ? -1 : 0;
    }

    return TRUE;
}
 
BOOL EnablePrivilege(HANDLE Token, LPWSTR Name, BOOL Enabled) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, Name, &luid) ) {
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    TOKEN_PRIVILEGES tp;
    tp.PrivilegeCount= 1;
    tp.Privileges[0].Luid= luid;
    tp.Privileges[0].Attributes=
      Enabled ? SE_PRIVILEGE_ENABLED : 0;
      // not use SE_PRIVILEGE_REMOVED, just disable

    if ( !AdjustTokenPrivileges(Token, FALSE, &tp,
            sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
        wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( GetLastError()==ERROR_NOT_ALL_ASSIGNED ) {
        wprintf(L"The process token does not have %s (%I64d).\n",
                Name, luid);
        return FALSE;
    }

    wprintf(L"%s (%I64d) is temporarily %s.\n", Name, luid,
        Enabled ? L"enabled" : L"disabled");

    return TRUE;
}

dacl.cpp

//
// dacl.cpp
//

#include <Windows.h>
#include <Sddl.h>
#include <stdio.h>

#include "logue.h"

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL AddAccessAllowedAceBasedSID(HANDLE Object, PSID Sid, DWORD AceCount,
                                CONST DWORD AceFlags[],
                                CONST DWORD AccessMasks[]) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
              HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, SdSize,
                &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    AclSizeInfo.AclBytesInUse = sizeof(ACL);
    if ( Acl ) {
        if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo,
               sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
            goto cleanup;
    }

    // Create a new ACL
    // (original ACL + new ACCESS_ALLOWED_ACEs)
    AclSize= AclSizeInfo.AclBytesInUse +
        AceCount * (sizeof(ACCESS_ALLOWED_ACE) +
        GetLengthSid(Sid) – sizeof(DWORD));
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;

    // If DACL is present, copy all the ACEs to a new DACL.
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD, Ace,
                  ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }

    // Add new ACEs of specified SID to the DACL
    for ( DWORD i=0 ; i<AceCount ; ++i ) {
        if (!AddAccessAllowedAceEx(AclNew, ACL_REVISION, AceFlags[i],
                AccessMasks[i], Sid) )
            goto cleanup;
    }

    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough because
    // SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
      HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL RemoveAccessAllowedAcesBasedSID(HANDLE Object, PSID Sid) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
              HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, SdSize,
               &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    if ( !DaclPresent || !Acl || Acl->AceCount==0 ) {
        // nothing to do for Null DACL or Empty DACL
        //
http://technet.microsoft.com/ja-jp/query/aa379286
        Ret= TRUE;
        goto cleanup;
    }

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo,
           sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
        goto cleanup;

    // Create an ACL copy
    AclSize= AclSizeInfo.AclBytesInUse;
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;
   
    // do not copy ACCESS_ALLOWED_ACEs of the specified SID
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;
           
            if ( ((PACE_HEADER)Ace)->AceType==ACCESS_ALLOWED_ACE_TYPE &&
                    EqualSid(Sid, &((ACCESS_ALLOWED_ACE*)Ace)->SidStart) )
                continue;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD, Ace,
                  ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }
   
    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough because
    // SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
      HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

BOOL AddAceToDesktop(HDESK Desktop, PSID Sid) {
    CONST DWORD AccessMasks[1] = {
        DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU |
        DESKTOP_HOOKCONTROL | DESKTOP_JOURNALRECORD |
        DESKTOP_JOURNALPLAYBACK |
        DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP |
        STANDARD_RIGHTS_REQUIRED
    };

    DWORD AceFlags[1] = {0};

    return AddAccessAllowedAceBasedSID(Desktop, Sid,
      1, AceFlags, AccessMasks);
}

BOOL AddAceToWindowStation(HWINSTA Winsta, PSID Sid) {
    CONST DWORD AccessMasks[2] = {
        GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | GENERIC_ALL,
        WINSTA_ENUMDESKTOPS | WINSTA_READATTRIBUTES |
        WINSTA_ACCESSCLIPBOARD |
        WINSTA_CREATEDESKTOP | WINSTA_WRITEATTRIBUTES |
        WINSTA_ACCESSGLOBALATOMS |
        WINSTA_EXITWINDOWS | WINSTA_ENUMERATE | WINSTA_READSCREEN |
        STANDARD_RIGHTS_REQUIRED};

    CONST DWORD AceFlags[2] = {
        CONTAINER_INHERIT_ACE|INHERIT_ONLY_ACE|OBJECT_INHERIT_ACE,
        NO_PROPAGATE_INHERIT_ACE
    };   

    return AddAccessAllowedAceBasedSID(Winsta, Sid,
       2, AceFlags, AccessMasks);
}

BOOL GetLogonSidFromToken(HANDLE Token, PSID *outSid) {
    BOOL Ret= FALSE;
    DWORD TokenGroupLength= 0;
    DWORD SidLength= 0;
    PTOKEN_GROUPS TokenGroup= NULL;
    LPWSTR SidString= NULL;

    if ( !GetTokenInformation(Token, TokenGroups, (LPVOID)TokenGroup,
            0, &TokenGroupLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (1st chance) failed – 0x%08x\n",
                GetLastError());
        goto Cleanup;
    }

    TokenGroup= (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(),
                 HEAP_ZERO_MEMORY, TokenGroupLength);
    if ( !TokenGroup ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

    if ( !GetTokenInformation(Token, TokenGroups, (LPVOID)TokenGroup,
            TokenGroupLength, &TokenGroupLength) ) {
        wprintf(L"GetTokenInformation failed (2nd chance) – 0x%08x\n",
          GetLastError());
        goto Cleanup;
    }

    //
    // SE_GROUP_LOGON_ID
    // The SID is a logon SID that identifies the logon session
    // associated with an access token.
    //
http://technet.microsoft.com/en-us/library/aa379624
    //
    for ( DWORD i=0 ; i<TokenGroup->GroupCount ; ++i ) {
        if ( SidString ) LocalFree(SidString);
        ConvertSidToStringSid(TokenGroup->Groups[i].Sid, &SidString);
        wprintf(L"SID: %s", SidString);

        if ( (TokenGroup->Groups[i].Attributes&SE_GROUP_LOGON_ID)
                ==SE_GROUP_LOGON_ID ) {
            SidLength= GetLengthSid(TokenGroup->Groups[i].Sid);

            *outSid= (PSID)HeapAlloc(GetProcessHeap(),
               HEAP_ZERO_MEMORY, SidLength);
            if ( *outSid==NULL ) {

                wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
                goto Cleanup;
            }

            if ( !CopySid(SidLength, *outSid, TokenGroup->Groups[i].Sid) ) {
                wprintf(L"CopySid failed – 0x%08x\n", GetLastError());
                HeapFree(GetProcessHeap(), 0, (LPVOID)*outSid);
                goto Cleanup;
            }

            wprintf(L" (Logon)\n");
            break;
        }
        else
            wprintf(L"\n");
    }
   
    Ret= TRUE;

Cleanup:
    if ( SidString ) LocalFree(SidString);
    if ( TokenGroup ) HeapFree(GetProcessHeap(), 0, TokenGroup);
    return Ret;
}

logue.h

//
// logue.h
//

#pragma once

#define _GUI
#define _TRACING

VOID EnumPrivileges(HANDLE Token, BOOL All);
BOOL AddPrivilege(HANDLE Token, LPCWSTR PrivilegeName);
BOOL EnablePrivilege(HANDLE Token, LPWSTR Name, BOOL Enabled);
BOOL CheckPrivilege(HANDLE Token, LPCWSTR PrivilegeName, LPLONG Privileged);

BOOL GetLogonSidFromToken(HANDLE Token, PSID *outSid);
BOOL AddAceToWindowStation(HWINSTA Winsta, PSID Sid);
BOOL AddAceToDesktop(HDESK Desktop, PSID Sid);
BOOL RemoveAccessAllowedAcesBasedSID(HANDLE Object, PSID Sid);

VOID RunAs(LPWSTR inUser, LPWSTR inPW, LPWSTR inCommand);

広告

[Win32] [C++] CreateProcessAsUser – #2 トークン編

次に、CreateProcessAsUser で STATUS_DLL_INIT_FAILED (=0xc0000142) エラーが発生する現象についてです。
こんなダイアログが表示されます。

「アプリケーションを正しく起動できませんでした (0xc0000142)。」
image

STATUS_DLL_INIT_FAILED エラーという名前からして、アクセス許可エラーではなく、何らかの DLL の初期化に失敗したようです。win32k とか、GUI スレッドに関連していそうです。

このダイアログを表示させているプロセスは、CreateProcessAsUser を実行したプロセスや、実行しようとしたプロセス (上の例だと notepad.exe) ではなく、csrss.exe です。CSRSS とは、Client Server Run-Time Subsystem の略で、win32 サブシステムのユーザー モード部分です。

とりあえず、デバッガーでスタックを見てみます。環境は Windows Server 2008 R2 SP1 です。tasklist で csrss を見ると Services と Console がありますが、ポップアップを表示させているのは Console の方です。

ユーザー モードで見ると、以下のスレッドがメッセージボックスを表示させていることが分かります。しかし、CsrApiRequestThread という関数からいきなりハードエラーが発生しているので、ここからは何も分かりません。カーネル モードから見ても、芳しい結果は得られず保留。CsrApiRequestThread の内容をごりごり見るしかないのかもしれない。ちょっとこれは大変そうなのでパス。

   4  Id: 560.450 Suspend: 1 Teb: 000007ff`fffdd000 Unfrozen
Child-SP          RetAddr           Call Site
00000000`00a4f198 00000000`77584bc4 USER32!ZwUserWaitMessage+0xa
00000000`00a4f1a0 00000000`77584edd USER32!DialogBox2+0x274
00000000`00a4f230 00000000`775d2920 USER32!InternalDialogBox+0x135
00000000`00a4f290 00000000`775d1c15 USER32!SoftModalMessageBox+0x9b4
00000000`00a4f3c0 00000000`775d146b USER32!MessageBoxWorker+0x31d
00000000`00a4f580 000007fe`fd702de9 USER32!MessageBoxTimeoutW+0xb3
00000000`00a4f650 000007fe`fd7031e8 winsrv!HardErrorHandler+0x33d
00000000`00a4f7f0 000007fe`fd703562 winsrv!ProcessHardErrorRequest+0xe8
00000000`00a4f860 000007fe`fd754a04 winsrv!UserHardErrorEx+0x356
00000000`00a4f8f0 000007fe`fd7550f4 CSRSRV!QueueHardError+0x138
00000000`00a4f930 00000000`777e4a00 CSRSRV!CsrApiRequestThread+0x510
00000000`00a4fc40 00000000`00000000 ntdll!RtlUserThreadStart+0x25

さて、STATUS_DLL_INIT_FAILED エラーの詳細は不明ですが、解決方法は分かっているわけです。2 月に記事を書いたときには ACL についてよく知らなかったので適当に MSDN のサンプルをコピペしましたが、もう少し真面目に見てみます。

サンプルはこれでした。

Starting an Interactive Client Process in C++
http://msdn.microsoft.com/en-us/library/aa379608(v=VS.85).aspx

CreateProcessAsUser を実行する前の主な処理は、こんな感じです。

  1. LogonUser API でログオン処理を行ない、トークンを生成
  2. GetLogonSID で、1. のトークンからログオン SID を取得
  3. AddAceToWindowStation で、ウィンドウ ステーション オブジェクトの DACL に
    2. の SID に対するアクセス許可 ACE を追加
  4. AddAceToDesktop で、デスクトップ オブジェクトの DACL に
    2. の SID に対するアクセス許可 ACE を追加
  5. CreateProcessAsUser 実行

まずは、GetLogonSID の処理から見てみます。ここで取得される SID はログオン SID といって、ユーザーやセキュリティ グループに割り当てられた SID とは別のものです。ログオン セッション毎にユニークな値になっています。つまり、CreateProcessAsUser が返すトークンに紐付いたログオン SID は毎回異なっています。

logon SID
http://msdn.microsoft.com/en-us/library/ms721592(v=VS.85).aspx#_security_logon_sid_gly

A security identifier (SID) that identifies a logon session. You can use the logon SID in a DACL to control access during a logon session. A logon SID is valid until the user logs off. A logon SID is unique while the computer is running; no other logon session will have the same logon SID. However, the set of possible logon SIDs is reset when the computer starts up. To retrieve the logon SID from an access token, call the GetTokenInformation function for TokenGroups.

ログオン SID は !token コマンドで見ることができます。そして、トークン オブジェクトのアドレスは !process コマンドで出力することができます。しかし、それだと何も面白くないので、もう少し捻ります。

プロセスに紐付く情報は、基本的に _EPROCESS 構造体から辿ることができます。トークンも例外ではありません。

kd> dt _EPROCESS Token
nt!_EPROCESS
   +0x208 Token : _EX_FAST_REF
kd> dt _EX_FAST_REF
nt!_EX_FAST_REF
   +0x000 Object           : Ptr64 Void
   +0x000 RefCnt           : Pos 0, 4 Bits
   +0x000 Value            : Uint8B

_EPROCESS::Token は単なるポインターではなく、_EX_FAST_REF という構造になっています。これは、以下のサイトの情報にあるように下位ビット (32bit なら 3、64bit なら 4 ビット) を RefCnt として保持させている構造体です。よって正しいポインターを得るためには、下位ビットを 0 にクリアする必要があります。

http://www.eggheadcafe.com/microsoft/Windows-Debugging/30558733/kernel-debug-unable-to-get-min-sid-header.aspx

適当にプロセスを 2 つ選んで、トークンを見てみます。_EPROCESS の中で Token のオフセットは 0x208 でした。

kd> !process 0 0 cmd.exe
PROCESS fffffa8001ccd060
    SessionId: 1  Cid: 0910    Peb: 7fffffde000  ParentCid: 0b80
    DirBase: 0363f000  ObjectTable: fffff8a001c59a30  HandleCount:  92.
    Image: cmd.exe

PROCESS fffffa8001c81860
    SessionId: 1  Cid: 0a1c    Peb: 7fffffdf000  ParentCid: 0b80
    DirBase: 15c1b000  ObjectTable: fffff8a001cebb60  HandleCount:  23.
    Image: cmd.exe

kd> dq fffffa8001ccd060+208 l1
fffffa80`01ccd268  fffff8a0`01d1c95b

kd> dq fffffa8001c81860+208 l1
fffffa80`01c81a68  fffff8a0`022a095f

kd> !token fffff8a0`01d1c950 -n
_TOKEN fffff8a001d1c950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500 (no name mapped)
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
    Attributes – Mandatory Default Enabled
01 S-1-1-0 (Well Known Group: localhost\Everyone)
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545 (Alias: BUILTIN\Users)
    Attributes – Mandatory Default Enabled
03 S-1-5-32-544 (Alias: BUILTIN\Administrators)
    Attributes – Mandatory Default Enabled Owner
04 S-1-5-4 (Well Known Group: NT AUTHORITY\INTERACTIVE)
    Attributes – Mandatory Default Enabled
05 S-1-2-1 (Well Known Group: localhost\CONSOLE LOGON)
    Attributes – Mandatory Default Enabled
06 S-1-5-11 (Well Known Group: NT AUTHORITY\Authenticated Users)
    Attributes – Mandatory Default Enabled
07 S-1-5-15 (Well Known Group: NT AUTHORITY\This Organization)
    Attributes – Mandatory Default Enabled
08 S-1-5-5-0-3706178 (no name mapped)
    Attributes – Mandatory Default Enabled LogonId

09 S-1-2-0 (Well Known Group: localhost\LOCAL)
    Attributes – Mandatory Default Enabled
10 S-1-5-21-2857284654-3416964824-2551679015-512 (no name mapped)
    Attributes – Mandatory Default Enabled
11 S-1-5-21-2857284654-3416964824-2551679015-520 (no name mapped)
    Attributes – Mandatory Default Enabled
12 S-1-5-21-2857284654-3416964824-2551679015-519 (no name mapped)
    Attributes – Mandatory Default Enabled
13 S-1-5-21-2857284654-3416964824-2551679015-518 (no name mapped)
    Attributes – Mandatory Default Enabled
14 S-1-5-21-2857284654-3416964824-2551679015-3683 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
15 S-1-5-21-2857284654-3416964824-2551679015-572 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
16 S-1-16-12288 Unrecognized SID
    Attributes – GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
Privs:
(略)
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 38f126           ParentToken ID: 0
Modified ID:               (0, 38d8de)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7

kd> !token fffff8a0`022a0950 -n
_TOKEN fffff8a0022a0950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500 (no name mapped)
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
    Attributes – Mandatory Default Enabled
01 S-1-1-0 (Well Known Group: localhost\Everyone)
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545 (Alias: BUILTIN\Users)
    Attributes – Mandatory Default Enabled
03 S-1-5-32-544 (Alias: BUILTIN\Administrators)
    Attributes – Mandatory Default Enabled Owner
04 S-1-5-4 (Well Known Group: NT AUTHORITY\INTERACTIVE)
    Attributes – Mandatory Default Enabled
05 S-1-2-1 (Well Known Group: localhost\CONSOLE LOGON)
    Attributes – Mandatory Default Enabled
06 S-1-5-11 (Well Known Group: NT AUTHORITY\Authenticated Users)
    Attributes – Mandatory Default Enabled
07 S-1-5-15 (Well Known Group: NT AUTHORITY\This Organization)
    Attributes – Mandatory Default Enabled
08 S-1-5-5-0-3706178 (no name mapped)
    Attributes – Mandatory Default Enabled LogonId

09 S-1-2-0 (Well Known Group: localhost\LOCAL)
    Attributes – Mandatory Default Enabled
10 S-1-5-21-2857284654-3416964824-2551679015-512 (no name mapped)
    Attributes – Mandatory Default Enabled
11 S-1-5-21-2857284654-3416964824-2551679015-520 (no name mapped)
    Attributes – Mandatory Default Enabled
12 S-1-5-21-2857284654-3416964824-2551679015-519 (no name mapped)
    Attributes – Mandatory Default Enabled
13 S-1-5-21-2857284654-3416964824-2551679015-518 (no name mapped)
    Attributes – Mandatory Default Enabled
14 S-1-5-21-2857284654-3416964824-2551679015-3683 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
15 S-1-5-21-2857284654-3416964824-2551679015-572 (no name mapped)
    Attributes – Mandatory Default Enabled GroupResource
16 S-1-16-12288 Unrecognized SID
    Attributes – GroupIntegrity GroupIntegrityEnabled
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513 (no name mapped)
Privs:
(略)
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 3a609d           ParentToken ID: 0
Modified ID:               (0, 3a5ce1)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7

どちらのプロセスも、S-1-5-5-0-3706178 というログオン SID をトークンの中に保持していることが分かります。ログオン SID は、S-1-5-5- で始まっていることから判断することができます。

Well-known security identifiers in Windows operating systems
http://support.microsoft.com/kb/243330/en

以上が GetLogonSID の処理でした。UI を持つプロセスは、デスクトップやウィンドウ ステーション オブジェクトに対してアクセス権を持っていなければならず、そのためにはログオン セッションのログオン SID に対して許可されていなければならないようです。つまり、オブジェクトの DACL に ACE を追加する必要があります。

ACL に関しては、半年ほど前に SDDL をパースするプログラムを書きました。

[Win32] [C++] CUI tool to parse SDDL Strings and account SIDs
https://msmania.wordpress.com/2011/06/30/win32-c-cui-tool-to-parse-sddl-strings-and-account-sids/

オブジェクトが持つセキュリティ記述子 (Security Descriptor) は、オブジェクト ヘッダー nt!_OBJECT_HEADER が保持しています。そこで、デスクトップとウィンドウ ステーションのセキュリティ記述子を見てみます。

winobj で見ることができるように、ウィンドウ ステーション オブジェクトは \Windows\WindowStations\WinSta0 のような名前を持っていますが、デスクトップ オブジェクトにはオブジェクト マネージャーの名前空間に名前を持っていないため、プロセスのハンドルテーブルから探します。

kd> !process 0 0 notepad.exe
PROCESS fffffa8001be6b30
    SessionId: 1  Cid: 0310    Peb: 7fffffdf000  ParentCid: 0910
    DirBase: 07a6b000  ObjectTable: fffff8a00192f600  HandleCount:  90.
    Image: notepad.exe

kd> !handle 0 7 fffffa8001be6b30
    PROCESS fffffa8001be6b30
    SessionId: 1  Cid: 0310    Peb: 7fffffdf000  ParentCid: 0910
    DirBase: 07a6b000  ObjectTable: fffff8a00192f600  HandleCount:  90.
    Image: notepad.exe

Handle table at fffff8a0019e0000 with 90 entries in use

(略)

0034: Object: fffffa8001a6e7c0  GrantedAccess: 000f037f Entry: fffff8a0019e00d0
Object: fffffa8001a6e7c0  Type: (fffffa8000ca7b40) WindowStation
    ObjectHeader: fffffa8001a6e790 (new version)
        HandleCount: 23  PointerCount: 39
        Directory Object: fffff8a0021e8720  Name: WinSta0

0038: Object: fffffa8001cfe830  GrantedAccess: 000f01ff Entry: fffff8a0019e00e0
Object: fffffa8001cfe830  Type: (fffffa8000ca79f0) Desktop
    ObjectHeader: fffffa8001cfe800 (new version)
        HandleCount: 11  PointerCount: 494
        Directory Object: 00000000  Name: Default

(略)

オブジェクトのセキュリティ記述子は、オブジェクト ヘッダー nt!_OBJECT_HEADER に含まれています。

kd> dt _object_header
nt!_OBJECT_HEADER
   +0x000 PointerCount     : Int8B
   +0x008 HandleCount      : Int8B
   +0x008 NextToFree       : Ptr64 Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : UChar
   +0x019 TraceFlags       : UChar
   +0x01a InfoMask         : UChar
   +0x01b Flags            : UChar
   +0x020 ObjectCreateInfo : Ptr64 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : Ptr64 Void
   +0x028 SecurityDescriptor : Ptr64 Void
   +0x030 Body             : _QUAD

セキュリティ識別子は、!sd でダンプできます。そこで、SecurityDescriptor メンバーに対して !sd を実行すると・・・

kd> dt _object_header fffffa8001cfe800
nt!_OBJECT_HEADER
   +0x000 PointerCount     : 0n494
   +0x008 HandleCount      : 0n11
   +0x008 NextToFree       : 0x00000000`0000000b Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0x15 ”
   +0x019 TraceFlags       : 0 ”
   +0x01a InfoMask         : 0xe ”
   +0x01b Flags            : 0 ”
   +0x020 ObjectCreateInfo : 0xfffff800`0166ac00 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0xfffff800`0166ac00 Void
   +0x028 SecurityDescriptor : 0xfffff8a0`01c133ee Void
   +0x030 Body             : _QUAD

kd> !sd 0xfffff8a0`01c133ee
1100000001001c: Unable to get MIN SID header
1100000001001c: Unable to read in Owner in SD

エラーになりました。ポインターが間違っているようです。ポインターの値をよくみると、64bit ポインターなのに 64bit 境界になっていないことに気づきます。64bit 境界ということでポインターは下位 4 ビットが必ず 0 か 8 になるはずですが、SecurityDescriptor の値の下位 4 ビットは e になっています。ここで思い出すのは、さっき出てきた _EX_FAST_REF 構造です。というか、上で引用したページにも書いてありましたが SecurityDescriptor フィールドは _EX_FAST_REF 構造になっているため、下位 4 ビットを 0 にすることでセキュリティ記述子のアドレスが得られます。SecurityDescriptor はオフセット 0x28 バイトなので・・・

▼ ウィンドウ ステーション "Winsta0"

kd> !sd poi(fffffa8001a6e790+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0xe4
->Dacl    : ->AceCount   : 0x9
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x4
->Dacl    : ->Ace[0]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[0]: ->AceSize: 0x24
->Dacl    : ->Ace[0]: ->Mask : 0x00000024
->Dacl    : ->Ace[0]: ->SID: S-1-5-21-2857284654-3416964824-2551679015-500

->Dacl    : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[1]: ->AceFlags: 0xb
->Dacl    : ->Ace[1]:             OBJECT_INHERIT_ACE
->Dacl    : ->Ace[1]:             CONTAINER_INHERIT_ACE
->Dacl    : ->Ace[1]:             INHERIT_ONLY_ACE
->Dacl    : ->Ace[1]: ->AceSize: 0x1c
->Dacl    : ->Ace[1]: ->Mask : 0xf0000000
->Dacl    : ->Ace[1]: ->SID: S-1-5-5-0-3706178

->Dacl    : ->Ace[2]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[2]: ->AceFlags: 0x4
->Dacl    : ->Ace[2]:             NO_PROPAGATE_INHERIT_ACE
->Dacl    : ->Ace[2]: ->AceSize: 0x1c
->Dacl    : ->Ace[2]: ->Mask : 0x000f037f
->Dacl    : ->Ace[2]: ->SID: S-1-5-5-0-3706178

(略)

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

▼ デスクトップ "Default"

kd> !sd poi(fffffa8001cfe800+28)&0xffffffff`fffffff0
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8014
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544
->Group   : S-1-5-18
->Dacl    :
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x64
->Dacl    : ->AceCount   : 0x4
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x1c
->Dacl    : ->Ace[0]: ->Mask : 0x000f01ff
->Dacl    : ->Ace[0]: ->SID: S-1-5-5-0-3706178

(略)

->Sacl    :
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000001
->Sacl    : ->Ace[0]: ->SID: S-1-16-4096

これでセキュリティ記述子をダンプできました。

まず注目すべきは紫色で示した SID です。これは S-1-5-5- で始まっているのでログオン SID です。現在のログオン セッションに対して、アクセス許可の設定がされていることが分かります。

次に、ログオン SID が割り当てられている ACE に注目します。DACL の中でログオン SID が割り当てられている ACE は全て ACCESS_ALLOWED_ACE_TYPE のアクセス許可 ACE です。そして、ウィンドウ ステーションでは 2 つの ACE があるのに対して、デスクトップは 1 つです。ウィンドウ ステーションに設定された 2 つの ACE では、AceFlags と Mask の値が異なっています。AceFlags の定数を見ると分かりますが、1 つは継承可能で、もう 1 つは継承を止める ACE になっています。つまり、Mask で設定されたアクセス許可の種類に応じて、継承させるものと継承させないものを区別していることが分かります。

ACE Inheritance Rules
http://msdn.microsoft.com/en-us/library/windows/desktop/aa374924(v=vs.85).aspx

ACCESS_MASK
http://msdn.microsoft.com/en-us/library/aa374892(v=vs.85).aspx

MSDN のサンプルに戻ります。サンプルにおいては、オブジェクトの DACL に ログオン SID の ACE を追加する処理がAddAceToWindowStation と AddAceToDesktop との 2 つの関数に分けてあります。この理由が上記の内容になります。デスクトップは 1 つの ACE を追加すればいいですが、ウィンドウ ステーションには、2 つの ACE を追加しているため、関数を分けているようです。

AddAceToWindowStation では、AddAce API を 2 回呼ぶことで 2 つの ACE を ACL に追加しています。一方 AddAceToDesktop では、AddAccessAllowedAce API を 1 回呼んで ACE を追加しています。

このサンプルは実はもっと効率よく書けます。まず、AddAceToWindowStation と AddAceToDesktop とでは共通する処理がほとんどです。というのも、いずれの関数も流れは共通で、異なっているのは以下の 5. の部分だけです。

  1. オブジェクト ハンドルからセキュリティ記述子を取得 (GetUserObjectSecurity API)
  2. 新たなセキュリティ記述子を作成 (InitializeSecurityDescriptor API)
  3. 新たな ACL を作成 (InitializeAcl API)
  4. 1. で取得したセキュリティ記述子の DACL を 3. の ACL に コピー
  5. SID に対するアクセス許可 ACE を 4. の ACL に追加
  6. 2. のセキュリティ記述子に 5. の ACL を設定 (SetSecurityDescriptorDacl API)
  7. オブジェクトに 6. の セキュリティ記述子を設定 (SetUserObjectSecurity API)

AddAceToDesktop で使っている AddAccessAllowedAce API では AceFlags を指定できませんが、AddAccessAllowedAceEx API を使うと AceFlags を指定できるので、AddAceToWindowStation と AddAceToDesktop とのいずれにおいても AddAccessAllowedAceEx を使って ACE を追加すれば、わざわざ ACE のサイズを計算して AddAce を使わなくて済みます。

AddAccessAllowedAce function
http://msdn.microsoft.com/en-us/library/aa374947

AddAccessAllowedAceEx function
http://msdn.microsoft.com/en-us/library/aa374951(v=VS.85).aspx

そこで、AddAceToWindowStation と AddAceToDesktop の ACE を追加する処理以外を AddAccessAllowedAceBasedSID という関数でまとめて、ACE の追加は AddAccessAllowedAceEx を使ってプログラムを書き直してみました。

さらに、追加した ACE を削除する関数 RemoveAccessAllowedAcesBasedSID も作りました。これは、指定した SID に対するアクセス許可 ACE のみだけを削除する関数です。

AddAceToWindowStation と AddAceToDesktop は ACE のフラグとマスクの配列を渡すだけになりました。
にも関わらず、エラー処理をするとコードが長くなってしまうのが Win32 の面倒なところ。

//
// dacl.cpp
//

#include <Windows.h>
#include <Sddl.h>
#include <stdio.h>

#include "logue.h"

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL AddAccessAllowedAceBasedSID(HANDLE Object, PSID Sid, DWORD AceCount,
                                CONST DWORD AceFlags[], CONST DWORD AccessMasks[]) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
              HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object,
                &DaclInfo, Sd, SdSize, &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    AclSizeInfo.AclBytesInUse = sizeof(ACL);
    if ( Acl ) {
        if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
            goto cleanup;
    }

    // Create a new ACL
    // (original ACL + new ACCESS_ALLOWED_ACEs)
    AclSize= AclSizeInfo.AclBytesInUse +
        AceCount * (sizeof(ACCESS_ALLOWED_ACE) +
        GetLengthSid(Sid) – sizeof(DWORD));
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;

    // If DACL is present, copy all the ACEs to a new DACL.
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD,
                        Ace, ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }

    // Add new ACEs of specified SID to the DACL
    for ( DWORD i=0 ; i<AceCount ; ++i ) {
        if (!AddAccessAllowedAceEx(AclNew, ACL_REVISION,
                                   AceFlags[i], AccessMasks[i], Sid) )
            goto cleanup;
    }

    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough
    // because SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
      HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

// add ACCESS_ALLOWED_ACEs of the specified SID to the object’s DACL
BOOL RemoveAccessAllowedAcesBasedSID(HANDLE Object, PSID Sid) {
    BOOL Ret= FALSE;
    SECURITY_INFORMATION DaclInfo= DACL_SECURITY_INFORMATION;
    PACL Acl= NULL; // no need to free
    PACL AclNew= NULL;
    PSECURITY_DESCRIPTOR Sd= NULL;
    PSECURITY_DESCRIPTOR SdNew= NULL;
    DWORD SdSize= 0;
    DWORD SdSizeNeeded= 0;
    ACL_SIZE_INFORMATION AclSizeInfo;
    DWORD AclSize= 0;
    BOOL DaclPresent;
    BOOL DaclDefaulted;

    //
    // Obtain DACL from the object.
    //
http://msdn.microsoft.com/en-us/library/aa379573
    //
    if ( !GetUserObjectSecurity(Object, &DaclInfo, Sd, 0, &SdSizeNeeded) ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER )
            goto cleanup;
           
        Sd= (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
               HEAP_ZERO_MEMORY, SdSizeNeeded);
        if ( Sd==NULL ) goto cleanup;

        SdSize= SdSizeNeeded;
        if ( !GetUserObjectSecurity(Object, &DaclInfo,
                                    Sd, SdSize, &SdSizeNeeded) )
            goto cleanup;
    }

    // Obtain the DACL from the security descriptor.
    if ( !GetSecurityDescriptorDacl(Sd, &DaclPresent, &Acl, &DaclDefaulted) )
        goto cleanup;

    if ( !DaclPresent || !Acl || Acl->AceCount==0 ) {
        // nothing to do for Null DACL or Empty DACL
        //
http://technet.microsoft.com/ja-jp/query/aa379286
        Ret= TRUE;
        goto cleanup;
    }

    // Initialize.
    ZeroMemory(&AclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
    if (!GetAclInformation(Acl, (LPVOID)&AclSizeInfo,
            sizeof(ACL_SIZE_INFORMATION), AclSizeInformation) )
        goto cleanup;

    // Create an ACL copy
    AclSize= AclSizeInfo.AclBytesInUse;
    AclNew= (PACL)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclSize);
    if ( AclNew==NULL ) goto cleanup;

    if ( !InitializeAcl(AclNew, AclSize, ACL_REVISION) )
        goto cleanup;
   
    // do not copy ACCESS_ALLOWED_ACEs of the specified SID
    if ( DaclPresent && AclSizeInfo.AceCount ) {
        for ( DWORD i=0; i < AclSizeInfo.AceCount; ++i ) {
            PVOID Ace= NULL;
            if ( !GetAce(Acl, i, &Ace) ) goto cleanup;
           
            if ( ((PACE_HEADER)Ace)->AceType==ACCESS_ALLOWED_ACE_TYPE &&
                    EqualSid(Sid, &((ACCESS_ALLOWED_ACE*)Ace)->SidStart) )
                continue;

            if (!AddAce(AclNew, ACL_REVISION, MAXDWORD,
                        Ace, ((PACE_HEADER)Ace)->AceSize) )
                goto cleanup;
        }
    }
   
    // Create a new security descriptor.
    // SECURITY_DESCRIPTOR_MIN_LENGTH is enough
    // because SetSecurityDescriptorDacl creates absolute security descriptor
    SdNew = (PSECURITY_DESCRIPTOR)HeapAlloc(GetProcessHeap(),
             HEAP_ZERO_MEMORY, SECURITY_DESCRIPTOR_MIN_LENGTH);
    if ( SdNew==NULL ) goto cleanup;

    if ( !InitializeSecurityDescriptor(SdNew, SECURITY_DESCRIPTOR_REVISION) )
        goto cleanup;

    // Set new DACL to the new security descriptor.
    // (this security descriptor becomes an absolute SD)
    if ( !SetSecurityDescriptorDacl(SdNew, TRUE, AclNew, FALSE) )
        goto cleanup;
   
#ifdef _TRACING
    wprintf(L"Original SD: %p\n", Sd);
    wprintf(L"New SD     : %p\n", SdNew);
    wprintf(L"–>\n");
    getwchar();
#endif

    // Set the new security descriptor for the desktop object.
    if (!SetUserObjectSecurity(Object, &DaclInfo, SdNew))
        goto cleanup;

    Ret= TRUE;

cleanup:
    if ( AclNew ) HeapFree(GetProcessHeap(), 0, AclNew);
    if ( Sd ) HeapFree(GetProcessHeap(), 0, Sd);
    if ( SdNew ) HeapFree(GetProcessHeap(), 0, SdNew);

    return Ret;
}

BOOL AddAceToDesktop(HDESK Desktop, PSID Sid) {
    CONST DWORD AccessMasks[1] = {
        DESKTOP_READOBJECTS | DESKTOP_CREATEWINDOW | DESKTOP_CREATEMENU |
        DESKTOP_HOOKCONTROL | DESKTOP_JOURNALRECORD |
        DESKTOP_JOURNALPLAYBACK |
        DESKTOP_ENUMERATE | DESKTOP_WRITEOBJECTS | DESKTOP_SWITCHDESKTOP |
        STANDARD_RIGHTS_REQUIRED
    };

    DWORD AceFlags[1] = {0};

    return AddAccessAllowedAceBasedSID(Desktop, Sid,
                                       1, AceFlags, AccessMasks);
}

BOOL AddAceToWindowStation(HWINSTA Winsta, PSID Sid) {
    CONST DWORD AccessMasks[2] = {
        GENERIC_READ | GENERIC_WRITE | GENERIC_EXECUTE | GENERIC_ALL,
        WINSTA_ENUMDESKTOPS | WINSTA_READATTRIBUTES |
        WINSTA_ACCESSCLIPBOARD |
        WINSTA_CREATEDESKTOP | WINSTA_WRITEATTRIBUTES |
        WINSTA_ACCESSGLOBALATOMS |
        WINSTA_EXITWINDOWS | WINSTA_ENUMERATE | WINSTA_READSCREEN |
        STANDARD_RIGHTS_REQUIRED};

    CONST DWORD AceFlags[2] = {
        CONTAINER_INHERIT_ACE|INHERIT_ONLY_ACE|OBJECT_INHERIT_ACE,
        NO_PROPAGATE_INHERIT_ACE
    };   

    return AddAccessAllowedAceBasedSID(Winsta, Sid,
                                       2, AceFlags, AccessMasks);
}

BOOL GetLogonSidFromToken(HANDLE Token, PSID *outSid) {
    BOOL Ret= FALSE;
    DWORD TokenGroupLength= 0;
    DWORD SidLength= 0;
    PTOKEN_GROUPS TokenGroup= NULL;
    LPWSTR SidString= NULL;

    if ( !GetTokenInformation(Token, TokenGroups,
                             (LPVOID)TokenGroup, 0, &TokenGroupLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (1st chance) failed – 0x%08x\n",
                  GetLastError());
        goto Cleanup;
    }

    TokenGroup= (PTOKEN_GROUPS)HeapAlloc(GetProcessHeap(),
                    HEAP_ZERO_MEMORY, TokenGroupLength);
    if ( !TokenGroup ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto Cleanup;
    }

    if ( !GetTokenInformation(Token, TokenGroups, (LPVOID)TokenGroup,
                              TokenGroupLength, &TokenGroupLength) ) {
        wprintf(L"GetTokenInformation failed (2nd chance) – 0x%08x\n",
                    GetLastError());
        goto Cleanup;
    }

    //
    // SE_GROUP_LOGON_ID
    // The SID is a logon SID that identifies the logon session
    // associated with an access token.
    //
http://technet.microsoft.com/en-us/library/aa379624
    //
    for ( DWORD i=0 ; i<TokenGroup->GroupCount ; ++i ) {
        if ( SidString ) LocalFree(SidString);
        ConvertSidToStringSid(TokenGroup->Groups[i].Sid, &SidString);
        wprintf(L"SID: %s", SidString);

        if ( (TokenGroup->Groups[i].Attributes&SE_GROUP_LOGON_ID)==
               SE_GROUP_LOGON_ID ) {
            SidLength= GetLengthSid(TokenGroup->Groups[i].Sid);

            *outSid= (PSID)HeapAlloc(GetProcessHeap(),
                     HEAP_ZERO_MEMORY, SidLength);
            if ( *outSid==NULL ) {

                wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
                goto Cleanup;
            }

            if ( !CopySid(SidLength, *outSid, TokenGroup->Groups[i].Sid) ) {
                wprintf(L"CopySid failed – 0x%08x\n", GetLastError());
                HeapFree(GetProcessHeap(), 0, (LPVOID)*outSid);
                goto Cleanup;
            }

            wprintf(L" (Logon)\n");
            break;
        }
        else
            wprintf(L"\n");
    }
   
    Ret= TRUE;

Cleanup:
    if ( SidString ) LocalFree(SidString);
    if ( TokenGroup ) HeapFree(GetProcessHeap(), 0, TokenGroup);
    return Ret;
}

[Win32] [C++] CreateProcessAsUser – #1 特権編

以前、LogonUser と CreateProcessAsUser を使って、別ユーザーとしてプログラムを実行する方法について書きました。あれから 1 年近くたって、背景がそれなりに理解できるようになってきたので、新たに書き直します。

[Win32] [C++] LogonUser と CreateProcessAsUser
https://msmania.wordpress.com/2011/02/06/win32-c-logonuser-%e3%81%a8-createprocessasuser/

長くなりそうなので、幾つかの記事に分けます。

まずは昔の記事のおさらいから。

LogonUser API でトークンを取得して、それを CreateProcessAsUser に渡すと ERROR_PRIVILEGE_NOT_HELD (0n1314) エラーが発生します。これは簡単で、呼び出し側プロセスが SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME を持っていないから。Administrator といえど、既定で前者は持っていても後者の特権を持っていないので、secpol.msc などから 「プロセス レベル トークンの置き換え」 特権を割り当てることでクリア。ちなみにこのエラー、Windows Server 2008 R2 に SAP NetWeaver の試用版をインストールするときにも sapinst で発生します。回避方法は同じです。

ERROR_PRIVILEGE_NOT_HELD エラーはクリアできても、UI (GUI or CUI) を持つプロセスは依然として起動できず、STATUS_DLL_INIT_FAILED (=0xc0000142) エラーが発生してしまう。これを解消するためには、MSDN のサンプルを参照して、ウィンドウ ステーションとデスクトップ オブジェクトの DACL にアクセス許可 ACE を追加する必要がある。

ポイントは上記 2 つでした。

まず、前者の特権について。
ユーザーがログオンすると、セキュリティ トークンというデータが LSA (=Local Security Authority) によって作成され、ログオン セッションに紐付けられます。このセッションの中で作成されたプロセスには、基本的にはログオン セッションのトークンが継承されて保持されます。

現在のログオン セッションの特権一覧を取得する方法で、最も簡単なのは whoami /priv コマンドを使う方法です。通常のコマンド プロンプトと、管理者として実行したコマンド プロンプトとで実行結果を比較すると、持っている特権が異なります。これが UAC の機能のキモで、前者が 「制限付きトークン」 というやつです。

> whoami /priv

PRIVILEGES INFORMATION
———————-

特権名                        説明                                            状態
============================= =============================================== ====
SeShutdownPrivilege           システムのシャットダウン                        無効
SeChangeNotifyPrivilege       走査チェックのバイパス                          有効
SeUndockPrivilege             ドッキング ステーションからコンピューターを削除 無効
SeIncreaseWorkingSetPrivilege プロセス ワーキング セットの増加                無効
SeTimeZonePrivilege           タイム ゾーンの変更                             無効

デバッガーでは !token というコマンドがあります。例えば、実行中のメモ帳のトークンを出力した例です。

kd> !process 0 0 notepad.exe
PROCESS fffffa8001b51570
    SessionId: 1  Cid: 05f8    Peb: 7fffffde000  ParentCid: 0910
    DirBase: 15090000  ObjectTable: fffff8a00228a300  HandleCount:  93.
    Image: notepad.exe

kd> !process fffffa8001b51570
PROCESS fffffa8001b51570
    SessionId: 1  Cid: 05f8    Peb: 7fffffde000  ParentCid: 0910
    DirBase: 15090000  ObjectTable: fffff8a00228a300  HandleCount:  93.
    Image: notepad.exe
    VadRoot fffffa8001b18cb0 Vads 86 Clone 0 Private 385. Modified 0. Locked 0.
    DeviceMap fffff8a001842670
    Token                             fffff8a0021c6950
    ElapsedTime                       00:00:02.846
    UserTime                          00:00:00.000
    KernelTime                        00:00:00.000
(略)

kd> !token fffff8a0021c6950
_TOKEN fffff8a0021c6950
TS Session ID: 0x1
User: S-1-5-21-2857284654-3416964824-2551679015-500
Groups:
00 S-1-5-21-2857284654-3416964824-2551679015-513
    Attributes – Mandatory Default Enabled
01 S-1-1-0
    Attributes – Mandatory Default Enabled
02 S-1-5-32-545
    Attributes – Mandatory Default Enabled
(略)
Primary Group: S-1-5-21-2857284654-3416964824-2551679015-513
Privs:
03 0x000000003 SeAssignPrimaryTokenPrivilege     Attributes –
05 0x000000005 SeIncreaseQuotaPrivilege          Attributes –
(略)
28 0x00000001c SeManageVolumePrivilege           Attributes –
29 0x00000001d SeImpersonatePrivilege            Attributes – Enabled Default
30 0x00000001e SeCreateGlobalPrivilege           Attributes – Enabled Default
33 0x000000021 SeIncreaseWorkingSetPrivilege     Attributes –
34 0x000000022 SeTimeZonePrivilege               Attributes –
35 0x000000023 SeCreateSymbolicLinkPrivilege     Attributes –
Authentication ID:         (0,388d63)
Impersonation Level:       Anonymous
TokenType:                 Primary
Source: User32             TokenFlags: 0x2000 ( Token in use )
Token ID: 396a44           ParentToken ID: 0
Modified ID:               (0, 38d8de)
RestrictedSidCount: 0      RestrictedSids: 0000000000000000
OriginatingLogonSession: 3e7
kd>

トークンは nt!_TOKEN という構造体で、そこに特権のリストが設定されているだけの話です。しかし whoami や !token で分かりにくいのが、トークンに含まれている特権しかダンプしてくれない点です。リストに含まれるトークンには無効と有効というスイッチがありますが、これ自体は AdjustTokenPrivileges API でプロセス自身が簡単に動的変更できるものです。

AdjustTokenPrivileges function
http://msdn.microsoft.com/en-us/library/aa375202(v=vs.85).aspx

上の MSDN に書いてありますが、リストに含まれていない特権を有効にしようとしても、戻り値こそ TRUE になるものの、GetLastError は ERROR_NOT_ALL_ASSIGNED に設定されます。

CreateProcessAsUser を実行するには SE_INCREASE_QUOTA_NAME と SE_ASSIGNPRIMARYTOKEN_NAME とが必要になりますが、ここで重要なのはこれらの特権がリストに含まれているかどうかであって、無効か有効かどうかは関係ありません。無効になっていても、リストに含まれていれば CreateProcessAsUser は成功します。CreateProcessAsUser を実行する前に、わざわざ自分で AdjustTokenPrivileges  を実行する必要はないわけです。

トークンに特権を追加するには、ローカル セキュリティ ポリシー (secpol.msc) を使います。以下のグループ ポリシーでも可能で、secpol.msc と同じことです。このポリシーはコンピューターに割り当てることに注意して下さい。

Computer Configuration > Policies > Windows Settings >
  Security Settings > Local Policies > User Rights Assignment

トークンはログオン時に作られるので、ローカル セキュリティ ポリシーを変更しても現在のログオン セッションに反映されることはなく、ログオフして再ログオンする必要があります。グループ ポリシー経由で設定した場合は、コンピューターアカウントにグループ ポリシーを反映 (gpupdate /force /target:computer) させた後、再ログオンして下さい。

プログラム的に特権を追加するには、LsaAddAccountRights API を使います。GUI の操作と同じで、指定した特権に指定した SID を追加するという機能を持っています。この API を実行した後も再ログオンが必要です。

LsaAddAccountRights function
http://msdn.microsoft.com/en-us/library/ms721786(v=vs.85).aspx

特権は SE_ASSIGNPRIMARYTOKEN_NAME というような定数で扱いますが、実体は “SeAssignPrimaryTokenPrivilege” というような文字列です。また、システム内では LUID という 64bit 整数値も割り振られています。GUID ではないので、異なるシステムでは同じ値であることが保証されません。whoami コマンドでは LUID を出力できませんが、!token では出力されます。上の出力例では、SE_ASSIGNPRIMARYTOKEN_NAME は LUID=3 でした。

03 0x000000003 SeAssignPrimaryTokenPrivilege Attributes –

whoami や !token では、トークンが保持している特権のリストしか得られませんでした。せっかくなら、トークンが保持していない特権もまとめて表示できると便利です。そんなわけで、まずは以下のような関数を書いてみました。

全ての特権を取得する方法がすぐに分からなかったので、0 から MAX_PRIVSCAN (256) までの LUID を全て舐めるといういい加減な動きにしています。本当は、システムで定義されている全ての特権を取得する方法があるはずです。

//
// priv.cpp
//

#include <windows.h>
#include <NTSecAPI.h>
#include <stdio.h>
#include <strsafe.h>

#include "logue.h"

#define MAX_PRIVNAME 32
#define MAX_PRIVSCAN 256

#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth

struct PRIVILAGENAME_MAPPING {
    WCHAR SymbolName[MAX_PRIVNAME];
    WCHAR PrivilegeName[MAX_PRIVNAME];
};

const PRIVILAGENAME_MAPPING PrivilegeNameMapping[]= {
    { L"SE_CREATE_TOKEN_NAME", SE_CREATE_TOKEN_NAME },
    { L"SE_ASSIGNPRIMARYTOKEN_NAME", SE_ASSIGNPRIMARYTOKEN_NAME },
    { L"SE_LOCK_MEMORY_NAME", SE_LOCK_MEMORY_NAME },
    { L"SE_INCREASE_QUOTA_NAME", SE_INCREASE_QUOTA_NAME },
    { L"SE_UNSOLICITED_INPUT_NAME", SE_UNSOLICITED_INPUT_NAME }, // no LUID?
    { L"SE_MACHINE_ACCOUNT_NAME", SE_MACHINE_ACCOUNT_NAME },
    { L"SE_TCB_NAME", SE_TCB_NAME },
    { L"SE_SECURITY_NAME", SE_SECURITY_NAME },
    { L"SE_TAKE_OWNERSHIP_NAME", SE_TAKE_OWNERSHIP_NAME },
    { L"SE_LOAD_DRIVER_NAME", SE_LOAD_DRIVER_NAME },
    { L"SE_SYSTEM_PROFILE_NAME", SE_SYSTEM_PROFILE_NAME },
    { L"SE_SYSTEMTIME_NAME", SE_SYSTEMTIME_NAME },
    { L"SE_PROF_SINGLE_PROCESS_NAME", SE_PROF_SINGLE_PROCESS_NAME },
    { L"SE_INC_BASE_PRIORITY_NAME", SE_INC_BASE_PRIORITY_NAME },
    { L"SE_CREATE_PAGEFILE_NAME", SE_CREATE_PAGEFILE_NAME },
    { L"SE_CREATE_PERMANENT_NAME", SE_CREATE_PERMANENT_NAME },
    { L"SE_BACKUP_NAME", SE_BACKUP_NAME },
    { L"SE_RESTORE_NAME", SE_RESTORE_NAME },
    { L"SE_SHUTDOWN_NAME", SE_SHUTDOWN_NAME },
    { L"SE_DEBUG_NAME", SE_DEBUG_NAME },
    { L"SE_AUDIT_NAME", SE_AUDIT_NAME },
    { L"SE_SYSTEM_ENVIRONMENT_NAME", SE_SYSTEM_ENVIRONMENT_NAME },
    { L"SE_CHANGE_NOTIFY_NAME", SE_CHANGE_NOTIFY_NAME },
    { L"SE_REMOTE_SHUTDOWN_NAME", SE_REMOTE_SHUTDOWN_NAME },
    { L"SE_UNDOCK_NAME", SE_UNDOCK_NAME },
    { L"SE_SYNC_AGENT_NAME", SE_SYNC_AGENT_NAME },
    { L"SE_ENABLE_DELEGATION_NAME", SE_ENABLE_DELEGATION_NAME },
    { L"SE_MANAGE_VOLUME_NAME", SE_MANAGE_VOLUME_NAME },
    { L"SE_IMPERSONATE_NAME", SE_IMPERSONATE_NAME },
    { L"SE_CREATE_GLOBAL_NAME", SE_CREATE_GLOBAL_NAME },
    { L"SE_TRUSTED_CREDMAN_ACCESS_NAME", SE_TRUSTED_CREDMAN_ACCESS_NAME },
    { L"SE_RELABEL_NAME", SE_RELABEL_NAME },
    { L"SE_INC_WORKING_SET_NAME", SE_INC_WORKING_SET_NAME },
    { L"SE_TIME_ZONE_NAME", SE_TIME_ZONE_NAME },
    { L"SE_CREATE_SYMBOLIC_LINK_NAME", SE_CREATE_SYMBOLIC_LINK_NAME },
    { L"", L"" }
};

BOOL LookupPrivilegeName(LPCWSTR SystemName, CONST PLUID Luid,
                         LPCWSTR *SymbolName,
                         LPWSTR PrivilegeName, LPDWORD PrivilegeNameLength,
                         LPWSTR DisplayName, LPDWORD DisplayNameLength,
                         BOOL NoErrMsg) {
    BOOL Ret= FALSE;
    DWORD LanguageId;
    int Index= -1;

    Ret= LookupPrivilegeName(NULL, Luid, PrivilegeName, PrivilegeNameLength);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeName failed – 0x%08x\n",
                    GetLastError());
        goto cleanup;
    }

    Ret= LookupPrivilegeDisplayName(NULL,}
      PrivilegeName, DisplayName, DisplayNameLength, &LanguageId);
    if ( !Ret ) {
        if ( GetLastError()!=ERROR_INSUFFICIENT_BUFFER && !NoErrMsg )
            wprintf(L"LookupPrivilegeDisplayName failed – 0x%08x\n",
              GetLastError());
        goto cleanup;
    }

    Ret= FALSE;
    const PRIVILAGENAME_MAPPING *p=PrivilegeNameMapping;
    for ( Index=0 ; p->SymbolName[0]!=0 ; ++p, ++Index ) {
        if ( wcscmp(PrivilegeName, p->PrivilegeName)==0 ) {
            Ret= TRUE;
            break;
        }
    }

    if ( Ret )
        *SymbolName= PrivilegeNameMapping[Index].SymbolName;
    else if ( NoErrMsg )
        wprintf(L"%s not found\n", PrivilegeName);

cleanup:
    return Ret;
}

BOOL LookupPrivilegeValueEx(LPCWSTR SystemName, LPCWSTR Name, PLUID Luid) {
    BOOL Ret= LookupPrivilegeValue(SystemName, Name, Luid);
    if ( !Ret && GetLastError()==ERROR_NO_SUCH_PRIVILEGE ) {
        const PRIVILAGENAME_MAPPING *p;
        for ( p=PrivilegeNameMapping ; p->SymbolName[0]!=0 ; ++p ) {
            if ( wcscmp(Name, p->SymbolName)==0 )
                return LookupPrivilegeValue(
                  SystemName, p->PrivilegeName, Luid);
        }
        SetLastError(ERROR_NO_SUCH_PRIVILEGE);
        Ret= FALSE;
    }
    return Ret;
}

VOID EnumPrivileges(HANDLE Token, BOOL All) {
    BOOL Ret= FALSE;
    DWORD TokenLength= 0;
    PTOKEN_PRIVILEGES TokenPriv= NULL;
    DWORD PrivilegeNameLength= 256;
    DWORD DisplayNameLength= 256;
    PWCHAR PrivilegeName= NULL;
    PWCHAR DisplayName= NULL;
    LPCWCHAR SymbolName= NULL;
   
    // LUID = Locally Unique Identifier
    wprintf(L"——————————————————————————————————-\n");
    wprintf(L"   LUID                Symbol                           PrivilegeName                    DisplayName\n");
    wprintf(L"——————————————————————————————————-\n");

    if ( !All ) {
        if ( !GetTokenInformation(Token, TokenPrivileges, NULL, 0, &TokenLength) &&
                GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
            wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n",
                    GetLastError());
            goto cleanup;
        }

        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), 0, TokenLength);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }

        if ( !GetTokenInformation(Token,
                TokenPrivileges, TokenPriv, TokenLength, &TokenLength) ) {
            wprintf(L"GetTokenInformation failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }

    }
    else {
        TokenPriv= (PTOKEN_PRIVILEGES)HeapAlloc(GetProcessHeap(), 0,
            sizeof(DWORD)+sizeof(LUID_AND_ATTRIBUTES)*MAX_PRIVSCAN);
        if ( !TokenPriv ) {
            wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
            goto cleanup;
        }
       
        TokenPriv->PrivilegeCount= MAX_PRIVSCAN;
        for (  LONGLONG i=0 ; i<MAX_PRIVSCAN ; ++i ) {
            TokenPriv->Privileges[i].Luid= *(PLUID)&i;
            TokenPriv->Privileges[i].Attributes= 0;
        }
    }
   
    for ( DWORD i=0 ; i<TokenPriv->PrivilegeCount ; ++i ) {
        do {
            if ( PrivilegeName ) delete [] PrivilegeName;
            if ( DisplayName ) delete [] DisplayName;

            PrivilegeName= new WCHAR[PrivilegeNameLength];
            DisplayName= new WCHAR[DisplayNameLength];

            Ret= LookupPrivilegeName(NULL, &TokenPriv->Privileges[i].Luid, &SymbolName,
                    PrivilegeName, &PrivilegeNameLength,
                    DisplayName, &DisplayNameLength,
                    All);
        } while( !Ret && GetLastError()==ERROR_INSUFFICIENT_BUFFER );

        if ( Ret ) {
            WCHAR Mark= 0;
            if ( All ) {
                LONG l= 0;
                CheckPrivilege(Token, PrivilegeName, &l);
                Mark= l==0 ? Mark= ‘X’ :
                    l>0 ? Mark= ‘O’ : ‘-‘;
            }
            else {
                Mark= TokenPriv->Privileges[i].Attributes&SE_PRIVILEGE_ENABLED ?
                       L’O’ : L’X’;
            }

            wprintf(L" %c 0x%08x`%08x %-32s %-32s %s\n", Mark,
                TokenPriv->Privileges[i].Luid.HighPart,
                TokenPriv->Privileges[i].Luid.LowPart,
                SymbolName,
                PrivilegeName,
                DisplayName);
        }
    }

cleanup:
    if ( PrivilegeName ) delete [] PrivilegeName;
    if ( DisplayName ) delete [] DisplayName;
    if ( TokenPriv ) HeapFree(GetProcessHeap(), 0, TokenPriv);
}

// http://msdn.microsoft.com/en-us/library/ms722492(v=VS.85) InitLsaString
//
http://msdn.microsoft.com/en-us/library/ms721874(v=vs.85).aspx
// http://msdn.microsoft.com/en-us/library/ms721863(v=vs.85).aspx
BOOL AddPrivilege(HANDLE Token, LPCWSTR PrivilegeName) {
    NTSTATUS Ret= 0;
    LSA_OBJECT_ATTRIBUTES ObjectAttributes;
    LSA_HANDLE PolicyHandle= NULL;
    PSID Sid= NULL;
    LSA_UNICODE_STRING Privilege[1];
    size_t PrivNameLength= 0;
    PTOKEN_USER CurrentUserSid= NULL;
    DWORD CurrentUserSidLength= 0;

    // get current user SID from the token
    if ( !GetTokenInformation(Token, TokenUser, NULL, 0, &CurrentUserSidLength) &&
            GetLastError()!=ERROR_INSUFFICIENT_BUFFER ) {
        wprintf(L"GetTokenInformation (size check) failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    CurrentUserSid= (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0, CurrentUserSidLength);
    if ( !CurrentUserSid ) {
        wprintf(L"HeapAlloc failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    if ( !GetTokenInformation(Token, TokenUser, CurrentUserSid,
            CurrentUserSidLength, &CurrentUserSidLength) ) {
        wprintf(L"GetTokenInformation failed – 0x%08x\n", GetLastError());
        goto cleanup;
    }
   
    PrivNameLength= StringCchLength(PrivilegeName, MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);
   
    ZeroMemory(&ObjectAttributes, sizeof(ObjectAttributes));
    Ret= LsaOpenPolicy(NULL, &ObjectAttributes, POLICY_ALL_ACCESS, &PolicyHandle);
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaOpenPolicy failed – 0x%08x\n", LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    StringCchLength(PrivilegeName, MAX_PRIVNAME, &PrivNameLength);
    Privilege[0].Buffer= (PWCHAR)PrivilegeName;
    Privilege[0].Length= PrivNameLength*sizeof(WCHAR);
    Privilege[0].MaximumLength= (PrivNameLength+1)*sizeof(WCHAR);

    Ret= LsaAddAccountRights(PolicyHandle, CurrentUserSid->User.Sid, Privilege, 1);;
    if ( Ret!=STATUS_SUCCESS ) {
        wprintf(L"LsaAddAccountRights failed – 0x%08x\n", LsaNtStatusToWinError(Ret));
        goto cleanup;
    }

    wprintf(L"Privilege ‘%s’ was assigned successfully.\n", PrivilegeName);
    wprintf(L"To apply it to the token, re-log on the system.\n");

cleanup:
    if ( PolicyHandle ) LsaClose(PolicyHandle);   
    if ( CurrentUserSid ) HeapFree(GetProcessHeap(), 0, CurrentUserSid);

    return Ret==STATUS_SUCCESS;
}

// >0 Enabled
// =0 Disabled
// <0 Not assigned
BOOL CheckPrivilege(HANDLE Token, LPCWSTR PrivilegeName, LPLONG Privileged) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, PrivilegeName, &luid) ){
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }

    PRIVILEGE_SET PrivilegeSet;
    PrivilegeSet.Control= 0;
    PrivilegeSet.PrivilegeCount= 1;
    PrivilegeSet.Privilege[0].Luid= luid;
    PrivilegeSet.Privilege[0].Attributes= 0; // not used

    BOOL Check;
    if ( !PrivilegeCheck(Token, &PrivilegeSet, &Check) ) {
        wprintf(L"PrivilegeCheck failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( Check )
        *Privileged= 1;
    else {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount= 1;
        tp.Privileges[0].Luid= luid;
        tp.Privileges[0].Attributes= 0;

        if ( !AdjustTokenPrivileges(Token,
                FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
            wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n", GetLastError());
            return FALSE;
        }

        *Privileged= (GetLastError()==ERROR_NOT_ALL_ASSIGNED) ? -1 : 0;
    }

    return TRUE;
}
 
BOOL EnablePrivilege(HANDLE Token, LPWSTR Name, BOOL Enabled) {
    LUID luid;
    if ( !LookupPrivilegeValueEx(NULL, Name, &luid) ) {
        wprintf(L"LookupPrivilegeValue failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    TOKEN_PRIVILEGES tp;
    tp.PrivilegeCount= 1;
    tp.Privileges[0].Luid= luid;
    tp.Privileges[0].Attributes=
      Enabled ? SE_PRIVILEGE_ENABLED : 0; // not use SE_PRIVILEGE_REMOVED, just disable

    if ( !AdjustTokenPrivileges(Token,
            FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL) ) {
        wprintf(L"AdjustTokenPrivileges failed – 0x%08x\n", GetLastError());
        return FALSE;
    }
   
    if ( GetLastError()==ERROR_NOT_ALL_ASSIGNED ) {
        wprintf(L"The process token does not have %s (%I64d).\n", Name, luid);
        return FALSE;
    }

    wprintf(L"%s (%I64d) is temporarily %s.\n", Name, luid,
        Enabled ? L"enabled" : L"disabled");

    return TRUE;
}

ここで定義した関数 EnumPrivileges を使って、特権の一覧を出力させると、こんな感じです。
(出力が横に広い・・・)

——————————————————————————————————-
   LUID                Symbol                           PrivilegeName                    DisplayName
——————————————————————————————————-
– 0x00000000`00000002 SE_CREATE_TOKEN_NAME             SeCreateTokenPrivilege           トークン オブジェクトの作成
X 0x00000000`00000003 SE_ASSIGNPRIMARYTOKEN_NAME       SeAssignPrimaryTokenPrivilege    プロセス レベル トークンの置き換え
– 0x00000000`00000004 SE_LOCK_MEMORY_NAME              SeLockMemoryPrivilege            メモリ内のページのロック
X 0x00000000`00000005 SE_INCREASE_QUOTA_NAME           SeIncreaseQuotaPrivilege         プロセスのメモリ クォータの増加
– 0x00000000`00000006 SE_MACHINE_ACCOUNT_NAME          SeMachineAccountPrivilege        ドメインにワークステーションを追加
– 0x00000000`00000007 SE_TCB_NAME                      SeTcbPrivilege                   オペレーティング システムの一部として機能
X 0x00000000`00000008 SE_SECURITY_NAME                 SeSecurityPrivilege              監査とセキュリティ ログの管理
X 0x00000000`00000009 SE_TAKE_OWNERSHIP_NAME           SeTakeOwnershipPrivilege         ファイルとその他のオブジェクトの所有権の取得
X 0x00000000`0000000a SE_LOAD_DRIVER_NAME              SeLoadDriverPrivilege            デバイス ドライバーのロードとアンロード
X 0x00000000`0000000b SE_SYSTEM_PROFILE_NAME           SeSystemProfilePrivilege         システム パフォーマンスのプロファイル
X 0x00000000`0000000c SE_SYSTEMTIME_NAME               SeSystemtimePrivilege            システム時刻の変更
X 0x00000000`0000000d SE_PROF_SINGLE_PROCESS_NAME      SeProfileSingleProcessPrivilege  単一プロセスのプロファイル
X 0x00000000`0000000e SE_INC_BASE_PRIORITY_NAME        SeIncreaseBasePriorityPrivilege  スケジューリング優先順位の繰り上げ
X 0x00000000`0000000f SE_CREATE_PAGEFILE_NAME          SeCreatePagefilePrivilege        ページ ファイルの作成
– 0x00000000`00000010 SE_CREATE_PERMANENT_NAME         SeCreatePermanentPrivilege       永続的共有オブジェクトの作成
X 0x00000000`00000011 SE_BACKUP_NAME                   SeBackupPrivilege                ファイルとディレクトリのバックアップ
X 0x00000000`00000012 SE_RESTORE_NAME                  SeRestorePrivilege               ファイルとディレクトリの復元
X 0x00000000`00000013 SE_SHUTDOWN_NAME                 SeShutdownPrivilege              システムのシャットダウン
X 0x00000000`00000014 SE_DEBUG_NAME                    SeDebugPrivilege                 プログラムのデバッグ
– 0x00000000`00000015 SE_AUDIT_NAME                    SeAuditPrivilege                 セキュリティ監査の生成
X 0x00000000`00000016 SE_SYSTEM_ENVIRONMENT_NAME       SeSystemEnvironmentPrivilege     ファームウェア環境値の修正
O 0x00000000`00000017 SE_CHANGE_NOTIFY_NAME            SeChangeNotifyPrivilege          走査チェックのバイパス
X 0x00000000`00000018 SE_REMOTE_SHUTDOWN_NAME          SeRemoteShutdownPrivilege        リモート コンピューターからの強制シャットダウン
X 0x00000000`00000019 SE_UNDOCK_NAME                   SeUndockPrivilege                ドッキング ステーションからコンピューターを削除
– 0x00000000`0000001a SE_SYNC_AGENT_NAME               SeSyncAgentPrivilege             ディレクトリ サービス データの同期化
– 0x00000000`0000001b SE_ENABLE_DELEGATION_NAME        SeEnableDelegationPrivilege      コンピューターとユーザー アカウントに委任時の信頼を付与
X 0x00000000`0000001c SE_MANAGE_VOLUME_NAME            SeManageVolumePrivilege          ボリュームの保守タスクを実行
O 0x00000000`0000001d SE_IMPERSONATE_NAME              SeImpersonatePrivilege           認証後にクライアントを偽装
O 0x00000000`0000001e SE_CREATE_GLOBAL_NAME            SeCreateGlobalPrivilege          グローバル オブジェクトの作成
– 0x00000000`0000001f SE_TRUSTED_CREDMAN_ACCESS_NAME   SeTrustedCredManAccessPrivilege  資格情報マネージャーに信頼された呼び出し側としてアクセス
– 0x00000000`00000020 SE_RELABEL_NAME                  SeRelabelPrivilege               オブジェクト ラベルの変更
X 0x00000000`00000021 SE_INC_WORKING_SET_NAME          SeIncreaseWorkingSetPrivilege    プロセス ワーキング セットの増加
X 0x00000000`00000022 SE_TIME_ZONE_NAME                SeTimeZonePrivilege              タイム ゾーンの変更
X 0x00000000`00000023 SE_CREATE_SYMBOLIC_LINK_NAME     SeCreateSymbolicLinkPrivilege    シンボリック リンクの作成

[Win32] [x86 Assembler] Time Stamp Counter

久々にアセンブラ関連の記事を書きます。需要はあるのだろうか・・・

プログラムの中で、実行時間を計測したくなる場面は多々あります。そんなときはどんな API を使うでしょうか。パッと思いつくのは timeGetTime とか GetTickCount ですかね。GetSystemTime を使う人はあまりいないでしょう。QueryPerformanceCounter というのもあります。

timeGetTime 関数
http://msdn.microsoft.com/ja-jp/library/cc428795.aspx

GetTickCount 関数
http://msdn.microsoft.com/ja-jp/library/cc429827.aspx

GetSystemTime function
http://msdn.microsoft.com/en-us/library/ms724390(v=vs.85).aspx

QueryPerformanceCounter function
http://msdn.microsoft.com/en-us/library/ms644904(v=vs.85).aspx

どれを使ってもいいわけですが、とにかく厳密に測りたい場合や、一瞬で終わる処理を計測したい場合には、これらの API を使うと関数呼び出し時の時間のロスが無視できなくなります。もちろん、その分のロスを計算して結果から引くというのが普通ですが、アセンブラを使うという手も一応あります。

それが、RDTSC (=Read Time Stamp Counter) 命令です。詳細は IA-32 の仕様書に書いてありますが、RDTSC を実行すると、1 命令で Tick 値を取得することができます。しかも単位はクロック単位です。Pentium 以降の IA-32 アーキテクチャーでは、プロセッサーごとに TSC (=Time Stamp Counter) という 64bit カウンターが MSR (マシン固有レジスタ) に含まれており、RDTSC はこれを EDX:EAX レジスターにロードする命令です。

こんなプログラムを書いてみました。後でいろいろ遊ぶ伏線として、幾つかの関数を読んでいます。

なお、開発環境、実行環境は同じで、こんな感じです。

  • OS: Windows 7 SP1 (x64)
  • IDE: Visual Studio 2010 SP1
  • CPU: Core i3 530 2.93GHz (予算がないのだ)

//
// main.cpp
//

#include <Windows.h>
#include <stdio.h>

__int64 __fastcall rdtsc() {
    __asm {
        rdtsc
    }
}

__inline __int64 rdtsc_inline() {
    __asm {
        rdtsc
    }
}

void rdtsc_x86(long long int *ll) {
    __asm {
        rdtsc
        mov ecx, [ll]
        mov [ecx], eax
        mov [ecx+4], edx
    }
}

int wmain(int argc, wchar_t *argv[]) {
    if ( argc<2 )
        return ERROR_INVALID_PARAMETER;
   
    LARGE_INTEGER ll, ll1, ll2, ll3;

    wprintf(L"—–\n");

    DWORD wait= _wtoi(argv[1]);
    while (1) {
        QueryPerformanceCounter(&ll);
        ll1.QuadPart= rdtsc();
        ll2.QuadPart= rdtsc_inline();
        rdtsc_x86((long long*)&ll3);

        wprintf(L"QPC:  0x%08x`%08x\n", ll.HighPart, ll.LowPart);
        wprintf(L"ASM1: 0x%08x`%08x\n", ll1.HighPart, ll1.LowPart);
        wprintf(L"ASM2: 0x%08x`%08x\n", ll2.HighPart, ll2.LowPart);
        wprintf(L"ASM3: 0x%08x`%08x\n", ll3.HighPart, ll3.LowPart);
   
        wprintf(L"–\n");
        Sleep(wait);
    }

    return 0;
}

実行すると、こんな感じです。

E:\Visual Studio 2010\Projects\asmtest\Release> asmtest 1000
—–
QPC:  0x0000000a`cdfd9d44
ASM1: 0x00002b37`f6751321
ASM2: 0x00002b37`f675135e
ASM3: 0x00002b37`f6751394

QPC:  0x0000000a`ce28d20e
ASM1: 0x00002b38`a3483a0d
ASM2: 0x00002b38`a3483a4a
ASM3: 0x00002b38`a3483a80

QPC:  0x0000000a`ce54577b
ASM1: 0x00002b39`515df112
ASM2: 0x00002b39`515df142
ASM3: 0x00002b39`515df172

QPC:  0x0000000a`ce802c82
ASM1: 0x00002b3a`00b20afe
ASM2: 0x00002b3a`00b20b2e
ASM3: 0x00002b3a`00b20b5e

ASM1 の結果に絞って間隔を計算すると、ACD326EC, AE15B705, AF5419EC となり、平均値は AE14529F = 2,920,567,455 となります。プロセッサーのクロックが 2.93GHz なので、RDTSC は確かにクロック単位の値を取得しています。

サンプルでは 3 つの関数を書きました。

  • rdtsc ・・・ __fastcall 呼び出し規約
  • rdtsc_inline ・・・ __inline でインライン展開
  • rdtsc_x86 ・・・ パラメーター渡し

ソースコードを既定の Release ビルド設定でビルドすると、rdtsc_inline だけでなく rdtsc_x86 もインライン展開されます。rdtsc に __fastcall をつけたのはインライン展開を防ぐためで、__stdcall や __cdecl では展開されてしまいます。

コンパイルされた関数のアセンブラを見てみます。

0:000> uf asmtest!rdtsc
asmtest!rdtsc [e:\visual studio 2010\projects\asmtest\main.cpp @ 9]:
    9 011e1000 0f31            rdtsc
   13 011e1002 c3              ret

0:000> uf asmtest!wmain
asmtest!wmain [e:\visual studio 2010\projects\asmtest\main.cpp @ 30]:
   30 011e1010 55              push    ebp
   30 011e1011 8bec            mov     ebp,esp
   30 011e1013 83e4f8          and     esp,0FFFFFFF8h
   30 011e1016 83ec2c          sub     esp,2Ch
   31 011e1019 837d0802        cmp     dword ptr [ebp+8],2
   31 011e101d 53              push    ebx
   31 011e101e 56              push    esi
   31 011e101f 57              push    edi
   31 011e1020 7d0c            jge     asmtest!wmain+0x1e (011e102e)

asmtest!wmain+0x12 [e:\visual studio 2010\projects\asmtest\main.cpp @ 55]:
   55 011e1022 5f              pop     edi
   55 011e1023 5e              pop     esi
   55 011e1024 b857000000      mov     eax,57h
   55 011e1029 5b              pop     ebx
   55 011e102a 8be5            mov     esp,ebp
   55 011e102c 5d              pop     ebp
   55 011e102d c3              ret

asmtest!wmain+0x1e [e:\visual studio 2010\projects\asmtest\main.cpp @ 36]:
   36 011e102e 8b359c201e01    mov     esi,dword ptr [asmtest!_imp__wprintf (011e209c)]
   36 011e1034 68f4201e01      push    offset asmtest!`string’ (011e20f4)
   36 011e1039 ffd6            call    esi
   38 011e103b 8b450c          mov     eax,dword ptr [ebp+0Ch]
   38 011e103e 8b4804          mov     ecx,dword ptr [eax+4]
   38 011e1041 51              push    ecx
   38 011e1042 ff15a0201e01    call    dword ptr [asmtest!_imp___wtoi (011e20a0)]
   38 011e1048 83c408          add     esp,8
   38 011e104b 89442414        mov     dword ptr [esp+14h],eax
   38 011e104f 90              nop

asmtest!wmain+0x40 [e:\visual studio 2010\projects\asmtest\main.cpp @ 40]:
   40 011e1050 8d542418        lea     edx,[esp+18h]
   40 011e1054 52              push    edx
   40 011e1055 ff1500201e01    call    dword ptr [asmtest!_imp__QueryPerformanceCounter (011e2000)]
   41 011e105b e8a0ffffff      call    asmtest!rdtsc (011e1000)
   41 011e1060 8bd8            mov     ebx,eax
   41 011e1062 8954242c        mov     dword ptr [esp+2Ch],edx
   42 011e1066 0f31            rdtsc
   42 011e1068 8bf8            mov     edi,eax
   43 011e106a 8d442420        lea     eax,[esp+20h]
   43 011e106e 89542434        mov     dword ptr [esp+34h],edx
   43 011e1072 89442410        mov     dword ptr [esp+10h],eax

   43 011e1076 0f31            rdtsc
   43 011e1078 8b4c2410        mov     ecx,dword ptr [esp+10h]
   43 011e107c 8901            mov     dword ptr [ecx],eax
   43 011e107e 895104          mov     dword ptr [ecx+4],edx
   45 011e1081 8b4c2418        mov     ecx,dword ptr [esp+18h]
   45 011e1085 8b54241c        mov     edx,dword ptr [esp+1Ch]
   45 011e1089 51              push    ecx
   45 011e108a 52              push    edx
   45 011e108b 6804211e01      push    offset asmtest!`string’ (011e2104)
   45 011e1090 ffd6            call    esi
   46 011e1092 8b442438        mov     eax,dword ptr [esp+38h]
   46 011e1096 53              push    ebx
   46 011e1097 50              push    eax
   46 011e1098 682c211e01      push    offset asmtest!`string’ (011e212c)
   46 011e109d ffd6            call    esi
   47 011e109f 8b4c244c        mov     ecx,dword ptr [esp+4Ch]
   47 011e10a3 57              push    edi
   47 011e10a4 51              push    ecx
   47 011e10a5 6854211e01      push    offset asmtest!`string’ (011e2154)
   47 011e10aa ffd6            call    esi
   48 011e10ac 8b542444        mov     edx,dword ptr [esp+44h]
   48 011e10b0 8b442448        mov     eax,dword ptr [esp+48h]
   48 011e10b4 52              push    edx
   48 011e10b5 50              push    eax
   48 011e10b6 687c211e01      push    offset asmtest!`string’ (011e217c)
   48 011e10bb ffd6            call    esi
   50 011e10bd 68a4211e01      push    offset asmtest!`string’ (011e21a4)
   50 011e10c2 ffd6            call    esi
   51 011e10c4 8b4c2448        mov     ecx,dword ptr [esp+48h]
   51 011e10c8 83c434          add     esp,34h
   51 011e10cb 51              push    ecx
   51 011e10cc ff1504201e01    call    dword ptr [asmtest!_imp__Sleep (011e2004)]
   52 011e10d2 e979ffffff      jmp     asmtest!wmain+0x40 (011e1050)

関数 rdtsc は、RDTSC を実行するだけの関数になっています。インライン展開された関数は茶色と紫色で示しています。戻り値が EDX:EAX であることが考慮されて、うまく動くようになっています。edx レジスタが考慮されないのかと予想していましたが、インライン展開されても 64bit の戻り値に影響はないようです。

さて、次に QueryPerformanceCounter に注目してみます。上述の MSDN の説明を見ると、こんな注意書きがあります。

On a multiprocessor computer, it should not matter which processor is called. However, you can get different results on different processors due to bugs in the basic input/output system (BIOS) or the hardware abstraction layer (HAL). To specify processor affinity for a thread, use the SetThreadAffinityMask function.

「マルチ プロセッサーでも使えますよ。でも BIOS や HAL にバグがあるとプロセッサー毎に異なる結果が返ってくることがありますよ。」 ということです。BIOS や HAL のバグって言われても・・・既知のバグなら直しとけよ、としか言いようがありません。QueryPerformanceCounter を使うときは SetThreadAffinityMask を必ず使えってことなのでしょうか。微妙です。

少なくとも、QueryPerformanceCounter にはマルチプロセッサーを考慮した実装がなされていることが分かります。TSC カウンターはプロセッサー毎なので、先ほどの RDTSC 命令を呼び出す関数は、プロセッサー毎に異なる値を返します。よって、途中で割り込みが入って実行 CPU が変わることが予想される場合には使えません。

デバッグしていて気づきましたが、実は QueryPerformanceCounter は内部的に RDTSC を呼び出しています。これを確かめてみます。

0:000> x kernel32!QueryPerformanceCounter
76921732 kernel32!QueryPerformanceCounter = <no type information>
0:000> u 76921732
kernel32!QueryPerformanceCounter:
76921732 ff25d40d9276    jmp     dword ptr [kernel32!_imp__QueryPerformanceCounter (76920dd4)]
76921738 90              nop
76921739 90              nop
7692173a 90              nop
7692173b 90              nop
7692173c 90              nop
kernel32!IsDBCSLeadByte:
7692173d ff2568079276    jmp     dword ptr [kernel32!_imp__IsDBCSLeadByte (76920768)]
76921743 90              nop
0:000> dd 76920dd4 l1
76920dd4  775e8884
0:000> ln 775e8884
(775e8884)   ntdll!RtlQueryPerformanceCounter   |  (775e88e2)   ntdll!EtwEventEnabled
Exact matches:
    ntdll!RtlQueryPerformanceCounter = <no type information>

QueryPerformanceCounter は Kernel32.dll の関数ですが、これは単なるラッパーで、実体は ntdll.dll に実装されている RtlQueryPerformanceCounter であることが分かります。この関数のアセンブラを見てみます。

0:000> uf ntdll!RtlQueryPerformanceCounter
ntdll!RtlQueryPerformanceCounter:
775e8884 8bff            mov     edi,edi
775e8886 55              push    ebp
775e8887 8bec            mov     ebp,esp
775e8889 51              push    ecx
775e888a 51              push    ecx
775e888b f605ed02fe7f01  test    byte ptr [SharedUserData+0x2ed (7ffe02ed)],1
775e8892 0f840bf50400    je      ntdll!RtlQueryPerformanceCounter+0x55 (77637da3)

ntdll!RtlQueryPerformanceCounter+0x10:
775e8898 56              push    esi

ntdll!RtlQueryPerformanceCounter+0x11:
775e8899 8b0db803fe7f    mov     ecx,dword ptr [SharedUserData+0x3b8 (7ffe03b8)]
775e889f 8b35bc03fe7f    mov     esi,dword ptr [SharedUserData+0x3bc (7ffe03bc)]
775e88a5 a1b803fe7f      mov     eax,dword ptr [SharedUserData+0x3b8 (7ffe03b8)]
775e88aa 8b15bc03fe7f    mov     edx,dword ptr [SharedUserData+0x3bc (7ffe03bc)]
775e88b0 3bc8            cmp     ecx,eax
775e88b2 75e5            jne     ntdll!RtlQueryPerformanceCounter+0x11 (775e8899)

ntdll!RtlQueryPerformanceCounter+0x2c:
775e88b4 3bf2            cmp     esi,edx
775e88b6 75e1            jne     ntdll!RtlQueryPerformanceCounter+0x11 (775e8899)

ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31            rdtsc
775e88ba 03c1            add     eax,ecx
775e88bc 0fb60ded02fe7f  movzx   ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6            adc     edx,esi
775e88c5 c1e902          shr     ecx,2
775e88c8 e893ffffff      call    ntdll!_aullshr (775e8860)
775e88cd 8b4d08          mov     ecx,dword ptr [ebp+8]
775e88d0 8901            mov     dword ptr [ecx],eax
775e88d2 895104          mov     dword ptr [ecx+4],edx
775e88d5 5e              pop     esi

ntdll!RtlQueryPerformanceCounter+0x4e:
775e88d6 33c0            xor     eax,eax
775e88d8 40              inc     eax

ntdll!RtlQueryPerformanceCounter+0x51:
775e88d9 c9              leave
775e88da c20400          ret     4

ntdll!RtlQueryPerformanceCounter+0x55:
77637da3 8d45f8          lea     eax,[ebp-8]
77637da6 50              push    eax
77637da7 ff7508          push    dword ptr [ebp+8]
77637daa e8717ff9ff      call    ntdll!ZwQueryPerformanceCounter (775cfd20)
77637daf 85c0            test    eax,eax
77637db1 7d0d            jge     ntdll!RtlQueryPerformanceCounter+0x6f (77637dc0)

ntdll!RtlQueryPerformanceCounter+0x65:
77637db3 50              push    eax
77637db4 e89549fdff      call    ntdll!RtlSetLastWin32ErrorAndNtStatusFromNtStatus (7760c74e)

ntdll!RtlQueryPerformanceCounter+0x6b:
77637db9 33c0            xor     eax,eax
77637dbb e9190bfbff      jmp     ntdll!RtlQueryPerformanceCounter+0x51 (775e88d9)

ntdll!RtlQueryPerformanceCounter+0x6f:
77637dc0 837df800        cmp     dword ptr [ebp-8],0
77637dc4 0f850c0bfbff    jne     ntdll!RtlQueryPerformanceCounter+0x4e (775e88d6)

ntdll!RtlQueryPerformanceCounter+0x75:
77637dca 837dfc00        cmp     dword ptr [ebp-4],0
77637dce 0f85020bfbff    jne     ntdll!RtlQueryPerformanceCounter+0x4e (775e88d6)

ntdll!RtlQueryPerformanceCounter+0x7b:
77637dd4 6a78            push    78h
77637dd6 e814a5f9ff      call    ntdll!RtlSetLastWin32Error (775d22ef)
77637ddb ebdc            jmp     ntdll!RtlQueryPerformanceCounter+0x6b (77637db9)

RDTSC 命令がありました。さっきのサンプルプログラムでこの部分にブレークポイントを貼ると、確かに RDTSC が実行されていることが分かります。

0:002> bp 775e88b8
0:002> g
Breakpoint 0 hit
eax=00000000 ebx=14c34d78 ecx=00000000 edx=00000000 esi=00000000 edi=14c34da8
eip=775e88b8 esp=0045fe80 ebp=0045fe8c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31            rdtsc
0:000> k
ChildEBP RetAddr
0045fe8c 00bb105b ntdll!RtlQueryPerformanceCounter+0x30
0045fecc 6a19263d asmtest!wmain+0x4b
0045ff18 7692339a MSVCR100!_initterm+0x13
0045ff24 775e9ed2 kernel32!BaseThreadInitThunk+0xe
0045ff64 775e9ea5 ntdll!__RtlUserThreadStart+0x70
0045ff7c 00000000 ntdll!_RtlUserThreadStart+0x1b

プログラムの実行結果を見ると分かりますが、QueryPerformanceCounter (以下 QPC と表記) と RDTSC の戻り値は全然違います。単位も異なっているようです。QPC の周波数は QueryPerformanceFrequency API で取得することができます。この環境で調べてみると、0x2BD8E6 = 2,852,116 となりました。クロックの 1/1000 ぐらいです。

QPC が RDTSC の値を元にしていることは確認できているので、何らかの計算をして周波数を調整していることになります。それが RtlQueryPerformanceCounter のアセンブラに隠されています。RDTSC の後のアセンブラをもう一度よく見てみます。

ntdll!RtlQueryPerformanceCounter+0x30:
775e88b8 0f31            rdtsc
775e88ba 03c1            add     eax,ecx
775e88bc 0fb60ded02fe7f  movzx   ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6            adc     edx,esi
775e88c5 c1e902          shr     ecx,2
775e88c8 e893ffffff      call    ntdll!_aullshr (775e8860)
775e88cd 8b4d08          mov     ecx,dword ptr [ebp+8]
775e88d0 8901            mov     dword ptr [ecx],eax
775e88d2 895104          mov     dword ptr [ecx+4],edx
775e88d5 5e              pop     esi

ntdll!RtlQueryPerformanceCounter+0x4e:
775e88d6 33c0            xor     eax,eax
775e88d8 40              inc     eax

ntdll!RtlQueryPerformanceCounter+0x51:
775e88d9 c9              leave
775e88da c20400          ret     4

_anullshr という関数が怪しいですね。これを見てみます。

0:000> uf ntdll!_aullshr
ntdll!_aullshr:
775e8860 80f940          cmp     cl,40h
775e8863 7315            jae     ntdll!_aullshr+0x1a (775e887a)

ntdll!_aullshr+0x5:
775e8865 80f920          cmp     cl,20h
775e8868 7306            jae     ntdll!_aullshr+0x10 (775e8870)

ntdll!_aullshr+0xa:
775e886a 0fadd0          shrd    eax,edx,cl
775e886d d3ea            shr     edx,cl
775e886f c3              ret

ntdll!_aullshr+0x10:
775e8870 8bc2            mov     eax,edx
775e8872 33d2            xor     edx,edx
775e8874 80e11f          and     cl,1Fh
775e8877 d3e8            shr     eax,cl
775e8879 c3              ret

ntdll!_aullshr+0x1a:
775e887a 33c0            xor     eax,eax
775e887c 33d2            xor     edx,edx
775e887e c3              ret

eax と edx をcl だけ右シフトしています。これで QPC の結果が小さくなるわけです。
では ECX レジスタはどこから来ていたでしょうか。

775e88bc 0fb60ded02fe7f  movzx   ecx,byte ptr [SharedUserData+0x2ed (7ffe02ed)]
775e88c3 13d6            adc     edx,esi
775e88c5 c1e902          shr     ecx,2
775e88c8 e893ffffff      call    ntdll!_aullshr (775e8860)

SharedUserData から来ています。これは共有ユーザーモードページと呼ばれるデータで、デバッガー コマンドの !kuser で概要を表示できます。詳しくはデバッガーのヘルプを参照して下さい。

0:000> !kuser
_KUSER_SHARED_DATA at 7ffe0000
TickCount:    fa00000 * 000000000011d55a (0:05:04:21.406)
TimeZone Id: 0
ImageNumber Range: [8664 .. 8664]
Crypto Exponent: 0
SystemRoot: ‘C:\Windows’

ここでちょっとしたトリックが必要になります。
実はユーザーモード側から _KUSER_SHARED_DATA 構造体を見ても、全メンバーを見ることができません。そこで、カーネル モードから見る必要があります。同じ環境で livekd を使った出力がこれです。

Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

E:\Visual Studio 2010\Projects\asmtest\Release>livekd

LiveKd v5.0 – Execute kd/windbg on a live system
Sysinternals – http://www.sysinternals.com
Copyright (C) 2000-2010 Mark Russinovich and Ken Johnson

Launching c:\debuggers\amd64\kd.exe:

Microsoft (R) Windows Debugger Version 6.12.0002.633 AMD64
Copyright (c) Microsoft Corporation. All rights reserved.

Loading Dump File [C:\Windows\livekd.dmp]
Kernel Complete Dump File: Full address space is available

Comment: ‘LiveKD live system view’
Symbol search path is: srv*c:\websymbols*
http://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 7 Kernel Version 7601 (Service Pack 1) MP (4 procs) Free x64
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 7601.17640.amd64fre.win7sp1_gdr.110622-1506
Machine Name:
Kernel base = 0xfffff800`03a63000 PsLoadedModuleList = 0xfffff800`03ca8670
Debug session time: Sun Feb 13 11:34:57.897 17420 (UTC + 9:00)
System Uptime: 0 days 5:44:06.294
Loading Kernel Symbols
………………………………………………………
……………………………………………………….
……………………………………
Loading User Symbols
…………
Loading unloaded module list
……..Unable to enumerate user-mode unloaded modules, NTSTATUS 0xC0000147
0: kd> !kuser
*** ERROR: Module load completed but symbols could not be loaded for LiveKdD.SYS
_KUSER_SHARED_DATA at fffff78000000000
TickCount:    fa00000 * 0000000000142992 (0:05:44:06.281)
TimeZone Id: 0
ImageNumber Range: [8664 .. 8664]
Crypto Exponent: 0
SystemRoot: ‘C:\Windows’
0: kd> dt _KUSER_SHARED_DATA fffff78000000000
ntdll!_KUSER_SHARED_DATA
   +0x000 TickCountLowDeprecated : 0
(省略)
   +0x2ed TscQpcData       : 0x29 ‘)’
   +0x2ed TscQpcEnabled    : 0y1
   +0x2ed TscQpcSpareFlag  : 0y0
   +0x2ed TscQpcShift      : 0y001010 (0xa)
   +0x2ee TscQpcPad        : [2]  ""
(省略)

ntdll!_aullshr に渡されていたデータは SharedUserData+0x2ed でした。+2ed のところには、TscQpc*** という、まさに TSC と QPC に関連した値が保存されていることが分かります。

_KUSER_SHARED_DATA は公開されている構造体なので、Windows Driver Kit  に含まれる ntddk.h で定義を確認することができます。それがこれです。

//
// The following byte is consumed by the user-mode performance counter
// routines to improve latency when the source is the processor’s cycle
// counter.
//

union {
    UCHAR TscQpcData;
    struct {
        UCHAR TscQpcEnabled   : 1;
        UCHAR TscQpcSpareFlag : 1;
        UCHAR TscQpcShift     : 6;
    } DUMMYSTRUCTNAME;
} DUMMYUNIONNAME;

ビットフィールドになっていて、3 つのフィールドがあります。名前から推測できてしまいますが、一応アセンブラで確認していきましょう。ntdll!RtlQueryPerformanceCounter において、ntdll!_aullshr を呼び出す前に shr ecx,2 という命令で 2 ビット右シフトさせます。これは何かというと、TscQpcData を 2 ビット右シフト、つまり、ビットフィールドの TscQpcShift を取得していることになります。で、カーネル デバッガーの出力結果を見るとこの値は TscQpcShift : 0y001010 (0xa) 、つまり 10 です。これで謎が解けました。

RDTSC の結果と QPC の結果が 1000 倍ぐらい違っていましたが、これは TscQpcShift  の値だけ右シフトした値、つまり 1024 倍異なっていたことになります。これで QPC の仕組みは大体分かりました。

もう一度 ntdll!RtlQueryPerformanceCounter のアセンブラに戻ります。ntdll!RtlQueryPerformanceCounter+0x51 の後に ret 命令があって、ここで RDTSC を右シフトした値を返して終わるわけですが、関数自体は続いています。これはいつ呼ばれるでしょうか。それが関数のアタマにある、この部分です。

775e888b f605ed02fe7f01  test    byte ptr [SharedUserData+0x2ed (7ffe02ed)],1
775e8892 0f840bf50400    je      ntdll!RtlQueryPerformanceCounter+0x55 (77637da3)

また SharedUserData の値を使っています。+2ed なので TscQpcData の値ですが、今度は最下位ビットを test 命令で調べています。つまり TscQpcEnabled です。これが 0 の場合は、RDTSC が使われずに、ntdll!ZwQueryPerformanceCounter が呼ばれることになります。つまりカーネル モードの関数が呼ばれます。この関数が何を使っているのかについては、ここでは触れません。