Skip to content

Commit 52859d1

Browse files
committed
feedback revisions
1 parent 6715b08 commit 52859d1

6 files changed

Lines changed: 145 additions & 24 deletions

File tree

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">
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
16+
import windows
17+
from windows.generated_def.ntstatus import STATUS_THREAD_IS_TERMINATING
18+
from windows.generated_def.windef import CREATE_SUSPENDED
19+
from windows.generated_def.winstructs import PROCESS_INFORMATION, STARTUPINFOW
20+
from windows.injection import RemotePythonError, \
21+
find_python_dll_to_inject, get_dll_name_from_python_version, inject_python_command, load_dll_in_remote_process, retrieve_last_exception_data
22+
23+
24+
CACHE_DIR = os.path.join(tempfile.gettempdir(), 'pfw_dllcache')
25+
INTERPRETER_DIR = os.path.dirname(find_python_dll_to_inject(64)) # Tailor bitness to your needs
26+
27+
28+
def mspython_acl_workaround(target, pydll_path):
29+
"""
30+
Works around mspython ACL restrictions on mspython interpreters
31+
by copying the critical DLLs to a TEMP dir and orienting the interpreter
32+
against that TEMP dir.
33+
"""
34+
35+
if not os.path.exists(CACHE_DIR):
36+
os.mkdir(CACHE_DIR)
37+
38+
for dll in [os.path.join(INTERPRETER_DIR, 'vcruntime140.dll'), pydll_path]:
39+
cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll))
40+
try:
41+
# Creates a copy of the DLL without bringing over restrictive ACLs
42+
shutil.copyfile(dll, cache_dll_path)
43+
except:
44+
# If its not writeable good chance these DLLs are just already loaded somewhere
45+
pass
46+
# Preloading python DLL and vcruntime so they don't get loaded from the path tree with restrictive ACLs
47+
load_dll_in_remote_process(target, cache_dll_path)
48+
49+
for dll in glob.glob(os.path.join(INTERPRETER_DIR, 'dlls', '*')):
50+
cache_dll_path = os.path.join(CACHE_DIR, os.path.basename(dll))
51+
try:
52+
# Dynamic lib DLLs with restrictive ACLs copied to unrestricted parent
53+
shutil.copyfile(dll, cache_dll_path)
54+
except:
55+
pass
56+
57+
58+
# Adapted from windows\winobject\process.py
59+
def execute_python_code(process, code):
60+
py_dll_name = get_dll_name_from_python_version()
61+
pydll_path = find_python_dll_to_inject(process.bitness)
62+
63+
mspython_acl_workaround(process, pydll_path)
64+
shellcode, pythoncode = inject_python_command(process, code, py_dll_name)
65+
t = process.create_thread(shellcode, pythoncode)
66+
return t
67+
68+
69+
def safe_execute_python(process, code):
70+
t = execute_python_code(process, code)
71+
t.wait() # Wait termination of the thread
72+
if t.exit_code == 0:
73+
return True
74+
if t.exit_code == STATUS_THREAD_IS_TERMINATING or process.is_exit:
75+
raise WindowsError("{0} died during execution of python command".format(process))
76+
if t.exit_code != 0xffffffff:
77+
raise ValueError("Unknown exit code {0}".format(hex(t.exit_code)))
78+
data = retrieve_last_exception_data(process)
79+
raise RemotePythonError(data)
80+
81+
82+
print("Starting target")
83+
proc_info = PROCESS_INFORMATION()
84+
StartupInfo = STARTUPINFOW()
85+
StartupInfo.cb = ctypes.sizeof(StartupInfo)
86+
windows.winproxy.CreateProcessW(
87+
r"C:\Windows\system32\winver.exe",
88+
dwCreationFlags=CREATE_SUSPENDED,
89+
# Point PYTHONHOME to the interpreter dir so non-DLL libs can load
90+
# Point PYTHONPATH to the newly created cache directory so DLL libs are loaded from there
91+
lpEnvironment=('\0'.join('{}={}'.format(e, v) for e, v in os.environ.items()) + \
92+
'\0PYTHONHOME={}\0PYTHONPATH={}\0\0'.format(INTERPRETER_DIR, CACHE_DIR)).encode(),
93+
lpProcessInformation=ctypes.byref(proc_info),
94+
lpStartupInfo=ctypes.byref(StartupInfo))
95+
process = windows.winobject.process.WinProcess(pid=proc_info.dwProcessId, handle=proc_info.hProcess)
96+
97+
print("Executing python code!")
98+
safe_execute_python(process, """
99+
import windows
100+
windows.utils.create_console()
101+
print('hello from inside the suspended process!', flush=True)
102+
""")
103+
104+
process.threads[0].resume()
105+
106+
print("Executing more python code!")
107+
safe_execute_python(process, """
108+
print('hello from inside the resumed process!', flush=True)
109+
""")
110+
111+
process.wait()

