c++windowswinapitoken

Start Notepad.exe or Calc.exe with CreateRestrictedToken


I'd like to start a GUI application like notepad.exe or calc.exe with a restricted token that only has the permissions of some groups (with the CreateRestrictedToken() API)

However, I get this error message from csrss.exe: The application was unable to start correctly (0xc0000022)

The application was unable to start correctly (0xc0000022) csrss.exe

Here is the code I used:

#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include <iostream>
#include <conio.h> 
#include <sstream> 
#include <sddl.h>
#include <vector>


#ifndef UNICODE  
typedef std::string STRING;
#else
typedef std::wstring STRING;
#endif

// Global Variables:
HINSTANCE hInst;
STRING szTitle = _T("TITLE");
STRING szWindowClass = _T("WINDOW_CLASS");
STRING szWindowInfo;

ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR    lpCmdLine,
    int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);

    std::wcout << L"Command line: " << lpCmdLine << std::endl;

    MyRegisterClass(hInstance);


    // Pass the parsed credentials to InitInstance
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    MSG msg;


    // Main message loop:
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEX wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = NULL;
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = szWindowClass.c_str();
    wcex.hIconSm = NULL;

    return RegisterClassEx(&wcex);
}


BOOL createWindow(HWND& hWnd, HINSTANCE hInstance, int nCmdShow) {
    hInst = hInstance;

    hWnd = CreateWindow(szWindowClass.c_str(), szTitle.c_str(), WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

    if (!hWnd)
    {
        return FALSE;
    }

    szWindowInfo = _T("Class: ") + szWindowClass + _T(", Handle: 0x") + std::to_wstring((unsigned)hWnd);

    STRING szCompleteTitle = szTitle + _T(" - ") + szWindowInfo;
    ::SetWindowText(hWnd, szCompleteTitle.c_str());

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    return TRUE;
}

BOOL launchNotepad(HWND hWnd)
{
    HANDLE hToken = NULL;
    HANDLE hRestrictedToken = NULL;
    std::vector<PSID> groupSids;
    std::vector<SID_AND_ATTRIBUTES> sidAndAttributes;
    std::vector<STRING> groupNames;
    if (true)
        groupNames = { _T("Users") };
    //groupNames = { _T("testgroup") , _T("Users") };
    SID_NAME_USE sidType;

    // Open the current process token
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_READ | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, &hToken))
    {
        std::wstringstream errorMsg;
        errorMsg << L"OpenProcessToken failed. Error: " << GetLastError();
        MessageBox(NULL, errorMsg.str().c_str(), _T("Error"), MB_OK | MB_ICONERROR);
        return false;
    }

    // Iterate over group names to lookup SIDs
    for (const auto& groupName : groupNames)
    {
        PSID pGroupSid = NULL;

        STRING domainName = _T(".");
        DWORD domainNameSize = static_cast<DWORD>(domainName.size());
        DWORD dwSize = 0;

        // Lookup the SID for the group name
        if (!LookupAccountName(NULL, groupName.c_str(), NULL, &dwSize, &domainName[0], &domainNameSize, &sidType) &&
            GetLastError() == ERROR_INSUFFICIENT_BUFFER)
        {
            pGroupSid = (PSID)malloc(dwSize);
            if (!LookupAccountName(NULL, groupName.c_str(), pGroupSid, &dwSize, &domainName[0], &domainNameSize, &sidType))
            {
                std::wstringstream errorMsg;
                errorMsg << L"LookupAccountName failed for group " << groupName << L". Error: " << GetLastError();
                MessageBox(NULL, errorMsg.str().c_str(), _T("Error"), MB_OK | MB_ICONERROR);
                free(pGroupSid);
                continue;
            }
        }
        else
        {
            std::wstringstream errorMsg;
            errorMsg << L"LookupAccountName failed for group " << groupName << L". Error: " << GetLastError();
            MessageBox(NULL, errorMsg.str().c_str(), _T("Error"), MB_OK | MB_ICONERROR);
            continue;
        }

        groupSids.push_back(pGroupSid);
        SID_AND_ATTRIBUTES sidAttr = { pGroupSid, 0 };
        sidAndAttributes.push_back(sidAttr);
    }

    if (!CreateRestrictedToken(
        hToken,
        0, // DISABLE_MAX_PRIVILEGE,
        0, NULL, // No privileges to disable
        0, NULL, // No SIDs to delete
        static_cast<DWORD>(sidAndAttributes.size()), sidAndAttributes.data(),
        &hRestrictedToken))
    {
        std::wstringstream errorMsg;
        errorMsg << L"CreateRestrictedToken failed. Error: " << GetLastError();
        MessageBox(NULL, errorMsg.str().c_str(), _T("Error"), MB_OK | MB_ICONERROR);
        for (auto sid : groupSids) free(sid); // Free allocated SIDs
        return false;
    }

    _tprintf(_T("Restricted token created successfully.\n"));

    STARTUPINFO si;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(STARTUPINFO);
    PROCESS_INFORMATION pi;
    ZeroMemory(&pi, sizeof(pi));
    STRING szCmdLine = _T("notepad.exe");
    //STRING szCmdLine = _T("calc.exe");

    if (!CreateProcessAsUser(
        hRestrictedToken,
        NULL,
        &szCmdLine[0],
        NULL,
        NULL,
        FALSE,
        0,
        NULL,
        NULL,
        &si,
        &pi))
    {
        std::wstringstream errorMsg;
        errorMsg << L"CreateProcessAsUser failed. Error: " << GetLastError();
        MessageBox(NULL, errorMsg.str().c_str(), _T("Error"), MB_OK | MB_ICONERROR);
        return FALSE;
    }
    else
    {
        MessageBox(NULL, _T("Process created successfully with restricted token."), _T("Success"), MB_OK | MB_ICONINFORMATION);
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }

    // Clean up
    CloseHandle(hToken);
    CloseHandle(hRestrictedToken);
    for (auto sid : groupSids) free(sid); // Free allocated SIDs

    //find window
    HWND hNotepadWnd = NULL;
    while (hNotepadWnd == NULL)
    {
        hNotepadWnd = FindWindow(_T("Notepad"), NULL);
        Sleep(100);
    }
    MessageBox(
        NULL,                           // Handle to the owner window (NULL for no owner)
        _T("FindWindow success."), // Message to display
        _T("Error"),        // Title of the message box
        MB_OK | MB_ICONINFORMATION      // Buttons and icon type
    );

    //set parent
    SetParent(hNotepadWnd, hWnd);

    return TRUE;
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{

    HWND hWnd;
    if (!createWindow(hWnd, hInstance, nCmdShow))
    {
        MessageBox(NULL, _T("Failed to create window"), _T("Error"), MB_OK | MB_ICONERROR);
        return FALSE;
    }

    if (!launchNotepad(hWnd))
    {
        MessageBox(NULL, _T("Failed to launch Notepad"), _T("Error"), MB_OK | MB_ICONERROR);
        return FALSE;
    }

    return TRUE;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    int wmId, wmEvent;
    PAINTSTRUCT ps;
    HDC hdc;

    switch (message)
    {
    case WM_COMMAND:
        wmId = LOWORD(wParam);
        wmEvent = HIWORD(wParam);
        return DefWindowProc(hWnd, message, wParam, lParam);
        
        break;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        RECT rcWin;
        GetWindowRect(hWnd, &rcWin);
        DrawText(hdc, szWindowInfo.c_str(), -1, &rcWin, DT_LEFT | DT_TOP);
        EndPaint(hWnd, &ps);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

This can be compiled as follows:

If I disable the restriction to groupNames = { _T("Users") };, Notepad starts without problem, but then the permissions are not restricted to a subset of the current users permissions as intended.

What do I need here to make this work?

Edit: Removed two calls to CloseHandle(hToken); when the group was not found. Only triggers when using other group names / SIDs in testing, should not be executed in the minimal example.


Solution

  • However, I get this error message from csrss.exe: The application was unable to start correctly (0xc0000022)

    really you got this error message from ZwRaiseHardError api. this api send message to process hard error port. from other side csrss.exe handle this port and show MessageBoxW. process by self can not call MessageBoxW, because for this need load and init, user32.dll, gdi32.dll, etc. when for call ZwRaiseHardError need only ntdll.dll and nothing more

    concrete STATUS_ACCESS_DENIED ( 0xc0000022 ) was from NtOpenDirectoryObject for \KnownDlls

    because security descriptor for \KnownDlls not grant any access to S-1-5-32-545 'BUILTIN\Users', but grant to S-1-5-12 'NT AUTHORITY\RESTRICTED' and S-1-1-0 '\Everyone', so we need add one of this SID.

    then application usually need open HKCU key. for do this need have how minimum 'NT AUTHORITY\RESTRICTED' (for '\Everyone' nothing granted).

    if application have UI, so interract with desctop, it need have 'NT AUTHORITY\LogonSessionId_0_*' LogonSession sid. we can got it from current process token by query TokenLogonSid

    for access C:\WINDOWS\WinSxS\* folders, process need how minimum S-1-5-32-545 'BUILTIN\Users' Sid. otherwise we will get an error (in case notepad)

    SXS: Unable to open assembly directory under storage root "C:\WINDOWS\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.26100.1591_none_3e0fac18e32dc903"; Status = 0xc0000022
    SXS: Attempt to probe assembly storage root C:\WINDOWS\WinSxS\ for assembly directory amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.26100.1591_none_3e0fac18e32dc903 failed with status = 0xc0000022
    SXS: RtlGetAssemblyStorageRoot() unable to resolve storage map entry.  Status = 0xc0000022
    

    so restricted UI process need have how minimum 3 SIDs:

    S-1-5-12 'NT AUTHORITY\RESTRICTED' - for access \KnownDlls and HKCU

    S-1-5-32-545 'BUILTIN\Users' for access C:\WINDOWS\WinSxS\* folders

    'NT AUTHORITY\LogonSessionId_0_*' for access Desktop

    demo code ( how i use alloca incompatible with /RTCs option. so it must be off)

    void StartRestricted(PCWSTR lpApplicationName, PWSTR lpCommandLine)
    {
        HANDLE hToken, hRestrictedToken = 0;
        NTSTATUS status;
        if (0 <= (status = NtOpenProcessToken(NtCurrentProcess(), TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY, &hToken)))
        {   
            ULONG cb = 0, rcb = sizeof(TOKEN_GROUPS) + SECURITY_SID_SIZE(3), 
                cbSids = SECURITY_SID_SIZE(1) + SECURITY_SID_SIZE(2), cbSid;
    
            union {
                PBYTE pb;
                PTOKEN_GROUPS ptg;
                PVOID buf;
            };
    
            PVOID stack = alloca(cbSids);
    
            do 
            {
                cb = RtlPointerToOffset(buf = alloca(rcb - cb), stack);
    
                status = NtQueryInformationToken(hToken, TokenLogonSid, buf, cb, &rcb);
            } while (STATUS_BUFFER_TOO_SMALL == status);
    
            if (0 <= status)
            {
                if (1 == ptg->GroupCount)
                {
                    static const WELL_KNOWN_SID_TYPE types[] = { WinRestrictedCodeSid, WinBuiltinUsersSid };
    
                    PSID_AND_ATTRIBUTES SidsToRestrict = ptg->Groups;
                    ptg->Groups->Attributes = 0;
    
                    pb += rcb;
                    alloca(_countof(types)*sizeof(SID_AND_ATTRIBUTES));
    
                    ULONG n = _countof(types);
                    do 
                    {
                        (--SidsToRestrict)->Attributes = 0;
                        if (status = GetLastHr(CreateWellKnownSid(types[--n], 0, SidsToRestrict->Sid = pb, &(cbSid = cbSids))))
                        {
                            break;
                        }
    
                        pb += cbSid, cbSids -= cbSid;
    
                    } while (n);
    
                    if (S_OK == status)
                    {
                        status = GetLastHr(CreateRestrictedToken(hToken, LUA_TOKEN, 0, 0, 0, 0, 
                            _countof(types) + 1, SidsToRestrict, &hRestrictedToken));
                    }
                }
                else
                {
                    status = STATUS_INTERNAL_ERROR;
                }
            }
    
            NtClose(hToken);
    
            if (S_OK == status)
            {
                STARTUPINFOW si = { sizeof(si) };
                PROCESS_INFORMATION pi;
    
                if (S_OK == (status = GetLastHr(CreateProcessAsUserW(
                    hRestrictedToken, lpApplicationName, lpCommandLine, 0, 0, 0, 0, 0, 0, &si, &pi))))
                {
                    NtClose(pi.hThread);
                    NtClose(pi.hProcess);
                }
                NtClose(hRestrictedToken);
            }
        }
    }
    
    inline HRESULT GetLastHr(ULONG dwError = GetLastError())
    {
        return dwError ? HRESULT_FROM_WIN32(dwError) : S_OK;
    }
    

    (i use NtQueryInformationToken, NtOpenProcessToken, because it more easy to use, it direct return error code and not need call GetLastError(), but of course easy possible replace it to kernel32 api)