Skip to content

Commit

Permalink
Merge pull request #9 from LibrePCB/fix-windows-64bit
Browse files Browse the repository at this point in the history
Fix broken Windows DLL injection for 64-bit architecture
  • Loading branch information
ubruhin committed Mar 17, 2024
2 parents b9adcfc + a341ecb commit 5a39812
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 98 deletions.
54 changes: 38 additions & 16 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down Expand Up @@ -130,26 +132,40 @@ 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}}

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:
Expand All @@ -161,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
Expand All @@ -176,12 +198,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
Expand All @@ -197,6 +217,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}}
203 changes: 132 additions & 71 deletions server/funq_server/runner_win.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,25 @@
# 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

# Constants from Windows API documentation.
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


Expand All @@ -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)

Expand All @@ -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)
19 changes: 13 additions & 6 deletions server/libFunq/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,16 +25,23 @@ 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
${QT}::Widgets
${QT}::Test
$<$<BOOL:${WITH_QTQUICK}>:${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})
1 change: 1 addition & 0 deletions server/player_tester/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set(CMAKE_AUTORCC OFF)

add_executable(
player_tester
WIN32
fenPrincipale.cpp
fenPrincipale.h
fenPrincipale.ui
Expand Down
Loading

0 comments on commit 5a39812

Please sign in to comment.