Skip to content

Commit 5d7e074

Browse files
authored
Merge pull request #73 from dariushoule/windows_store_interpreter_tweaks
Improved behavior when running under Microsoft Store python interpreters
2 parents 929959d + 92b3cfc commit 5d7e074

File tree

7 files changed

+220
-15
lines changed

7 files changed

+220
-15
lines changed

docs/build/html/process.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,15 +577,20 @@ <h2><span class="section-number">2.1.3. </span>WinProcess<a class="headerlink" h
577577
<dl class="py method">
578578
<dt class="sig sig-object py" id="windows.winobject.process.WinProcess.execute_python">
579579
<span class="sig-name descname"><span class="pre">execute_python</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">pycode</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/windows/winobject/process.html#WinProcess.execute_python"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#windows.winobject.process.WinProcess.execute_python" title="Link to this definition"></a></dt>
580-
<dd><p>Execute Python code into the remote process.</p>
580+
<dd><p>Execute Python code in the remote process.</p>
581581
<p>This function waits for the remote process to end and
582582
raises an exception if the remote thread raised one</p>
583+
<div class="admonition note">
584+
<p class="admonition-title">Note</p>
585+
<p>This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users.
586+
See workaround: <a class="reference external" href="https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py">https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py</a></p>
587+
</div>
583588
</dd></dl>
584589

585590
<dl class="py method">
586591
<dt class="sig sig-object py" id="windows.winobject.process.WinProcess.execute_python_unsafe">
587592
<span class="sig-name descname"><span class="pre">execute_python_unsafe</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">pycode</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/windows/winobject/process.html#WinProcess.execute_python_unsafe"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#windows.winobject.process.WinProcess.execute_python_unsafe" title="Link to this definition"></a></dt>
588-
<dd><p>Execute Python code into the remote process.</p>
593+
<dd><p>Execute Python code in the remote process.</p>
589594
<dl class="field-list simple">
590595
<dt class="field-odd">Return type<span class="colon">:</span></dt>
591596
<dd class="field-odd"><p><dl class="field-list simple">
@@ -596,6 +601,11 @@ <h2><span class="section-number">2.1.3. </span>WinProcess<a class="headerlink" h
596601
</p>
597602
</dd>
598603
</dl>
604+
<div class="admonition note">
605+
<p class="admonition-title">Note</p>
606+
<p>This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users.
607+
See workaround: <a class="reference external" href="https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py">https://github.com/hakril/PythonForWindows/tree/master/samples/process/msstore_interpreter_remote_python.py</a></p>
608+
</div>
599609
</dd></dl>
600610

601611
<dl class="py method">

docs/source/sample.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ Output
6969

7070
.. _token_sample:
7171

