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
+
-
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
+
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
"""