Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix broken Windows DLL injection for 64-bit architecture #9

Merged
merged 5 commits into from
Mar 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading