diff --git a/tests/test_syswow.py b/tests/test_syswow.py index 32ff65d..7f87961 100644 --- a/tests/test_syswow.py +++ b/tests/test_syswow.py @@ -69,3 +69,47 @@ class TestSyswowRemoteProcess(object): assert RETURN_VALUE == proc32.read_qword(addr) +import threading +import windows.test + +threads_error = {} + +def loop_query_ppid(proc, target_ppid): + assert proc.bitness == 64 + + try: + for i in range(10): + del proc._ppid # Force requery via syswow API + assert proc.ppid == target_ppid + for i in [x for x in proc.memory_state() if x.Protect == gdef.PAGE_EXECUTE_READ][:10]: + assert proc.read_memory(i.BaseAddress, 0x1000) + # assert False, "LOL" + except Exception as e: + # import traceback; traceback.print( + threads_error[windows.current_thread.tid] = e + raise + return True + +@process_syswow_only +def test_syswow_call_multithread(): + all_threads = [] + all_procs = [] + + # Create multiple thread that will trigger concurrent call to NtQueryInformationProcess_32_to_64 + # Old version of PFW did not handled that thus generating invalid result / crash + for tnb in range(10): + new_proc = windows.test.pop_proc_64() + new_proc_pid = new_proc.ppid + all_procs.append(new_proc) + t = threading.Thread(target=loop_query_ppid, args=(new_proc, new_proc_pid)) + all_threads.append(t) + + # import pdb; pdb.set_trace() + for t in all_threads: + t.start() + for t in all_threads: + t.join() + for p in all_procs: + p.exit() + + assert not threads_error, "syswow call inconsistent with MultiThreading inconsistent" diff --git a/windows/syswow64.py b/windows/syswow64.py index d685204..28b1b0a 100644 --- a/windows/syswow64.py +++ b/windows/syswow64.py @@ -3,6 +3,7 @@ import ctypes from ctypes import byref import codecs import functools +import threading import windows import windows.native_exec.simple_x86 as x86 @@ -17,6 +18,15 @@ from .pycompat import int_types CS_32bits = 0x23 CS_64bits = 0x33 +# Allow to keep per-thread state of asm stub +class ThreadState(threading.local): + def __init__(self): # Called once per thread + self.allocator = windows.native_exec.native_function.CustomAllocator() + self.raw_call_per_function = {} + self.current_original_args = None + +thread_state = ThreadState() + def generate_64bits_execution_stub_from_syswow(x64shellcode): """shellcode must NOT end by a ret""" @@ -30,7 +40,7 @@ def generate_64bits_execution_stub_from_syswow(x64shellcode): transition64 += x64.Shr("RDX", 32) transition64 += x64.Retf32() # 32 bits return addr transition64 += x64.Label(":TOEXEC") - x64shellcodeaddr = windows.current_process.allocator.write_code(transition64.get_code() + x64shellcode) + x64shellcodeaddr = thread_state.allocator.write_code(transition64.get_code() + x64shellcode) transition = x86.MultipleInstr() transition += x86.Call(CS_64bits, x64shellcodeaddr) @@ -42,7 +52,7 @@ def generate_64bits_execution_stub_from_syswow(x64shellcode): transition += x86.Mov("SS", "ECX") transition += x86.Ret() - stubaddr = windows.current_process.allocator.write_code(transition.get_code()) + stubaddr = thread_state.allocator.write_code(transition.get_code()) exec_stub = ctypes.CFUNCTYPE(ULONG64)(stubaddr) return exec_stub @@ -53,8 +63,8 @@ def generate_syswow64_call(target, errcheck=None): nb_args = len(target.prototype._argtypes_) target_addr = get_syswow_ntdll_exports()[target.__name__] argument_buffer_len = (nb_args * 8) - argument_buffer = windows.current_process.allocator.reserve_size(argument_buffer_len) - alignement_information = windows.current_process.allocator.reserve_size(8) + argument_buffer = thread_state.allocator.reserve_size(argument_buffer_len) + alignement_information = thread_state.allocator.reserve_size(8) nb_args_on_stack = max(nb_args - 4, 0) @@ -140,7 +150,8 @@ def try_generate_stub_target(shellcode, argument_buffer, target, errcheck=None): buffer = struct.pack("<" + "Q" * len(writable_args), *writable_args) ctypes.memmove(argument_buffer, buffer, len(buffer)) # Copy origincal args in function, for errcheck if needed - native_caller.current_original_args = args # TODO: THIS IS NOT THREAD SAFE + thread_state.current_original_args = args + return native_caller() wrapper.__name__ = "{0}".format(target.__name__,) wrapper.__doc__ = "This is a wrapper to {0} in 64b mode, it accept <{1}> args".format(target.__name__, expected_arguments_number) @@ -201,23 +212,20 @@ class Syswow64ApiProxy(object): """Create a python wrapper around a function""" def __init__(self, winproxy_function, errcheck=None): self.winproxy_function = winproxy_function - self.raw_call = None self.errcheck = errcheck if winproxy_function is not None: self.params_name = [param[1] for param in winproxy_function.params] - - - def __call__(self, python_proxy): if not windows.winproxy.is_implemented(self.winproxy_function): return None def force_resolution(): - if self.raw_call: + if self.winproxy_function in thread_state.raw_call_per_function: return True try: - self.raw_call = generate_syswow64_call(self.winproxy_function, errcheck=self.errcheck) + stub = generate_syswow64_call(self.winproxy_function, errcheck=self.errcheck) + thread_state.raw_call_per_function[self.winproxy_function] = stub except KeyError: raise windows.winproxy.ExportNotFound(self.winproxy_function.__name__, "SysWow[ntdll64]") @@ -232,15 +240,17 @@ class Syswow64ApiProxy(object): if param_value is NeededParameter: raise TypeError("{0}: Missing Mandatory parameter <{1}>".format(self.winproxy_function.__name__, param_name)) - if self.raw_call is None: + if self.winproxy_function not in thread_state.raw_call_per_function: force_resolution() - return self.raw_call(*args) + return thread_state.raw_call_per_function[self.winproxy_function](*args) + + setattr(python_proxy, "ctypes_function", perform_call) setattr(python_proxy, "force_resolution", force_resolution) return python_proxy def ntquerysysteminformation_syswow64_error_check(result, func, args): - args = func.current_original_args + args = thread_state.current_original_args if result == 0: return args # Ignore STATUS_INFO_LENGTH_MISMATCH if SystemInformation is None