72+
73+
Microsoft Store Python Injection
74+
''''''''''''''''''''''''''''''''
75+
76+
Python execution in remote process fails with Microsoft Store builds of pythons (`mspython`), as the interpreter DLLs do not grant execute to Users.
77+
This sample shows a workaround by user https://github.com/dariushoule by copying needed mspython files to a temporary directory and injecting those instead.
78+
79+
.. literalinclude:: ..\..\samples\process\msstore_interpreter_remote_python.py
80+
81+
Output
82+
83+
.. literalinclude:: samples_output\process_msstore_interpreter_remote_python.txt
84+
85+
7286
Token
7387
"""""
7488

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
PS C:\Users\hakril\PythonForWindows> py .\samples\process\msstore_interpreter_remote_python.py
2+
Executable is: C:\Users\hakril\AppData\Local\Microsoft\WindowsApps\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\python.exe
3+
Trying normal execute_python()
4+
Exception during proc1.execute_python():
5+
InjectionFailedError('Injection of <c:\\program files\\windowsapps\\pythonsoftwarefoundation.python.3.13_3.13.496.0_x64__qbz5n2kfra8p0\\vcruntime140.dll> failed')
6+
Trying mspython workaround:
7+
Executing python code!
8+
Injecting: C:\Users\hakril\AppData\Local\Temp\pfw_dllcache\vcruntime140.dll
9+
Injecting: C:\Users\hakril\AppData\Local\Temp\pfw_dllcache\python313.dll
10+
Executing more python code!
11+
Executing an error python code!
12+
Expected error during safe_execute_python
13+
b'Traceback (most recent call last):\n File "<string>", line 1, in <module>\nNameError: name \'BAD_VARIABLE\' is not defined\n'
14+
Sleeping a little
15+
Killing target process !
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# Some python interpreters run in environments with restrictive ACLs (no Users/* execute) on bundled DLLs.
2+
# The Microsoft Store version of python is the prime example of this.
3+
#
4+
# Remote execution of python is still possible by creating a minimal set of the dependencies outside of the restricted directory.
5+
#
6+
# This can be very helpful when operating PFW in environments with restrive GPOs / AppLocker.
7+
8+
9+
import ctypes
10+
import glob
11+
import os
12+
import shutil
13+
import tempfile
14+
import time
15+
import sys
16+
import struct
17+
18+
import windows
19+
from windows.generated_def.ntstatus import STATUS_THREAD_IS_TERMINATING
20+
from windows.generated_def.windef import CREATE_SUSPENDED
21+
from windows.generated_def.winstructs import PROCESS_INFORMATION, STARTUPINFOW
22+
from windows.injection import RemotePythonError, \
23+
find_python_dll_to_inject, get_dll_name_from_python_version, inject_python_command, load_dll_in_remote_process, retrieve_exc
24+
25+
26+
print("Executable is: {0}".format(sys.executable))
27+
28+
CACHE_DIR = os.path.join(tempfile.gettempdir(), 'pfw_dllcache')
29+
INTERPRETER_DIR = os.path.dirname(find_python_dll_to_inject(64)) # Tailor bitness to your needs
30+
31+
32+
def mspython_acl_workaround(target, pydll_path):
33+
"""
34+
Works around mspython ACL restrictions on mspython interpreters
35+
by copying the critical DLLs to a TEMP dir and orienting the interpreter
36+
against that TEMP dir.
37+
"""
38+
39+
if not os.path.exists(CACHE_DIR):
40+
os.mkdir(CACHE_DIR)
41+
42+
for dll in [os.path.join(INTERPRETER_DIR, 'vcruntime140.dll'), pydll_path]:
43+
cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll))
44+
try:
45+
# Creates a copy of the DLL without bringing over restrictive ACLs
46+
shutil.copyfile(dll, cache_dll_path)
47+
except Exception as e:
48+
# If its not writeable good chance these DLLs are just already loaded somewhere
49+
print(e)
50+
51+
# Preloading python DLL and vcruntime so they don't get loaded from the path tree with restrictive ACLs
52+
print("Injecting: {0}".format(cache_dll_path))
53+
load_dll_in_remote_process(target, cache_dll_path)
54+
55+
for dll in glob.glob(os.path.join(INTERPRETER_DIR, 'dlls', '*')):
56+
cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll))
57+
try:
58+
# Dynamic lib DLLs with restrictive ACLs copied to unrestricted parent
59+
shutil.copyfile(dll, cache_dll_path)
60+
except Exception as e:
61+
print(e)
62+
63+
target._workaround_applied = True
64+
65+
66+
# Adapted from windows\winobject\process.py
67+
def execute_python_code(process, code):
68+
py_dll_name = get_dll_name_from_python_version()
69+
pydll_path = find_python_dll_to_inject(process.bitness)
70+
71+
if not getattr(process, "_workaround_applied", None):
72+
mspython_acl_workaround(process, pydll_path)
73+
shellcode, pythoncode = inject_python_command(process, code, py_dll_name)
74+
t = process.create_thread(shellcode, pythoncode)
75+
return t
76+
77+
78+
def safe_execute_python(process, code):
79+
t = execute_python_code(process, code)
80+
t.wait() # Wait termination of the thread
81+
if t.exit_code == 0:
82+
return True
83+
if t.exit_code == STATUS_THREAD_IS_TERMINATING or process.is_exit:
84+
raise WindowsError("{0} died during execution of python command".format(process))
85+
if t.exit_code != 0xffffffff:
86+
raise ValueError("Unknown exit code {0}".format(hex(t.exit_code)))
87+
data = retrieve_last_exception_data(process)
88+
raise RemotePythonError(data)
89+
90+
# Adapted from windows\injection.py
91+
def retrieve_last_exception_data(process):
92+
with process.allocated_memory(0x1000) as mem:
93+
execute_python_code(process, retrieve_exc.format(mem)).wait()
94+
size = struct.unpack("<I", process.read_memory(mem, ctypes.sizeof(ctypes.c_uint)))[0]
95+
data = process.read_memory(mem + ctypes.sizeof(ctypes.c_uint), size)
96+
return data
97+
98+
# First: show what happen when injecting mspython normally
99+
print("Trying normal execute_python()")
100+
proc1 = windows.utils.create_process(r"C:\Windows\system32\winver.exe")
101+
try:
102+
proc1.execute_python("2 + 2 == 5")
103+
except Exception as e:
104+
print(" Exception during proc1.execute_python():")
105+
print(" {0}".format(repr(e)))
106+
proc1.exit()
107+
108+
print("Trying mspython workaround:")
109+
proc_info = PROCESS_INFORMATION()
110+
StartupInfo = STARTUPINFOW()
111+
StartupInfo.cb = ctypes.sizeof(StartupInfo)
112+
windows.winproxy.CreateProcessW(
113+
r"C:\Windows\system32\winver.exe",
114+
dwCreationFlags=CREATE_SUSPENDED,
115+
# Point PYTHONHOME to the interpreter dir so non-DLL libs can load
116+
# Point PYTHONPATH to the newly created cache directory so DLL libs are loaded from there
117+
lpEnvironment=('\0'.join('{}={}'.format(e, v) for e, v in os.environ.items()) + \
118+
'\0PYTHONHOME={}\0PYTHONPATH={}\0\0'.format(INTERPRETER_DIR, CACHE_DIR)).encode(),
119+
lpProcessInformation=ctypes.byref(proc_info),
120+
lpStartupInfo=ctypes.byref(StartupInfo))
121+
122+
process = windows.winobject.process.WinProcess(pid=proc_info.dwProcessId, handle=proc_info.hProcess)
123+
124+
print(" Executing python code!")
125+
safe_execute_python(process, """
126+
import windows
127+
windows.utils.create_console()
128+
print('hello from inside the suspended process!', flush=True)
129+
""")
130+
131+
process.threads[0].resume()
132+
133+
print(" Executing more python code!")
134+
safe_execute_python(process, """
135+
print('hello from inside the resumed process!', flush=True)
136+
""")
137+
138+
print(" Executing an error python code!")
139+
try:
140+
safe_execute_python(process, """BAD_VARIABLE""")
141+
except RemotePythonError as e:
142+
print(" Expected error during safe_execute_python")
143+
print(" {0}".format(e))
144+
145+
print(" Sleeping a little")
146+
time.sleep(5)
147+
print(" Killing target process !")
148+
process.exit()

tests/test_process.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
import os
55
import sys
66
import time
7-
import struct
7+
import ctypes
8+
89
import textwrap
910
import shutil
11+
import re
1012

1113
import windows
1214
import windows.pipe
@@ -26,8 +28,10 @@ def test_get_current_process_peb(self):
2628
return windows.current_process.peb
2729

2830
def test_get_current_process_modules(self):
29-
# Use sys.executable because executable can be a PyInstaller exe
30-
assert os.path.basename(sys.executable) in windows.current_process.peb.modules[0].name
31+
# Use module filename because this executable can be:
32+
# 1. A PyInstaller exe
33+
# 2. A Windows App execution alias (Microsoft Store builds)
34+
assert os.path.basename(windows.current_process.peb.ProcessParameters[0].ImagePathName.str) in windows.current_process.peb.modules[0].name
3135

3236
def test_get_current_process_exe(self):
3337
exe = windows.current_process.peb.exe
@@ -36,12 +40,12 @@ def test_get_current_process_exe(self):
3640
exe.bitness == exe_by_module.bitness
3741

3842
def test_current_process_pe_imports(self):
39-
python_module = windows.current_process.peb.modules[0]
40-
imp = python_module.pe.imports
41-
assert "kernel32.dll" in imp.keys(), 'Kernel32.dll not in python imports'
42-
current_proc_id_iat = [f for f in imp["kernel32.dll"] if f.name == "GetCurrentProcessId"][0]
43-
k32_base = windows.winproxy.LoadLibraryA(b"kernel32.dll")
44-
assert windows.winproxy.GetProcAddress(k32_base, b"GetCurrentProcessId") == current_proc_id_iat.value
43+
k32_mod = windows.current_process.peb.modules[2]
44+
imp = k32_mod.pe.imports
45+
assert "ntdll.dll" in imp.keys(), 'ntdll.dll not in python imports'
46+
fn_id_iat = [f for f in imp["ntdll.dll"] if f.name == "NtCreateFile"][0]
47+
ntdll_base = windows.winproxy.LoadLibraryA(b"ntdll.dll")
48+
assert windows.winproxy.GetProcAddress(ntdll_base, b"NtCreateFile") == fn_id_iat.value
4549

4650
def test_current_process_pe_exports(self):
4751
mods = [m for m in windows.current_process.peb.modules if m.name == "kernel32.dll"]

windows/com.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ def init():
3333
return initsecurity()
3434

3535
def initsecurity(): # Should take some parameters..
36-
return winproxy.CoInitializeSecurity(0, -1, None, 0, 0, RPC_C_IMP_LEVEL_IMPERSONATE, 0,0,0)
36+
try:
37+
winproxy.CoInitializeSecurity(0, -1, None, 0, 0, RPC_C_IMP_LEVEL_IMPERSONATE, 0,0,0)
38+
except OSError as e:
39+
if e.winerror & 0xFFFFFFFF != gdef.RPC_E_TOO_LATE:
40+
# RPC_E_TOO_LATE can happen when the python environment invokes CoInitializeSecurity before we get to it
41+
# mspython builds do this consistently.
42+
raise e
3743

3844

3945
class Dispatch(interfaces.IDispatch):

windows/winobject/process.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,17 +1127,25 @@ def load_library(self, dll_path):
11271127
return [m for m in self.peb.modules if m.baseaddr == dllbase][0]
11281128

11291129
def execute_python(self, pycode):
1130-
"""Execute Python code into the remote process.
1130+
"""Execute Python code in the remote process.
11311131
11321132
This function waits for the remote process to end and
11331133
raises an exception if the remote thread raised one
1134-
"""
1134+
1135+
.. note::
1136+
This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users.
1137+
See workaround: https://hakril.github.io/PythonForWindows/build/html/sample.html#microsoft-store-python-injection
1138+
"""
11351139
return injection.safe_execute_python(self, pycode)
11361140

11371141
def execute_python_unsafe(self, pycode):
1138-
"""Execute Python code into the remote process.
1142+
"""Execute Python code in the remote process.
11391143
11401144
:rtype: :rtype: :class:`WinThread` or :class:`DeadThread` : The thread executing the python code
1145+
1146+
.. note::
1147+
This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users.
1148+
See workaround: https://hakril.github.io/PythonForWindows/build/html/sample.html#microsoft-store-python-injection
11411149
"""
11421150
return injection.execute_python_code(self, pycode)
11431151

0 commit comments

Comments
 (0)