From 52859d19c475001802b1a763f9af24629a77d3f5 Mon Sep 17 00:00:00 2001 From: Darius Houle Date: Sun, 26 Jan 2025 23:38:50 -0700 Subject: [PATCH] feedback revisions --- docs/build/html/process.html | 14 ++- .../msstore_interpreter_remote_python.py | 111 ++++++++++++++++++ tests/test_process.py | 19 ++- windows/com.py | 8 +- windows/injection.py | 7 -- windows/winobject/process.py | 10 +- 6 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 samples/process/msstore_interpreter_remote_python.py diff --git a/docs/build/html/process.html b/docs/build/html/process.html index e07cb8b..5b43350 100644 --- a/docs/build/html/process.html +++ b/docs/build/html/process.html @@ -577,15 +577,20 @@
execute_python(pycode)[source]
-

Execute Python code into the remote process.

+

Execute Python code in the remote process.

This function waits for the remote process to end and raises an exception if the remote thread raised one

+
+

Note

+

This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users, +see workaround: https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py

+
execute_python_unsafe(pycode)[source]
-

Execute Python code into the remote process.

+

Execute Python code in the remote process.

Return type:

@@ -596,6 +601,11 @@ raises an exception if the remote thread raised one

+
+

Note

+

This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users, +see workaround: https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py

