From 3445c56e89f0b5bdc7bb30d3390302f2384bd24d Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Sun, 17 Mar 2024 16:25:38 +0100 Subject: [PATCH 1/5] Fix Windows DLL injection for x86_64 --- server/funq_server/runner_win.py | 203 ++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 71 deletions(-) diff --git a/server/funq_server/runner_win.py b/server/funq_server/runner_win.py index bf176c9..bfab975 100644 --- a/server/funq_server/runner_win.py +++ b/server/funq_server/runner_win.py @@ -33,13 +33,15 @@ # knowledge of the CeCILL v2.1 license and that you accept its terms. from funq_server.runner import RunnerInjector -from ctypes import windll, wintypes, byref +from ctypes import wintypes, byref +import ctypes import time # Useful resources regarding DLL injection: # # - https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices # noqa: E501 # - https://blog.nettitude.com/uk/dll-injection-part-two +# - https://stackoverflow.com/questions/17392721/error-invalid-parameter-error-57-when-calling-createremotethread-with-python-3-2/17524073#17524073 # noqa: E501 # - https://stackoverflow.com/questions/27332509/createremotethread-on-loadlibrary-and-get-the-hmodule-back # noqa: E501 # - https://github.com/numaru/injector @@ -47,8 +49,9 @@ PROCESS_CREATE_THREAD = 0x0002 PROCESS_VM_OPERATION = 0x0008 PROCESS_VM_WRITE = 0x0020 -MEM_COMMIT = 0x00001000 -MEM_RESERVE = 0x00002000 +MEM_COMMIT = 0x1000 +MEM_RESERVE = 0x2000 +MEM_RELEASE = 0x8000 PAGE_READWRITE = 0x04 @@ -62,7 +65,7 @@ def start_subprocess(self): # too early, it does not work 100% reliable (in rare cases, the # process freezes or crashes). When slightly delaying the injection, # it seems to work more reliable. One seconds seems to be a safe - # choice to also make it reliable if the system is very busy. # + # choice to also make it reliable if the system is very busy. # Hopefully someone finds a better way some day (without delay)... time.sleep(1.0) @@ -84,79 +87,137 @@ def start_subprocess(self): raise def _inject_dll(self, pid, dll_path): - # Get handle to kernel32.dll. - kernel32_handle = windll.kernel32.GetModuleHandleA(b"kernel32.dll") - if not kernel32_handle: - self._raise_windows_error("GetModuleHandleA()", kernel32_handle) - - # Get handle to LoadLibraryA(). - loadlibrary_address = windll.kernel32.GetProcAddress( - kernel32_handle, b"LoadLibraryA") - if not loadlibrary_address: - self._raise_windows_error("GetProcAddress()", loadlibrary_address) - - # Get handle to the running process. - process_handle = windll.kernel32.OpenProcess( - PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, - 0, pid) - if not process_handle: - self._raise_windows_error("OpenProcess()", process_handle) - - # Allocate memory for the DLL path. - dll_path = dll_path.encode("ascii") - path_address = windll.kernel32.VirtualAllocEx( - process_handle, 0, len(dll_path), MEM_COMMIT | MEM_RESERVE, - PAGE_READWRITE) - if not path_address: - self._raise_windows_error("VirtualAllocEx()", path_address) - - # Write DLL path into the allocated memory region. - success = windll.kernel32.WriteProcessMemory( - process_handle, path_address, dll_path, len(dll_path), None) - if not success: - self._raise_windows_error("WriteProcessMemory()", success) - - # Create and start new thread in the process. The entry point of the - # new thread is LoadLibraryA() with our DLL path as argument. - thread_handle = windll.kernel32.CreateRemoteThread( - process_handle, 0, 0, loadlibrary_address, path_address, 0, None) - if not thread_handle: - self._raise_windows_error("CreateRemoteThread()", thread_handle) - - # Release process handle since we no longer need it. - success = windll.kernel32.CloseHandle(process_handle) - if not success: - self._raise_windows_error("CloseHandle()", success) - - # Wait (with 10s timeout) until the thread exited, i.e. our DLL - # injection either succeeded or failed. - error = windll.kernel32.WaitForSingleObject(thread_handle, 10000) - if error: - self._raise_windows_error("WaitForSingleObject()", error) - - # Get the exit code of our thread, which corresponds to the return - # value of LoadLibraryA() so we can check if the DLL was loaded - # successfully or not. - libfunq_handle = wintypes.DWORD(0) - success = windll.kernel32.GetExitCodeThread( - thread_handle, byref(libfunq_handle)) - if not success: - self._raise_windows_error("GetExitCodeThread()", success) - if not libfunq_handle: - self._raise_windows_error("LoadLibraryA()", libfunq_handle) - - # Release thread handle since we no longer need it. - success = windll.kernel32.CloseHandle(thread_handle) - if not success: - self._raise_windows_error("CloseHandle()", success) + # Get handle to kernel32.dll and prepare functions. + kernel32 = ctypes.WinDLL('kernel32.dll', use_last_error=True) + kernel32.OpenProcess.restype = wintypes.HANDLE + kernel32.OpenProcess.argtypes = ( + wintypes.DWORD, # dwDesiredAccess + wintypes.BOOL, # bInheritHandle + wintypes.DWORD, # dwProcessId + ) + kernel32.VirtualAllocEx.restype = wintypes.LPVOID + kernel32.VirtualAllocEx.argtypes = ( + wintypes.HANDLE, # hProcess + wintypes.LPVOID, # lpAddress + ctypes.c_size_t, # dwSize + wintypes.DWORD, # flAllocationType + wintypes.DWORD, # flProtect + ) + kernel32.VirtualFreeEx.restype = wintypes.BOOL + kernel32.VirtualFreeEx.argtypes = ( + wintypes.HANDLE, # hProcess + wintypes.LPVOID, # lpAddress + ctypes.c_size_t, # dwSize + wintypes.DWORD, # dwFreeType + ) + kernel32.WriteProcessMemory.restype = wintypes.BOOL + kernel32.WriteProcessMemory.argtypes = ( + wintypes.HANDLE, # hProcess + wintypes.LPVOID, # lpBaseAddress + wintypes.LPCVOID, # lpBuffer + ctypes.c_size_t, # nSize + ctypes.POINTER(ctypes.c_size_t), # lpNumberOfBytesWritten _Out_ + ) + kernel32.CreateRemoteThread.restype = wintypes.LPVOID + kernel32.CreateRemoteThread.argtypes = ( + wintypes.HANDLE, # hProcess + wintypes.LPVOID, # lpThreadAttributes + ctypes.c_size_t, # dwStackSize + wintypes.LPVOID, # lpStartAddress + wintypes.LPVOID, # lpParameter + wintypes.DWORD, # dwCreationFlags + wintypes.LPDWORD, # lpThreadId _Out_ + ) + kernel32.WaitForSingleObject.restype = wintypes.DWORD + kernel32.WaitForSingleObject.argtypes = ( + wintypes.HANDLE, # hHandle + wintypes.DWORD, # dwMilliseconds + ) + kernel32.GetExitCodeThread.restype = wintypes.BOOL + kernel32.GetExitCodeThread.argtypes = ( + wintypes.HANDLE, # hThread + wintypes.LPDWORD, # lpExitCode + ) + kernel32.CloseHandle.restype = wintypes.BOOL + kernel32.CloseHandle.argtypes = ( + wintypes.HANDLE, # hObject + ) + + # Start the injection. + size = (len(dll_path) + 1) * ctypes.sizeof(wintypes.WCHAR) + h_process = None + adr_path = None + h_thread = None + try: + # Get handle to the running process. + h_process = kernel32.OpenProcess( + PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | + PROCESS_VM_WRITE, False, pid) + if h_process is None: + self._raise_windows_error("OpenProcess()", h_process) + + # Allocate memory for the DLL path. + adr_path = kernel32.VirtualAllocEx( + h_process, None, size, MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE) + if adr_path is None: + self._raise_windows_error("VirtualAllocEx()", adr_path) + + # Write DLL path into the allocated memory region. + success = kernel32.WriteProcessMemory( + h_process, adr_path, dll_path, size, None) + if not success: + self._raise_windows_error("WriteProcessMemory()", success) + + # Create and start new thread in the process. The entry point of + # the new thread is LoadLibraryW() with our DLL path as argument. + h_thread = kernel32.CreateRemoteThread( + h_process, None, 0, kernel32.LoadLibraryW, adr_path, 0, None) + if h_thread is None: + self._raise_windows_error("CreateRemoteThread()", h_thread) + + # Wait (with 10s timeout) until the thread exited, i.e. our DLL + # injection either succeeded or failed. + error = kernel32.WaitForSingleObject(h_thread, 10000) + if error: + self._raise_windows_error("WaitForSingleObject()", error) + + # Get the exit code of our thread, which corresponds to the return + # value of LoadLibraryW() so we can check if the DLL was loaded + # successfully or not. + libfunq_handle = wintypes.DWORD(0) + success = kernel32.GetExitCodeThread( + h_thread, byref(libfunq_handle)) + if not success: + self._raise_windows_error("GetExitCodeThread()", success) + if not libfunq_handle: + self._raise_windows_error("LoadLibraryW()", libfunq_handle) + finally: + if adr_path is not None: + success = kernel32.VirtualFreeEx(h_process, adr_path, 0, + MEM_RELEASE) + if not success: + self._raise_windows_error("VirtualFreeEx()", success) + + if h_thread is not None: + success = kernel32.CloseHandle(h_thread) + if not success: + self._raise_windows_error("CloseHandle()", success) + + if h_process is not None: + success = kernel32.CloseHandle(h_process) + if not success: + self._raise_windows_error("CloseHandle()", success) def _raise_windows_error(self, function_name, return_value): """ Helper function to raise an error returned by a WIN32 API function. """ - last_error = windll.kernel32.GetLastError() + last_error = ctypes.get_last_error() + win_error = ctypes.WinError(last_error) message = "Failed to inject DLL! " message += "{} returned 0x{:X}. ".format(function_name, return_value) - message += "The last error is 0x{:X}. ".format(last_error) + message += "The last error is 0x{:X} ({}). ".format( + last_error, str(win_error)) message += "Maybe x86/x64 mismatch between python.exe and Qt DLLs?" raise RuntimeError(message) From 2251dabe4c585217158f089af956310e75038d3f Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Sun, 17 Mar 2024 16:23:01 +0100 Subject: [PATCH 2/5] Fix broken executables on Windows CI --- .github/workflows/main.yml | 10 ++++------ server/libFunq/CMakeLists.txt | 19 +++++++++++++------ server/player_tester/CMakeLists.txt | 1 + server/tests/libFunq/CMakeLists.txt | 3 ++- server/tests/protocole/CMakeLists.txt | 3 ++- .../funq-test-app/CMakeLists.txt | 1 + 6 files changed, 23 insertions(+), 14 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81892ba..4f61a70 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -176,12 +176,10 @@ jobs: cd build cmake ../server -DBUILD_TESTS=1 -DBUILD_DISALLOW_WARNINGS=1 make - - # Note: The executables don't run yet, don't know why :-/ - # - name: Run libFunq tests - # run: build/tests/libFunq/testLibFunq.exe - # - name: Run protocole tests - # run: build/tests/protocole/testProtocole.exe + - name: Run libFunq tests + run: build\tests\libFunq\testLibFunq.exe + - name: Run protocole tests + run: build\tests\protocole\testProtocole.exe # Server - name: Install server diff --git a/server/libFunq/CMakeLists.txt b/server/libFunq/CMakeLists.txt index 848fdeb..f578646 100644 --- a/server/libFunq/CMakeLists.txt +++ b/server/libFunq/CMakeLists.txt @@ -2,8 +2,8 @@ set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTORCC OFF) -add_library( - Funq SHARED +set( + FUNQ_SOURCES delayedresponse.cpp delayedresponse.h dragndropresponse.cpp @@ -25,12 +25,13 @@ add_library( shortcutresponse.h ) if(WIN32) - target_sources(Funq PRIVATE WindowsInjector.cpp WindowsInjector.h) + list(APPEND FUNQ_SOURCES WindowsInjector.cpp WindowsInjector.h) else() - target_sources(Funq PRIVATE ldPreloadInjector.cpp) + list(APPEND FUNQ_SOURCES ldPreloadInjector.cpp) endif() -target_link_libraries( - Funq PUBLIC + +set( + FUNQ_DEPENDENCIES ${QT}::Core ${QT}::Gui ${QT}::Network @@ -38,3 +39,9 @@ target_link_libraries( ${QT}::Test $<$:${QT}::Quick> ) + +add_library(FunqStatic STATIC ${FUNQ_SOURCES}) +target_link_libraries(FunqStatic PUBLIC ${FUNQ_DEPENDENCIES}) + +add_library(Funq SHARED ${FUNQ_SOURCES}) +target_link_libraries(Funq PUBLIC ${FUNQ_DEPENDENCIES}) diff --git a/server/player_tester/CMakeLists.txt b/server/player_tester/CMakeLists.txt index a8ddad0..b469ff1 100644 --- a/server/player_tester/CMakeLists.txt +++ b/server/player_tester/CMakeLists.txt @@ -4,6 +4,7 @@ set(CMAKE_AUTORCC OFF) add_executable( player_tester + WIN32 fenPrincipale.cpp fenPrincipale.h fenPrincipale.ui diff --git a/server/tests/libFunq/CMakeLists.txt b/server/tests/libFunq/CMakeLists.txt index 1cf6423..5fd769a 100644 --- a/server/tests/libFunq/CMakeLists.txt +++ b/server/tests/libFunq/CMakeLists.txt @@ -4,6 +4,7 @@ set(CMAKE_AUTORCC OFF) add_executable( testLibFunq + WIN32 test.cpp ) target_include_directories( @@ -16,7 +17,7 @@ target_compile_options( ) target_link_libraries( testLibFunq PUBLIC - Funq + FunqStatic ${QT}::Core ${QT}::Gui ${QT}::Widgets diff --git a/server/tests/protocole/CMakeLists.txt b/server/tests/protocole/CMakeLists.txt index e1aff6b..3966cd8 100644 --- a/server/tests/protocole/CMakeLists.txt +++ b/server/tests/protocole/CMakeLists.txt @@ -4,6 +4,7 @@ set(CMAKE_AUTORCC OFF) add_executable( testProtocole + WIN32 test.cpp ) target_include_directories( @@ -12,6 +13,6 @@ target_include_directories( ) target_link_libraries( testProtocole PUBLIC - Funq + FunqStatic ${QT}::Core ) diff --git a/tests-functionnal/funq-test-app/CMakeLists.txt b/tests-functionnal/funq-test-app/CMakeLists.txt index 69a498e..63a4f9c 100644 --- a/tests-functionnal/funq-test-app/CMakeLists.txt +++ b/tests-functionnal/funq-test-app/CMakeLists.txt @@ -82,6 +82,7 @@ set(CMAKE_AUTOUIC OFF) set(CMAKE_AUTORCC OFF) add_executable( funq-test-app + WIN32 main.cpp widgets.h ) From abefdfdf237b02afde5a1e85a2dc6891a3d5fb41 Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Sun, 17 Mar 2024 16:25:04 +0100 Subject: [PATCH 3/5] Add simple injection test for CI --- .github/workflows/main.yml | 6 ++++++ tests-functionnal/funq-test-app/main.cpp | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4f61a70..d3ce931 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -76,6 +76,8 @@ jobs: # Functional tests - name: Build test app run: cd tests-functionnal/funq-test-app && cmake . && make + - name: Test injection + run: xvfb-run -a funq tests-functionnal/funq-test-app/funq-test-app --exit-after-startup - name: Test functional run: cd tests-functionnal && xvfb-run -a nosetests if: ${{ matrix.nosetests != 0}} @@ -130,6 +132,8 @@ jobs: # Functional tests - name: Build test app run: cd tests-functionnal/funq-test-app && cmake . && make + - name: Test injection + run: funq tests-functionnal/funq-test-app/funq-test-app --exit-after-startup - name: Test functional run: cd tests-functionnal && xvfb-run -a nosetests if: ${{ matrix.nosetests != 0}} @@ -195,6 +199,8 @@ jobs: # Functional tests - name: Build test app run: cd tests-functionnal/funq-test-app && cmake . && make + - name: Test injection + run: funq tests-functionnal/funq-test-app/funq-test-app.exe --exit-after-startup - name: Test functional run: cd tests-functionnal && nosetests if: ${{ matrix.nosetests != 0}} diff --git a/tests-functionnal/funq-test-app/main.cpp b/tests-functionnal/funq-test-app/main.cpp index 8fd5f4f..dd21fc7 100644 --- a/tests-functionnal/funq-test-app/main.cpp +++ b/tests-functionnal/funq-test-app/main.cpp @@ -35,6 +35,7 @@ knowledge of the CeCILL v2.1 license and that you accept its terms. #include "widgets.h" #include +#include template inline void execDialog(QLabel * statusLabel, QWidget * parent) { @@ -45,6 +46,13 @@ inline void execDialog(QLabel * statusLabel, QWidget * parent) { int main(int argc, char * argv[]) { QApplication app(argc, argv); + if (app.arguments().contains("--exit-after-startup")) { + // Used to test injection without user interaction. + qDebug() << "Application started with '--exit-after-startup'."; + QThread::msleep(5000); + return 0; + } + if (app.arguments().contains("--show-message-box-at-startup")) { // This is needed to test if the injection of libFunq also works if Qt's // main event loop is not called directly at application startup. A From 1bf210d39042781a2b8bc480c6e0b340e6501dea Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Sun, 17 Mar 2024 16:33:37 +0100 Subject: [PATCH 4/5] Add more Windows CI jobs --- .github/workflows/main.yml | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d3ce931..d3f746d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -139,21 +139,33 @@ jobs: if: ${{ matrix.nosetests != 0}} windows: - name: "qt:${{ matrix.qt }} on windows" + name: "qt:${{ matrix.qt }} py:${{ matrix.py }} ${{ matrix.arch }} on windows" runs-on: windows-2022 strategy: matrix: include: - - qt: 5 + - py: "3.8" + arch: "x86" + qt: 5 qt_full: "5.15.2" - arch: "win32_mingw81" - tools: "tools_mingw,qt.tools.win32_mingw810" + qt_arch: "win32_mingw81" + qt_tools: "tools_mingw,qt.tools.win32_mingw810" compiler_path: "D:/a/funq/Qt/Tools/mingw810_32/bin" - nosetests: 0 # Nosetest not working anymore - - qt: 6 + nosetests: 1 + - py: "3.8" + arch: "x64" + qt: 6 + qt_full: "6.7.0" + qt_arch: "win64_mingw" + qt_tools: "tools_mingw1310" + compiler_path: "D:/a/funq/Qt/Tools/mingw1310_64/bin" + nosetests: 1 + - py: "3.11" + arch: "x64" + qt: 6 qt_full: "6.7.0" - arch: "win64_mingw" - tools: "tools_mingw1310" + qt_arch: "win64_mingw" + qt_tools: "tools_mingw1310" compiler_path: "D:/a/funq/Qt/Tools/mingw1310_64/bin" nosetests: 0 # Nosetest not working anymore env: @@ -165,12 +177,18 @@ jobs: shell: cmd steps: - uses: actions/checkout@v2 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "${{ matrix.py }}" + architecture: "${{ matrix.arch }}" - name: Install Qt uses: jurplel/install-qt-action@v3 with: version: "${{ matrix.qt_full }}" - tools: "${{ matrix.tools }}" - arch: "${{ matrix.arch }}" + tools: "${{ matrix.qt_tools }}" + arch: "${{ matrix.qt_arch }}" + setup-python: false cache: true # Build & test C++ modules From a341ecb45493066c0503911eaad003ccdd7d8487 Mon Sep 17 00:00:00 2001 From: "U. Bruhin" Date: Sun, 17 Mar 2024 16:21:56 +0100 Subject: [PATCH 5/5] Fix failed functional test on Qt6 --- tests-functionnal/funq-test-app/widgets.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests-functionnal/funq-test-app/widgets.h b/tests-functionnal/funq-test-app/widgets.h index 3c4a358..e89b0f1 100644 --- a/tests-functionnal/funq-test-app/widgets.h +++ b/tests-functionnal/funq-test-app/widgets.h @@ -228,13 +228,13 @@ class ComboBoxDialog : public SimpleDialog { for (int i = 0; i < 10; ++i) { combobox->addItem(QString("Item %1").arg(i)); } - connect(combobox, SIGNAL(currentIndexChanged(QString)), this, - SLOT(currentIndexChanged(QString))); + connect(combobox, SIGNAL(currentTextChanged(QString)), this, + SLOT(currentTextChanged(QString))); layout()->addWidget(combobox); } private slots: - void currentIndexChanged(const QString & text) { + void currentTextChanged(const QString & text) { showResult("Text: " + text); } };