tests/test_process.py

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ def test_get_current_process_modules(self):
3131
# Use module filename because this executable can be:
3232
# 1. A PyInstaller exe
3333
# 2. A Windows App execution alias (Microsoft Store builds)
34-
current_proc_filename = ctypes.create_string_buffer(1000)
35-
windows.winproxy.GetModuleFileNameA(None, current_proc_filename, 1000)
36-
assert os.path.basename(current_proc_filename.value.decode()) in windows.current_process.peb.modules[0].name
34+
assert os.path.basename(windows.current_process.peb.ProcessParameters[0].ImagePathName.str) in windows.current_process.peb.modules[0].name
3735

3836
def test_get_current_process_exe(self):
3937
exe = windows.current_process.peb.exe
@@ -42,15 +40,12 @@ def test_get_current_process_exe(self):
4240
exe.bitness == exe_by_module.bitness
4341

4442
def test_current_process_pe_imports(self):
45-
python_module = windows.current_process.peb.modules[0]
46-
imp = python_module.pe.imports
47-
python_dll_regex = re.compile(r'python[0-9.]+dll', re.IGNORECASE)
48-
python_dll_imp = next((i for i in imp.keys() if python_dll_regex.match(i)), None)
49-
assert python_dll_imp is not None, 'Python dll not in python imports'
50-
51-
imp_id_iat = imp[python_dll_imp][0]
52-
mod_base = windows.winproxy.LoadLibraryA(python_dll_imp.encode())
53-
assert windows.winproxy.GetProcAddress(mod_base, imp_id_iat.name.encode()) == imp_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
5449

5550
def test_current_process_pe_exports(self):
5651
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/injection.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,6 @@ def execute_python_code(process, code):
389389
# Cache the value ?
390390
py_dll_name = get_dll_name_from_python_version()
391391
pydll_path = find_python_dll_to_inject(process.bitness)
392-
393-
# NB: Sandboxing on Windows Store apps prevents remote loading DLLs stored under "C:\Program Files*\WindowsApps\*"
394-
# This is relevant for remote python execution, as the sandboxed python process is the _only_ one that can
395-
# access its CRT and shared libraries.
396-
if '\\windowsapps\\' in pydll_path.lower():
397-
raise ValueError('Cannot execute remote python code from a sandboxed python interpreter. Install python outside of the Microsoft Store to resolve.')
398-
399392
if sys.version_info.major == 3:
400393
# FOr py3, we may have a per-user install.
401394
# Meaning that the vcruntime140.dll will not be in the injected process path

windows/winobject/process.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,15 +1127,21 @@ 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.
1131+
1132+
This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users/*.
1133+
See: samples/process/msstore_interpreter_remote_python.py for a workaround.
11311134
11321135
This function waits for the remote process to end and
11331136
raises an exception if the remote thread raised one
11341137
"""
11351138
return injection.safe_execute_python(self, pycode)
11361139

11371140
def execute_python_unsafe(self, pycode):
1138-
"""Execute Python code into the remote process.
1141+
"""Execute Python code in the remote process.
1142+
1143+
This method is incompatible with Microsoft Store builds of python, as the interpreter DLLs do not grant execute to Users/*.
1144+
See: samples/process/msstore_interpreter_remote_python.py for a workaround.
11391145
11401146
:rtype: :rtype: :class:`WinThread` or :class:`DeadThread` : The thread executing the python code
11411147
"""

0 commit comments

Comments
 (0)