+
diff --git a/samples/process/msstore_interpreter_remote_python.py b/samples/process/msstore_interpreter_remote_python.py new file mode 100644 index 0000000..184bea6 --- /dev/null +++ b/samples/process/msstore_interpreter_remote_python.py @@ -0,0 +1,111 @@ +# Some python interpreters run in environments with restrictive ACLs (no Users/* execute) on bundled DLLs. +# The Microsoft Store version of python is the prime example of this. +# +# Remote execution of python is still possible by creating a minimal set of the dependencies outside of the restricted directory. +# +# This can be very helpful when operating PFW in environments with restrive GPOs / AppLocker. + + +import ctypes +import glob +import os +import shutil +import tempfile +import time + +import windows +from windows.generated_def.ntstatus import STATUS_THREAD_IS_TERMINATING +from windows.generated_def.windef import CREATE_SUSPENDED +from windows.generated_def.winstructs import PROCESS_INFORMATION, STARTUPINFOW +from windows.injection import RemotePythonError, \ + find_python_dll_to_inject, get_dll_name_from_python_version, inject_python_command, load_dll_in_remote_process, retrieve_last_exception_data + + +CACHE_DIR = os.path.join(tempfile.gettempdir(), 'pfw_dllcache') +INTERPRETER_DIR = os.path.dirname(find_python_dll_to_inject(64)) # Tailor bitness to your needs + + +def mspython_acl_workaround(target, pydll_path): + """ + Works around mspython ACL restrictions on mspython interpreters + by copying the critical DLLs to a TEMP dir and orienting the interpreter + against that TEMP dir. + """ + + if not os.path.exists(CACHE_DIR): + os.mkdir(CACHE_DIR) + + for dll in [os.path.join(INTERPRETER_DIR, 'vcruntime140.dll'), pydll_path]: + cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll)) + try: + # Creates a copy of the DLL without bringing over restrictive ACLs + shutil.copyfile(dll, cache_dll_path) + except: + # If its not writeable good chance these DLLs are just already loaded somewhere + pass + # Preloading python DLL and vcruntime so they don't get loaded from the path tree with restrictive ACLs + load_dll_in_remote_process(target, cache_dll_path) + + for dll in glob.glob(os.path.join(INTERPRETER_DIR, 'dlls', '*')): + cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll)) + try: + # Dynamic lib DLLs with restrictive ACLs copied to unrestricted parent + shutil.copyfile(dll, cache_dll_path) + except: + pass + + +# Adapted from windows\winobject\process.py +def execute_python_code(process, code): + py_dll_name = get_dll_name_from_python_version() + pydll_path = find_python_dll_to_inject(process.bitness) + + mspython_acl_workaround(process, pydll_path) + shellcode, pythoncode = inject_python_command(process, code, py_dll_name) + t = process.create_thread(shellcode, pythoncode) + return t + + +def safe_execute_python(process, code): + t = execute_python_code(process, code) + t.wait() # Wait termination of the thread + if t.exit_code == 0: + return True + if t.exit_code == STATUS_THREAD_IS_TERMINATING or process.is_exit: + raise WindowsError("{0} died during execution of python command".format(process)) + if t.exit_code != 0xffffffff: + raise ValueError("Unknown exit code {0}".format(hex(t.exit_code))) + data = retrieve_last_exception_data(process) + raise RemotePythonError(data) + + +print("Starting target") +proc_info = PROCESS_INFORMATION() +StartupInfo = STARTUPINFOW() +StartupInfo.cb = ctypes.sizeof(StartupInfo) +windows.winproxy.CreateProcessW( + r"C:\Windows\system32\winver.exe", + dwCreationFlags=CREATE_SUSPENDED, + # Point PYTHONHOME to the interpreter dir so non-DLL libs can load + # Point PYTHONPATH to the newly created cache directory so DLL libs are loaded from there + lpEnvironment=('\0'.join('{}={}'.format(e, v) for e, v in os.environ.items()) + \ + '\0PYTHONHOME={}\0PYTHONPATH={}\0\0'.format(INTERPRETER_DIR, CACHE_DIR)).encode(), + lpProcessInformation=ctypes.byref(proc_info), + lpStartupInfo=ctypes.byref(StartupInfo)) +process = windows.winobject.process.WinProcess(pid=proc_info.dwProcessId, handle=proc_info.hProcess) + +print("Executing python code!") +safe_execute_python(process, """ +import windows +windows.utils.create_console() +print('hello from inside the suspended process!', flush=True) +""") + +process.threads[0].resume() + +print("Executing more python code!") +safe_execute_python(process, """ +print('hello from inside the resumed process!', flush=True) +""") + +process.wait() \ No newline at end of file diff --git a/tests/test_process.py b/tests/test_process.py index 2eb3f5d..be6e25a 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -31,9 +31,7 @@ class TestCurrentProcessWithCheckGarbage(object): # Use module filename because this executable can be: # 1. A PyInstaller exe # 2. A Windows App execution alias (Microsoft Store builds) - current_proc_filename = ctypes.create_string_buffer(1000) - windows.winproxy.GetModuleFileNameA(None, current_proc_filename, 1000) - assert os.path.basename(current_proc_filename.value.decode()) in windows.current_process.peb.modules[0].name + assert os.path.basename(windows.current_process.peb.ProcessParameters[0].ImagePathName.str) in windows.current_process.peb.modules[0].name def test_get_current_process_exe(self): exe = windows.current_process.peb.exe @@ -42,15 +40,12 @@ class TestCurrentProcessWithCheckGarbage(object): exe.bitness == exe_by_module.bitness def test_current_process_pe_imports(self): - python_module = windows.current_process.peb.modules[0] - imp = python_module.pe.imports - python_dll_regex = re.compile(r'python[0-9.]+dll', re.IGNORECASE) - python_dll_imp = next((i for i in imp.keys() if python_dll_regex.match(i)), None) - assert python_dll_imp is not None, 'Python dll not in python imports' - - imp_id_iat = imp[python_dll_imp][0] - mod_base = windows.winproxy.LoadLibraryA(python_dll_imp.encode()) - assert windows.winproxy.GetProcAddress(mod_base, imp_id_iat.name.encode()) == imp_id_iat.value + k32_mod = windows.current_process.peb.modules[2] + imp = k32_mod.pe.imports + assert "ntdll.dll" in imp.keys(), 'ntdll.dll not in python imports' + fn_id_iat = [f for f in imp["ntdll.dll"] if f.name == "NtCreateFile"][0] + ntdll_base = windows.winproxy.LoadLibraryA(b"ntdll.dll") + assert windows.winproxy.GetProcAddress(ntdll_base, b"NtCreateFile") == fn_id_iat.value def test_current_process_pe_exports(self): mods = [m for m in windows.current_process.peb.modules if m.name == "kernel32.dll"] diff --git a/windows/com.py b/windows/com.py index 418f485..d83a141 100644 --- a/windows/com.py +++ b/windows/com.py @@ -33,7 +33,13 @@ def init(): return initsecurity() def initsecurity(): # Should take some parameters.. - return winproxy.CoInitializeSecurity(0, -1, None, 0, 0, RPC_C_IMP_LEVEL_IMPERSONATE, 0,0,0) + try: + winproxy.CoInitializeSecurity(0, -1, None, 0, 0, RPC_C_IMP_LEVEL_IMPERSONATE, 0,0,0) + except OSError as e: + if e.winerror & 0xFFFFFFFF != gdef.RPC_E_TOO_LATE: + # RPC_E_TOO_LATE can happen when the python environment invokes CoInitializeSecurity before we get to it + # mspython builds do this consistently. + raise e class Dispatch(interfaces.IDispatch): diff --git a/windows/injection.py b/windows/injection.py index 0546e78..0530a50 100644 --- a/windows/injection.py +++ b/windows/injection.py @@ -389,13 +389,6 @@ def execute_python_code(process, code): # Cache the value ? py_dll_name = get_dll_name_from_python_version() pydll_path = find_python_dll_to_inject(process.bitness) - - # NB: Sandboxing on Windows Store apps prevents remote loading DLLs stored under "C:\Program Files*\WindowsApps\*" - # This is relevant for remote python execution, as the sandboxed python process is the _only_ one that can - # access its CRT and shared libraries. - if '\\windowsapps\\' in pydll_path.lower(): - raise ValueError('Cannot execute remote python code from a sandboxed python interpreter. Install python outside of the Microsoft Store to resolve.') - if sys.version_info.major == 3: # FOr py3, we may have a per-user install. # Meaning that the vcruntime140.dll will not be in the injected process path diff --git a/windows/winobject/process.py b/windows/winobject/process.py index 92d8121..c74a750 100644 --- a/windows/winobject/process.py +++ b/windows/winobject/process.py @@ -1127,7 +1127,10 @@ class WinProcess(Process): return [m for m in self.peb.modules if m.baseaddr == dllbase][0] def execute_python(self, pycode): - """Execute Python code into the remote process. + """Execute Python code in the remote process. + + This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users/*. + See: samples/process/msstore_interpreter_remote_python.py for a workaround. This function waits for the remote process to end and raises an exception if the remote thread raised one @@ -1135,7 +1138,10 @@ class WinProcess(Process): return injection.safe_execute_python(self, pycode) def execute_python_unsafe(self, pycode): - """Execute Python code into the remote process. + """Execute Python code in the remote process. + + This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users/*. + See: samples/process/msstore_interpreter_remote_python.py for a workaround. :rtype: :rtype: :class:`WinThread` or :class:`DeadThread` : The thread executing the python code """