From 55876d1b4e6a98d8a6ea7a03a0ef3288df4c84e7 Mon Sep 17 00:00:00 2001 From: Fredrik Orderud Date: Mon, 28 Nov 2022 13:49:44 +0100 Subject: Extend TestCon with AppContainer sandboxing TestCon already support starting OLE controls in a "regular" separate process and in a low integrity level (low IL) process. This commit will extend QAxWidget::setClassContext and the impersonation support to also allow starting OLE controls in a AppContainer-isolated process for enhanced security sandboxing. Pick-to: 6.5 Task-number: QTBUG-109184 Change-Id: I525e99ba6934025710bbc97fba0045d0358413b7 Reviewed-by: Volker Hilsheimer --- tools/testcon/mainwindow.cpp | 2 +- tools/testcon/sandboxing.cpp | 140 ++++++++++++++++++++++++++++++++++++++++++- tools/testcon/sandboxing.h | 3 +- 3 files changed, 142 insertions(+), 3 deletions(-) (limited to 'tools') diff --git a/tools/testcon/mainwindow.cpp b/tools/testcon/mainwindow.cpp index 482840e..df81e42 100644 --- a/tools/testcon/mainwindow.cpp +++ b/tools/testcon/mainwindow.cpp @@ -107,7 +107,7 @@ bool MainWindow::addControlFromClsid(const QString &clsid, QAxSelect::Sandboxing break; default: // impersonate desired sandboxing - sandbox_impl = Sandboxing::Create(sandboxing); + sandbox_impl = Sandboxing::Create(sandboxing, clsid); // require out-of-process and activate impersonation container->setClassContext(CLSCTX_LOCAL_SERVER | CLSCTX_ENABLE_CLOAKING); break; diff --git a/tools/testcon/sandboxing.cpp b/tools/testcon/sandboxing.cpp index 4774833..e0c0eea 100644 --- a/tools/testcon/sandboxing.cpp +++ b/tools/testcon/sandboxing.cpp @@ -6,8 +6,144 @@ #include #include #include +#include +#include +/** RAII wrapper of STARTUPINFOEX. */ +struct StartupInfoExWrap +{ + STARTUPINFOEX si = {}; + + StartupInfoExWrap () + { + si.StartupInfo.cb = sizeof(STARTUPINFOEX); + + const DWORD attr_count = 1; // SECURITY_CAPABILITIES + SIZE_T attr_size = 0; + InitializeProcThreadAttributeList(NULL, attr_count, 0, &attr_size); + si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)new BYTE[attr_size](); + if (!InitializeProcThreadAttributeList(si.lpAttributeList, attr_count, 0, &attr_size)) + qFatal("InitializeProcThreadAttributeList failed"); + } + + void SetSecurity(SECURITY_CAPABILITIES *sc) + { + if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES, + sc, sizeof(SECURITY_CAPABILITIES), NULL, NULL)) + qFatal("UpdateProcThreadAttribute failed"); + } + + ~StartupInfoExWrap() + { + DeleteProcThreadAttributeList(si.lpAttributeList); + delete [] (BYTE*)si.lpAttributeList; + } +}; + +/** RAII wrapper of PROCESS_INFORMATION. */ +struct ProcessInformationWrap +{ + PROCESS_INFORMATION pi = {}; + + ~ProcessInformationWrap() + { + CloseHandle(pi.hThread); + pi.hThread = nullptr; + CloseHandle(pi.hProcess); + pi.hProcess = nullptr; + } +}; + +/** RAII class for temporarily impersonating an AppContainer-isolated process + * for the current thread. Intended to be used together with CLSCTX_ENABLE_CLOAKING + * when creating COM objects. There's no direct support for AppContainer + * impersonation in Windows, so the impl. will instead create a suspended throw-away + * process within the AppContainer to use as basis for AppContainer impersonation. + * This seem kind of weird, but is the approach recommended by Microsoft when opening + * a support case on the matter. Based on "AppContainer Isolation" + * https://learn.microsoft.com/en-us/windows/win32/secauthz/appcontainer-isolation */ +struct AppContainer : public Sandboxing +{ + AppContainer(const QString &clsid) + { + // Create AppContainer sandbox without any special capabilities + static const wchar_t name[] = L"Qt_testcon"; + static const wchar_t desc[] = L"Qt ActiveQt Test Container"; + HRESULT hr = CreateAppContainerProfile(name, name, desc, nullptr, 0, &m_sid); + if (HRESULT_CODE(hr) == ERROR_ALREADY_EXISTS) + hr = DeriveAppContainerSidFromAppContainerName(name, &m_sid); // fallback to existing container + if (FAILED(hr)) + qFatal("CreateAppContainerProfile and DeriveAppContainerSidFromAppContainerName failed"); + + SECURITY_CAPABILITIES sec_cap = {}; + sec_cap.AppContainerSid = m_sid; + + StartupInfoExWrap si; + si.SetSecurity(&sec_cap); + + // Create suspended COM server process in AppContainer + QString exe_path = GetExePath(clsid); + ProcessInformationWrap pi; + DWORD flags = EXTENDED_STARTUPINFO_PRESENT | CREATE_SUSPENDED; // don't start main thread + if (!CreateProcess(exe_path.toStdWString().data(), nullptr, nullptr, nullptr, + FALSE, flags, nullptr, nullptr, (STARTUPINFO*)&si.si, &pi.pi)) + qFatal("CreateProcess failed"); + + // Kill process since we're only interested in the handle for now. + // The COM runtime will later recreate the process when calling CoCreateInstance. + TerminateProcess(pi.pi.hProcess, 0); + + // Create AppContainer impersonation token + HANDLE cur_token = nullptr; + if (!OpenProcessToken(pi.pi.hProcess, + TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, + &cur_token)) + qFatal("OpenProcessToken failed"); + + if (!DuplicateTokenEx(cur_token, 0, NULL, SecurityImpersonation, TokenPrimary, &m_token)) + qFatal("DuplicateTokenEx failed"); + + CloseHandle(cur_token); + cur_token = nullptr; + + // Impersonate AppContainer on current thread + if (!ImpersonateLoggedOnUser(m_token)) + qFatal("ImpersonateLoggedOnUser failed"); + } + + ~AppContainer() + { + if (!RevertToSelf()) + qFatal("RevertToSelf failed"); + + CloseHandle(m_token); + m_token = nullptr; + + FreeSid(m_sid); + m_sid = nullptr; + } + +private: + /** Get EXE path for a COM class. Input is on "{hex-guid}" format. + * Returns empty string if the COM class is DLL-based and on failure. */ + static QString GetExePath (const QString &clsid) + { + // extract COM class + QSettings cls_folder("HKEY_LOCAL_MACHINE\\Software\\Classes\\CLSID\\" + clsid, QSettings::NativeFormat); + QString command = cls_folder.value("LocalServer32/.").toString(); + if (command.isEmpty()) + return ""; // not exe path found + + // remove any quotes and "/automation" or "-activex" arguments + QStringList arguments = QProcess::splitCommand(command); + return arguments[0]; // executable is first argument + } + + PSID m_sid = nullptr; + HANDLE m_token = nullptr; +}; + /** RAII class for temporarily impersonating low-integrity level for the current thread. Intended to be used together with CLSCTX_ENABLE_CLOAKING when creating COM objects. Based on "Designing Applications to Run at a Low Integrity Level" https://msdn.microsoft.com/en-us/library/bb625960.aspx */ @@ -56,10 +192,12 @@ private: HANDLE m_token = nullptr; }; -std::unique_ptr Sandboxing::Create(QAxSelect::SandboxingLevel level) +std::unique_ptr Sandboxing::Create(QAxSelect::SandboxingLevel level, const QString &clsid) { if (level == QAxSelect::SandboxingLowIntegrity) return std::make_unique(); + else if (level == QAxSelect::SandboxingAppContainer) + return std::make_unique(clsid); Q_ASSERT_X(false, "Sandboxing::Create", "unknown sandboxing level"); return {}; diff --git a/tools/testcon/sandboxing.h b/tools/testcon/sandboxing.h index d63d746..5f51227 100644 --- a/tools/testcon/sandboxing.h +++ b/tools/testcon/sandboxing.h @@ -3,13 +3,14 @@ #ifndef SANDBOXING_H #define SANDBOXING_H +#include #include class Sandboxing { public: - static std::unique_ptr Create(QAxSelect::SandboxingLevel level); + static std::unique_ptr Create(QAxSelect::SandboxingLevel level, const QString &clsid); Sandboxing() {} -- cgit v1.2.1