From 34c2d94bc266b12a1cd14726ea72530ab3c4023c Mon Sep 17 00:00:00 2001 From: hakril Date: Tue, 27 Mar 2018 20:37:30 +0200 Subject: [PATCH] Some fixes in code/samples/test --- ctypes_generation/generate.py | 3 +- samples/alpc/advanced_alpc.py | 2 + samples/com/icallinterceptor.py | 3 -- samples/process/iat_hook.py | 3 ++ tests/conftest.py | 2 + tests/test_alpc.py | 1 + tests/test_apisetmap.py | 14 +++--- tests/test_process.py | 15 +++++- windows/injection.py | 63 ++++++++++++++++++++---- windows/native_exec/simple_x64.py | 3 -- windows/utils/pythonutils.py | 62 +++++++++++++++++++++++- windows/winobject/apisetmap.py | 2 + windows/winobject/process.py | 80 +++++++++++++------------------ 13 files changed, 180 insertions(+), 73 deletions(-) diff --git a/ctypes_generation/generate.py b/ctypes_generation/generate.py index 6f8c1e8..e80782d 100644 --- a/ctypes_generation/generate.py +++ b/ctypes_generation/generate.py @@ -247,6 +247,7 @@ class CtypesGenerator(object): def generate_into(self, filename): self.generate() + print("Writing generated code into {0}".format(filename)) with open(filename, "w") as f: f.write(self.result.getvalue()) @@ -592,7 +593,7 @@ class ModuleGenerator(object): self.after_ctypes_generator_init(ctypesgen) finalfilename = "{0}.py".format(self.name) - ctypesgen.generate_into(to_dest(finalfilename)) + ctypesgen.generate_into(to_dest(finalfilename)) # Need to handle dest != PythonForWindows def generate_doc(self, filename): nodelist = self.nodes diff --git a/samples/alpc/advanced_alpc.py b/samples/alpc/advanced_alpc.py index 00c4e86..612ac4c 100644 --- a/samples/alpc/advanced_alpc.py +++ b/samples/alpc/advanced_alpc.py @@ -40,6 +40,8 @@ def full_alpc_server(): windows.utils.print_ctypes_struct(msg.view_attribute, " - VIEW", hexa=True) view_data = windows.current_process.read_string(msg.view_attribute.ViewBase) print(" * Reading view content: <{0}>".format(view_data)) + # Needed in Win7 - TODO: why is there a different behavior ? + msg.attributes.ValidAttributes -= gdef.ALPC_MESSAGE_VIEW_ATTRIBUTE print(" * security_is_valid <{0}>".format(msg.security_is_valid)) print(" * handle_is_valid <{0}>".format(msg.handle_is_valid)) if msg.handle_is_valid: diff --git a/samples/com/icallinterceptor.py b/samples/com/icallinterceptor.py index ec56045..db4ad84 100644 --- a/samples/com/icallinterceptor.py +++ b/samples/com/icallinterceptor.py @@ -21,9 +21,6 @@ class MySink(windows.com.COMImplementation): IMPLEMENT = gdef.ICallFrameEvents def OnCall(self, this, frame): - import pdb;pdb.set_trace() - # this = gdef.ICallFrameEvents(this) # TODO: auto-translate this ? - # frame = gdef.ICallFrame(frame) ifname = gdef.PWSTR() methodname = gdef.PWSTR() print("Hello from python sink !") diff --git a/samples/process/iat_hook.py b/samples/process/iat_hook.py index 1074cf6..7cd8bea 100644 --- a/samples/process/iat_hook.py +++ b/samples/process/iat_hook.py @@ -41,6 +41,9 @@ RegOpenKeyExA_iat = [n for n in adv_imports if n.name == "RegOpenKeyExA"][0] # Setup our hook RegOpenKeyExA_iat.set_hook(open_reg_hook) +### !!!! You must keep the iat_entry alive !!!! +### If the hook is garbage collected while active -> python will crash + # Use python native module _winreg that call 'RegOpenKeyExA' print("Asking for ") v = _winreg.OpenKey(1234567, "MY_SECRET_KEY") diff --git a/tests/conftest.py b/tests/conftest.py index 6d7b28c..eb24420 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import gc +import time import pytest import collections @@ -35,6 +36,7 @@ def generate_pop_and_exit_fixtures(proc_popers, ids=[], dwCreationFlags=DEFAULT_ def pop_and_exit_process(request): proc_poper = request.param proc = proc_poper(dwCreationFlags=dwCreationFlags) + time.sleep(0.1) # Give time to the process to load :) yield weakref.proxy(proc) # provide the fixture value try: proc.exit(0) diff --git a/tests/test_alpc.py b/tests/test_alpc.py index 393e485..5f53bea 100644 --- a/tests/test_alpc.py +++ b/tests/test_alpc.py @@ -71,6 +71,7 @@ def alpc_view_test_server(): assert msg.type & 0xfff == gdef.LPC_REQUEST assert msg.view_is_valid view_data = windows.current_process.read_memory(msg.view_attribute.ViewBase, len(CLIENT_VIEW_DATA)) + msg.attributes.ValidAttributes -= gdef.ALPC_MESSAGE_VIEW_ATTRIBUTE assert view_data == CLIENT_VIEW_DATA assert msg.data == CLIENT_VIEW_MESSAGE msg.data = SERVER_MESSAGE diff --git a/tests/test_apisetmap.py b/tests/test_apisetmap.py index 42adf4f..40608b8 100644 --- a/tests/test_apisetmap.py +++ b/tests/test_apisetmap.py @@ -19,6 +19,8 @@ def dumped_apisetmap_base_and_version(request): ctypes_data = ctypes.c_buffer(data) yield ctypes.addressof(ctypes_data), version +KNOWN_APISETMAP_PREFIX = ["api-", "ext-", "MS-Win-"] + def verify_apisetmap_parsing(apisetmap_base, version=None): if version is not None: assert windows.current_process.read_dword(apisetmap_base) == version @@ -29,12 +31,12 @@ def verify_apisetmap_parsing(apisetmap_base, version=None): # Verify that at least one entry resolve to kernel32.dll # This ensure that the ApiSetMap parsing works at least a little assert "kernel32.dll" in apisetmap_dict.values() - assert all(dll.startswith("api-") or dll.startswith("ext-") for dll in apisetmap_dict) - # This key was found in all current tested version by hand - # Might need to change that if I add another APISET dump - # But as it was in Windows7 to Windows10 there are big chance it will be in others APISET. - # We do as MS code and ignore everything after the last '-' - assert 'api-ms-win-core-com-l2-1-' in apisetmap_dict + assert all(any(dll.startswith(pref) for pref in KNOWN_APISETMAP_PREFIX) for dll in apisetmap_dict) + # This first key was found in most of the tested version by hand + # MS-Win found on: 6.1.7600 (Win7) + assert 'api-ms-win-core-com-l2-1-' in apisetmap_dict or "MS-Win-Core-ProcessThreads-L1-1-" in apisetmap_dict + + def test_apisetmap_parsing_current_process(): return verify_apisetmap_parsing(windows.current_process.peb.ApiSetMap) diff --git a/tests/test_process.py b/tests/test_process.py index cc52e05..ae3ac34 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,6 +1,7 @@ import pytest import os +import sys import time import struct import textwrap @@ -23,7 +24,8 @@ class TestCurrentProcessWithCheckGarbage(object): return windows.current_process.peb def test_get_current_process_modules(self): - assert "python" in windows.current_process.peb.modules[0].name + # Use sys.executable because executable can be a PyInstaller exe + assert os.path.basename(sys.executable) in windows.current_process.peb.modules[0].name def test_get_current_process_exe(self): exe = windows.current_process.peb.exe @@ -151,7 +153,16 @@ class TestProcessWithCheckGarbage(object): # Python execution + def _skip_if_injection_dll_not_found(self, target): + if windows.current_process.bitness == target.bitness: + return # Should never fail if we have the same bitness + try: + windows.injection.validate_python_dll_presence_on_disk(target) + except IOError as e: + pytest.skip("Python DLL to inject not installed") + def test_execute_python(self, proc32_64): + self._skip_if_injection_dll_not_found(proc32_64) with proc32_64.allocated_memory(0x1000) as addr: proc32_64.execute_python('import ctypes; ctypes.c_uint.from_address({0}).value = 0x42424242'.format(addr)) dword = proc32_64.read_dword(addr) @@ -159,6 +170,7 @@ class TestProcessWithCheckGarbage(object): def test_execute_python_suspended(self, proc32_64_suspended): + self._skip_if_injection_dll_not_found(proc32_64_suspended) proc = proc32_64_suspended with proc.allocated_memory(0x1000) as addr: proc.execute_python('import ctypes; ctypes.c_uint.from_address({0}).value = 0x42424242'.format(addr)) @@ -217,6 +229,7 @@ class TestProcessWithCheckGarbage(object): assert exe.bitness == exe_by_module.bitness def test_execute_python_raises(self, proc32_64): + self._skip_if_injection_dll_not_found(proc32_64) res = proc32_64.execute_python("import time;time.sleep(0.1); 2") assert res == True with pytest.raises(windows.injection.RemotePythonError) as ar: diff --git a/windows/injection.py b/windows/injection.py index 105f3f4..06fc828 100644 --- a/windows/injection.py +++ b/windows/injection.py @@ -16,8 +16,17 @@ from windows.dbgprint import dbgprint class InjectionFailedError(WindowsError): pass +def get_kernel32_dll_name(): + # Our injected shellcode search for 'kernel32.dll' with a strcmp + # The BaseDllName of k32 might be 'KERNEL32.DLL' or 'kernel32.dll' on different system32 + # We base the name on our own loaded kernel32 + k32 = [m for m in windows.current_process.peb.modules if m.name == "kernel32.dll"] + assert len(k32) == 1 + k32name = k32[0].BaseDllName.str + return (k32name + "\x00").encode("utf-16-le") + def perform_manual_getproc_loadlib_32(target, dll_name): - dll = "KERNEL32.DLL\x00".encode("utf-16-le") + dll = get_kernel32_dll_name() api = "LoadLibraryA\x00" dll_to_load = dll_name + "\x00" @@ -54,7 +63,7 @@ def perform_manual_getproc_loadlib_32(target, dll_name): return True def perform_manual_getproc_loadlib_64(target, dll_name): - dll = "KERNEL32.DLL\x00".encode("utf-16-le") + dll = get_kernel32_dll_name() api = "LoadLibraryA\x00" dll_to_load = dll_name + "\x00" @@ -94,6 +103,19 @@ def perform_manual_getproc_loadlib_64(target, dll_name): raise InjectionFailedError("Injection of <{0}> failed".format(dll_name)) return True +def generate_simple_LoadLibraryW_64(load_libraryW, remote_store): + code = RemoteLoadLibrayStub = x64.MultipleInstr() + # code += x64.Int3() + code += x64.Mov("RAX", load_libraryW) + code += (x64.Push("RDI") * 5) # Prepare stack + code += x64.Call("RAX") + code += (x64.Pop("RDI") * 5) # Clean stack + code += x64.Mov(x64.deref(remote_store), "RAX") + code += x64.Ret() + return RemoteLoadLibrayStub.get_code() + + + def perform_manual_getproc_loadlib(target, *args, **kwargs): if target.bitness == 32: return perform_manual_getproc_loadlib_32(target, *args, **kwargs) @@ -101,6 +123,8 @@ def perform_manual_getproc_loadlib(target, *args, **kwargs): def load_dll_in_remote_process(target, dll_name): + # if target.bitness == 64: + # import pdb;pdb.set_trace() rpeb = target.peb if rpeb.Ldr: # LDR est parcourable, ca va etre deja plus simple.. @@ -119,14 +143,33 @@ def load_dll_in_remote_process(target, dll_name): raise ValueError("Kernel32 have no export (wtf)") with target.allocated_memory(0x1000) as addr: - target.write_memory(addr, (dll_name + "\x00").encode('utf-16le')) - t = target.create_thread(load_libraryW, addr) - t.wait() - if not t.exit_code: - raise InjectionFailedError(u"Injection of <{0}> failed".format(dll_name)) + if target.bitness == 32: + target.write_memory(addr, (dll_name + "\x00").encode('utf-16le')) + t = target.create_thread(load_libraryW, addr) + t.wait() + module_baseaddr = t.exit_code + else: + # For 64b target we need a special stub as the return value of + # load_libraryW does not fit in t.exit_code (DWORD) + retval_addr = addr + target.write_ptr(retval_addr, 0) + addr += ctypes.sizeof(ctypes.c_ulonglong) + full_dll_name = (dll_name + "\x00").encode('utf-16le') + target.write_memory(addr, full_dll_name) + param_addr = addr + addr += len(full_dll_name) + shellcode_addr = addr + shellcode = generate_simple_LoadLibraryW_64(load_libraryW, retval_addr) + target.write_memory(shellcode_addr, shellcode) + t = target.create_thread(shellcode_addr, param_addr) + t.wait() + module_baseaddr = target.read_ptr(retval_addr) + + if not module_baseaddr: + raise InjectionFailedError(u"Injection of <{0}> failed".format(dll_name)) dbgprint("DLL Injected via LoadLibray", "DLLINJECT") # Cannot return the full return value of load_libraryW in 64b target.. (exit_code is a DWORD) - return t.exit_code + return module_baseaddr # Hardcore mode # We don't have k32 or PEB->Ldr # Go inject a GetProcAddress(LoadLib) + LoadLib shellcode :D @@ -287,11 +330,11 @@ def validate_python_dll_presence_on_disk(process): if windows.current_process.bitness == 32 and process.bitness == 64: with windows.utils.DisableWow64FsRedirection(): if not os.path.exists(r"C:\Windows\system32\python27.dll"): - raise ValueError("Could not find Python DLL to inject") + raise IOError("Could not find Python DLL to inject") return True if windows.current_process.bitness == 64 and process.bitness == 32: if not os.path.exists(r"C:\Windows\SysWOW64\python27.dll"): - raise ValueError("Could not find Python DLL to inject") + raise IOError("Could not find Python DLL to inject") return True raise NotImplementedError("Unknown bitness") diff --git a/windows/native_exec/simple_x64.py b/windows/native_exec/simple_x64.py index 9b02ee4..2c4c464 100644 --- a/windows/native_exec/simple_x64.py +++ b/windows/native_exec/simple_x64.py @@ -1,6 +1,5 @@ import collections import struct -from windows.dbgprint import dbgprint DEBUG = False @@ -740,7 +739,6 @@ class Instruction(object): default_rex = BitArray.from_int(8, 0x40) def __init__(self, *initial_args): - dbgprint("Assembling {0}{1}".format(type(self).__name__, initial_args), "X64") for type_encoding in self.encoding: args = list(initial_args) res = [] @@ -759,7 +757,6 @@ class Instruction(object): else: # if no break if args: # if still args: fail continue - dbgprint("Valid encoding found: REX={0:#x}".format(ord(full_rex.dump())), "X64") self.prefix = prefix self.value = sum(res, BitArray(0, "")) if str(full_rex.dump()) != "\x40": diff --git a/windows/utils/pythonutils.py b/windows/utils/pythonutils.py index 804a639..95fab93 100644 --- a/windows/utils/pythonutils.py +++ b/windows/utils/pythonutils.py @@ -1,7 +1,11 @@ """utils fonctions non windows-related""" +import sys import ctypes import _ctypes -from windows.generated_def import Flag, LPCSTR, LPWSTR +from windows.generated_def import Flag, LPCSTR, LPWSTR, INFINITE + +from windows.dbgprint import dbgprint +from windows import winproxy def buffer(size): # Test @@ -24,6 +28,26 @@ def buffer(size): # Test return ImprovedCtypesBufferImpl() +def wbuffer(size): # Test + buf = ctypes.create_string_buffer(size) + buf.size = size + buf.address = ctypes.addressof(buf) + + class ImprovedCtypesBufferImpl(ctypes.Array): + _length_ = size + _type_ = ctypes.c_wchar + def lol(self): + return "lol" + + def as_string(self): + return ctypes.cast(self, LPCSTR).value + + def as_wstring(self): + return ctypes.cast(self, LPWSTR).value + + return ImprovedCtypesBufferImpl() + + def fixedpropety(f): cache_name = "_" + f.__name__ @@ -75,4 +99,38 @@ def print_ctypes_struct(struct, name="", ident=0, hexa=False): def sprint(struct, name="struct", hexa=True): """Print recursively the content of a :mod:`ctypes` structure""" - return print_ctypes_struct(struct, name=name, hexa=hexa) \ No newline at end of file + return print_ctypes_struct(struct, name=name, hexa=hexa) + + +class AutoHandle(object): + """An abstract class that allow easy handle creation/destruction/wait""" + # Big bypass to prevent missing reference at programm exit.. + _close_function = ctypes.WinDLL("kernel32").CloseHandle + def _get_handle(self): + raise NotImplementedError("{0} is abstract".format(type(self).__name__)) + + @property + def handle(self): + """An handle on the object + + :type: HANDLE + + .. note:: + The handle is automaticaly closed when the object is destroyed + """ + if hasattr(self, "_handle"): + return self._handle + self._handle = self._get_handle() + dbgprint("Open handle {0} for {1}".format(hex(self._handle), self), "HANDLE") + return self._handle + + def wait(self, timeout=INFINITE): + """Wait for the object""" + return winproxy.WaitForSingleObject(self.handle, timeout) + + def __del__(self): + # sys.path is not None -> check if python shutdown + if hasattr(sys, "path") and sys.path is not None and hasattr(self, "_handle") and self._handle: + # Prevent some bug where dbgprint might be None when __del__ is called in a closing process + dbgprint("Closing Handle {0} for {1}".format(hex(self._handle), self), "HANDLE") if dbgprint is not None else None + self._close_function(self._handle) \ No newline at end of file diff --git a/windows/winobject/apisetmap.py b/windows/winobject/apisetmap.py index c7e17f5..c9e336e 100644 --- a/windows/winobject/apisetmap.py +++ b/windows/winobject/apisetmap.py @@ -1,3 +1,5 @@ +import ctypes + import windows import windows.generated_def as gdef diff --git a/windows/winobject/process.py b/windows/winobject/process.py index 1fb4218..dfcca6b 100644 --- a/windows/winobject/process.py +++ b/windows/winobject/process.py @@ -33,44 +33,7 @@ TimeInfo = namedtuple("TimeInfo", ["creation", "exit", "kernel", "user"]) """Time information about a process""" -class AutoHandle(object): - """An abstract class that allow easy handle creation/destruction/wait""" - # Big bypass to prevent missing reference at programm close.. - _close_function = ctypes.WinDLL("kernel32").CloseHandle - def _get_handle(self): - raise NotImplementedError("{0} is abstract".format(type(self).__name__)) - - @property - def handle(self): - """An handle on the object - - :type: HANDLE - - .. note:: - The handle is automaticaly closed when the object is destroyed - """ - if hasattr(self, "_handle"): - return self._handle - self._handle = self._get_handle() - dbgprint("Open handle {0} for {1}".format(hex(self._handle), self), "HANDLE") - #if "DEAD" in str(self): - # print("OPEN FOR THE DEADS") - # import pdb;pdb.set_trace() - return self._handle - - def wait(self, timeout=INFINITE): - """Wait for the object""" - return winproxy.WaitForSingleObject(self.handle, timeout) - - def __del__(self): - # sys.path is not None -> check if python shutdown - if hasattr(sys, "path") and sys.path is not None and hasattr(self, "_handle") and self._handle: - # Prevent some bug where dbgprint might be None when __del__ is called in a closing process - dbgprint("Closing Handle {0} for {1}".format(hex(self._handle), self), "HANDLE") if dbgprint is not None else None - self._close_function(self._handle) - - -class WinThread(AutoHandle): +class WinThread(utils.AutoHandle): """Represent a thread """ def __init__(self, tid=None, handle=None, owner_pid=None, owner=None): @@ -297,7 +260,7 @@ class WinThread(AutoHandle): # return Token(token_handle.value) -class DeadThread(AutoHandle): +class DeadThread(utils.AutoHandle): """An already dead thread (returned only by API returning a new thread if thread die before being returned)""" def __init__(self, handle, tid=None): if tid is None: @@ -325,7 +288,7 @@ class DeadThread(AutoHandle): return res.value -class Process(AutoHandle): +class Process(utils.AutoHandle): @utils.fixedpropety def is_wow_64(self): """``True`` if the process is a SysWow64 process (32bit process on 64bits system). @@ -666,6 +629,12 @@ class Process(AutoHandle): """write a qword at ``addr``""" return self.write_memory(addr, struct.pack("