Last active
May 17, 2026 15:40
-
-
Save valinet/5e380b857a3800309248c54897316c95 to your computer and use it in GitHub Desktop.
Launch processes from SSH into the logged in user session
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // cl /nologo main.c kernel32.lib user32.lib advapi32.lib wtsapi32.lib userenv.lib /O1 /GS- /Gs9999999 /GF /kernel /link /ENTRY:main /NODEFAULTLIB /SUBSYSTEM:windows /NOCOFFGRPINFO /ALIGN:16 /MERGE:.rdata=.text /MERGE:.pdata=.text /OUT:local_spawn.exe | |
| #include <Windows.h> | |
| #include <strsafe.h> | |
| #include <TlHelp32.h> | |
| #include <wtsapi32.h> | |
| #include <UserEnv.h> | |
| #include <Lmcons.h> | |
| #ifndef _DEBUG | |
| #pragma comment(linker, "/NODEFAULTLIB") | |
| #pragma comment(linker, "/ENTRY:main") | |
| #endif | |
| #pragma comment(lib, "Kernel32.lib") | |
| #pragma comment(lib, "User32.lib") | |
| #pragma comment(lib, "Advapi32.lib") | |
| #pragma comment(lib, "Wtsapi32.lib") | |
| #pragma comment(lib, "Userenv.lib") | |
| #ifndef _DEBUG | |
| inline BOOL CheckWin32Impl(BOOL result, const char* expr, const char* file, int line) { | |
| DWORD err = GetLastError(); | |
| if (err != ERROR_SUCCESS) { | |
| ExitProcess(err); | |
| } | |
| if (!result) | |
| ExitProcess(result); | |
| return result; | |
| } | |
| #else | |
| inline INT64 CheckWin32Impl(INT64 result, const char* expr, const char* file, int line) { | |
| DWORD err = GetLastError(); | |
| if (err != ERROR_SUCCESS) { | |
| LPSTR msg = NULL; | |
| FormatMessageA( | |
| FORMAT_MESSAGE_ALLOCATE_BUFFER | | |
| FORMAT_MESSAGE_FROM_SYSTEM | | |
| FORMAT_MESSAGE_IGNORE_INSERTS, | |
| NULL, err, | |
| MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), | |
| (LPSTR)&msg, 0, NULL); | |
| char buf[1024]; | |
| wsprintfA(buf, "[%s:%d] %s failed (err=%lu): %s\n", | |
| file, line, expr, err, | |
| msg ? msg : "<no message>"); | |
| OutputDebugStringA(buf); | |
| printf(buf); | |
| if (msg) LocalFree(msg); | |
| ExitProcess(err); | |
| } | |
| if (!result) | |
| ExitProcess(result); | |
| return result; | |
| } | |
| #endif | |
| #define CHECK(expr) (SetLastError(ERROR_SUCCESS), CheckWin32Impl((expr), #expr, __FILE__, __LINE__)) | |
| #define IsEqualToLiteralW(s, literal) \ | |
| (RtlCompareMemory((s), L"" literal, sizeof(L"" literal)) == sizeof(L"" literal)) | |
| inline BOOL VnPatchIAT(HMODULE hMod, PSTR libName, PSTR funcName, uintptr_t hookAddr) | |
| { | |
| // Increment module reference count to prevent other threads from unloading it while we're working with it | |
| HMODULE module; | |
| if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)hMod, &module)) return FALSE; | |
| // Get a reference to the import table to locate the kernel32 entry | |
| PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)module; | |
| PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((uintptr_t)module + dos->e_lfanew); | |
| PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)((uintptr_t)module + | |
| nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); | |
| // In the import table find the entry that corresponds to kernel32 | |
| BOOL found = FALSE; | |
| while (importDescriptor->Characteristics && importDescriptor->Name) { | |
| PSTR importName = (PSTR)((PBYTE)module + importDescriptor->Name); | |
| found = TRUE; | |
| char* pA = importName, * pB = libName; | |
| while (*pB) { | |
| if (*pA++ != *pB++ && *(pA - 1) != (*(pB - 1) - 32) && (*(pA - 1) - 32) != *(pB - 1)) { | |
| found = FALSE; | |
| break; | |
| } | |
| } | |
| if (found) | |
| break; | |
| importDescriptor++; | |
| } | |
| if (!found) { | |
| FreeLibrary(module); | |
| return FALSE; | |
| } | |
| // From the kernel32 import descriptor, go over its IAT thunks to | |
| // find the one used by the rest of the code to call GetProcAddress | |
| PIMAGE_THUNK_DATA oldthunk = (PIMAGE_THUNK_DATA)((PBYTE)module + importDescriptor->OriginalFirstThunk); | |
| PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)((PBYTE)module + importDescriptor->FirstThunk); | |
| while (thunk->u1.Function) { | |
| PROC* funcStorage = (PROC*)&thunk->u1.Function; | |
| BOOL bFound = FALSE; | |
| if (oldthunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) | |
| { | |
| bFound = (!(*((WORD*)&(funcName)+1)) && IMAGE_ORDINAL32(oldthunk->u1.Ordinal) == (LONG64)funcName); | |
| } | |
| else | |
| { | |
| PIMAGE_IMPORT_BY_NAME byName = (PIMAGE_IMPORT_BY_NAME)((uintptr_t)module + oldthunk->u1.AddressOfData); | |
| if ((*((WORD*)&(funcName)+1))) { | |
| bFound = TRUE; | |
| char* pA = (char*)byName->Name, * pB = funcName; | |
| while (*pB) { | |
| if (*pA++ != *pB++ && *(pA - 1) != (*(pB - 1) - 32) && (*(pA - 1) - 32) != *(pB - 1)) { | |
| bFound = FALSE; | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| // Found it, now let's patch it | |
| if (bFound) { | |
| // Get the memory page where the info is stored | |
| MEMORY_BASIC_INFORMATION mbi; | |
| VirtualQuery(funcStorage, &mbi, sizeof(MEMORY_BASIC_INFORMATION)); | |
| // Try to change the page to be writable if it's not already | |
| if (!VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_READWRITE, &mbi.Protect)) { | |
| FreeLibrary(module); | |
| return FALSE; | |
| } | |
| // Store our hook | |
| *funcStorage = (PROC)hookAddr; | |
| // Restore the old flag on the page | |
| DWORD dwOldProtect; | |
| VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOldProtect); | |
| // Profit | |
| FreeLibrary(module); | |
| return TRUE; | |
| } | |
| thunk++; | |
| oldthunk++; | |
| } | |
| FreeLibrary(module); | |
| return FALSE; | |
| } | |
| BOOL WINAPI GetComputerNameExW_Hook(COMPUTER_NAME_FORMAT NameType, LPWSTR lpBuffer, LPDWORD nSize) { | |
| SetLastError(ERROR_SUCCESS); | |
| BOOL rv = GetComputerNameExW(NameType, lpBuffer, nSize); | |
| if (rv && GetLastError() != ERROR_SUCCESS) | |
| SetLastError(ERROR_SUCCESS); | |
| return rv; | |
| } | |
| int main(int argc, char** argv) { | |
| PROCESS_INFORMATION pi; | |
| STARTUPINFOW si; | |
| for (int i = 0; i < sizeof(si); i = i + 2) | |
| ((char*)(&si))[i] = 0; | |
| for (int i = 1; i < sizeof(si); i = i + 2) | |
| ((char*)(&si))[i] = 0; | |
| si.cb = sizeof(si); | |
| DWORD dwSessionId = 0; | |
| CHECK(ProcessIdToSessionId(GetCurrentProcessId(), &dwSessionId)); | |
| if (dwSessionId) { | |
| wchar_t* pCommandLine = GetCommandLineW(); | |
| while (*pCommandLine && *pCommandLine++ != L' '); | |
| pCommandLine++; | |
| INPUT i = { .type = INPUT_MOUSE }; | |
| SendInput(1, &i, sizeof(i)); | |
| CHECK(CreateProcessW(NULL, pCommandLine, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)); | |
| ExitProcess(GetLastError()); | |
| } | |
| // This fixes CreateEnvironmentBlock returning 203 | |
| HMODULE hProfApi = LoadLibraryW(L"profapi.dll"); | |
| if (hProfApi) | |
| CHECK(VnPatchIAT(hProfApi, "api-ms-win-core-sysinfo-l1-1-0.dll", "GetComputerNameExW", (uintptr_t)GetComputerNameExW_Hook)); | |
| PROCESSENTRY32W entry; | |
| entry.dwSize = sizeof(PROCESSENTRY32W); | |
| HANDLE hSnapshot = INVALID_HANDLE_VALUE; | |
| CHECK((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)) != INVALID_HANDLE_VALUE); | |
| CHECK(Process32FirstW(hSnapshot, &entry)); | |
| HANDLE hProcess = INVALID_HANDLE_VALUE; | |
| do { | |
| if (IsEqualToLiteralW(entry.szExeFile, L"winlogon.exe")) { | |
| CHECK((hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID)) != INVALID_HANDLE_VALUE); | |
| break; | |
| } | |
| } while (CHECK(Process32NextW(hSnapshot, &entry))); | |
| HANDLE hToken = INVALID_HANDLE_VALUE; | |
| CHECK(OpenProcessToken(hProcess, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, &hToken)); | |
| CHECK(ImpersonateLoggedOnUser(hToken)); | |
| WTS_SESSION_INFOW* wsi = NULL; | |
| DWORD kWsi = 0; | |
| CHECK(WTSEnumerateSessionsW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &wsi, &kWsi)); | |
| DWORD dwActiveSessionId = -1; | |
| for (unsigned i = 0; i < kWsi; ++i) { | |
| if (wsi[i].State == WTSActive) { | |
| dwActiveSessionId = wsi[i].SessionId; | |
| break; | |
| } | |
| } | |
| if (dwActiveSessionId == -1) | |
| dwActiveSessionId = WTSGetActiveConsoleSessionId(); | |
| CHECK(dwActiveSessionId != -1); | |
| HANDLE hImpersonationToken = INVALID_HANDLE_VALUE; | |
| CHECK(WTSQueryUserToken(dwActiveSessionId, &hImpersonationToken)); | |
| LPVOID lpEnvironment = NULL; | |
| CHECK(CreateEnvironmentBlock(&lpEnvironment, hImpersonationToken, FALSE)); | |
| HANDLE hOwnToken = INVALID_HANDLE_VALUE; | |
| CHECK(OpenThreadToken(GetCurrentThread(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, FALSE, &hOwnToken)); | |
| TOKEN_PRIVILEGES tokPrivs; | |
| tokPrivs.PrivilegeCount = 1; | |
| tokPrivs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; | |
| CHECK(LookupPrivilegeValueW(NULL, L"SeAssignPrimaryTokenPrivilege", &tokPrivs.Privileges[0].Luid)); | |
| CHECK(AdjustTokenPrivileges(hOwnToken, FALSE, &tokPrivs, sizeof(tokPrivs), NULL, NULL)); | |
| CHECK(LookupPrivilegeValueW(NULL, L"SeIncreaseQuotaPrivilege", &tokPrivs.Privileges[0].Luid)); | |
| CHECK(AdjustTokenPrivileges(hOwnToken, FALSE, &tokPrivs, sizeof(tokPrivs), NULL, NULL)); | |
| //wchar_t* pCommandLine = GetCommandLineW(); | |
| //while (*pCommandLine && *pCommandLine++ != L' '); | |
| //pCommandLine++; | |
| // wchar_t* p = (wchar_t*)lpEnvironment; | |
| // int i = 0; | |
| // while (*p) { | |
| // wprintf(L"[%d] %s\n", i++, p); | |
| // p += wcslen(p) + 1; | |
| // } | |
| // wprintf(L"(total: %d vars)\n", i); | |
| // CREATE_BREAKAWAY_FROM_JOB is essential => https://www.sysadmins.lv/retired-msft-blogs/alejacma/createprocessasuser-fails-with-error-5-access-denied-when-using-jobs.aspx | |
| CHECK(CreateProcessAsUserW(hImpersonationToken, NULL, GetCommandLineW(), NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT | CREATE_BREAKAWAY_FROM_JOB, lpEnvironment, NULL, &si, &pi)); | |
| ExitProcess(GetLastError()); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment