diff --git a/CHANGELOG b/CHANGELOG index 1193aa8..5087b9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,4 +10,5 @@ Since 0.2: * registry.py handle REG_QWORD manually (_winreg does not) * CurrentProcessReadSyswow doest not use ``current_process.handle`` anymore but ``OpenProcess(current_process.pid)`` (Compat windows10 where method 1 doest not work) * Add: system.handles (winobject\handle.py) - * You can have multiple execute_python_unsafe at the same time in the same process (didn't know: consequence of new injection code) \ No newline at end of file + * You can have multiple execute_python_unsafe at the same time in the same process (didn't know: consequence of new injection code) + * WinProcess.execute_python does not regenerate/reinject the python_exec_shellcode for each execution \ No newline at end of file diff --git a/TODO b/TODO index 273ce42..f4641b8 100644 --- a/TODO +++ b/TODO @@ -16,12 +16,14 @@ TODO: - Quid IAT hook stub ? just einit threads and remove this ? - Continue test with new generate_callback_stub and remove commented code if it works + - rewrite/REMOVE? generate_stub_64[32] : it's a non-sens to not save stuff on the stack.. + I can re-copy the args on stack.. + - Injection - code generated by generate_python_exec_shellcode_64[32] may be reused Just need to pass the address of the python string as argument - - rewrite generate_stub_64[32] : it's a non-sens to not save stuff on the stack.. - I can re-copy the args on stack.. + - Winproxy: - rethink OptionalExport ? not useful with lazy resolution (or we need to force resolution..) @@ -44,15 +46,11 @@ TODO: - Some test/doc on windows.system.handles -CHANGELOG: - * re-check every sample - Documentation * verif samples FIXME: - setup.py build seems to raise an error - - winutils.create_process : use WinProcess._from_handle - Push("[ECX]") in simple_x64 as a "H" rex and i think it should not.. RESSOURCE diff --git a/windows/injection.py b/windows/injection.py index 11e62fa..bae9603 100644 --- a/windows/injection.py +++ b/windows/injection.py @@ -122,7 +122,7 @@ def load_dll_in_remote_process(target, dll_name): python_function_32_bits = {} # 32 to 32 injection -def generate_python_exec_shellcode_32(target, PYCODE_ADDR, PyDll): +def generate_python_exec_shellcode_32(target, PyDll): if not python_function_32_bits: pymodule = [mod for mod in target.peb.modules if mod.name == PyDll][0] Py_exports = pymodule.pe.exports @@ -158,7 +158,9 @@ def generate_python_exec_shellcode_32(target, PYCODE_ADDR, PyDll): code += x86.Mov('EAX', PyGILState_Ensure) code += x86.Call('EAX') code += x86.Push('EAX') - code += x86.Push(PYCODE_ADDR) + # Get the string to execute from parameters + code += x86.Mov("EAX", x86.mem("[ESP + 0x8]")) + code += x86.Push('EAX') code += x86.Mov('EAX', PyRun_SimpleString) code += x86.Call('EAX') code += x86.Mov("ESI", "EAX") @@ -180,7 +182,7 @@ def generate_python_exec_shellcode_32(target, PYCODE_ADDR, PyDll): python_function_64_bits = {} # 64 to 64 injection -def generate_python_exec_shellcode_64(target, PYCODE_ADDR, PyDll): +def generate_python_exec_shellcode_64(target, PyDll): if not python_function_64_bits: pymodule = [mod for mod in target.peb.modules if mod.name == PyDll][0] Py_exports = pymodule.pe.exports @@ -219,8 +221,8 @@ def generate_python_exec_shellcode_64(target, PYCODE_ADDR, PyDll): code += x64.Mov('RAX', PyGILState_Ensure) code += x64.Call('RAX') code += x64.Mov('R15', 'RAX') + code += x64.Mov("RCX", x64.mem("[RSP + 0x20]")) code += x64.Mov('RAX', PyRun_SimpleString) - code += x64.Mov('RCX', PYCODE_ADDR) code += x64.Call('RAX') code += x64.Mov('RCX', 'R15') code += x64.Mov('R15', 'RAX') @@ -245,18 +247,22 @@ def inject_python_command(target, code_injected, PYDLL): """Postulate: PYDLL is already loaded in target process""" PYCODE = code_injected + "\x00" # TODO: free this (how ? when ?) - remote_addr = target.virtual_alloc(len(PYCODE) + 0x100) - target.write_memory(remote_addr, PYCODE) - SHELLCODE_ADDR = remote_addr + len(PYCODE) + remote_python_code_addr = target.virtual_alloc(len(PYCODE)) + target.write_memory(remote_python_code_addr, PYCODE) + shellcode_addr = getattr(target, "_execute_python_shellcode", None) + if shellcode_addr is not None: + return shellcode_addr, remote_python_code_addr if target.bitness == 32: shellcode_generator = generate_python_exec_shellcode_32 else: shellcode_generator = generate_python_exec_shellcode_64 - shellcode = shellcode_generator(target, remote_addr, PYDLL) - target.write_memory(SHELLCODE_ADDR, shellcode) - return SHELLCODE_ADDR + shellcode = shellcode_generator(target, PYDLL) + shellcode_addr = target.virtual_alloc(len(shellcode)) + target.write_memory(shellcode_addr, shellcode) + target._execute_python_shellcode = shellcode_addr + return shellcode_addr, remote_python_code_addr def validate_python_dll_presence_on_disk(process): @@ -276,8 +282,8 @@ def validate_python_dll_presence_on_disk(process): def execute_python_code(process, code): validate_python_dll_presence_on_disk(process) load_dll_in_remote_process(process, "python27.dll") - addr = inject_python_command(process, code, "python27.dll") - t = process.create_thread(addr, 0) + shellcode, pythoncode = inject_python_command(process, code, "python27.dll") + t = process.create_thread(shellcode, pythoncode) return t diff --git a/windows/native_exec/native_function.py b/windows/native_exec/native_function.py index 6812298..a10c689 100644 --- a/windows/native_exec/native_function.py +++ b/windows/native_exec/native_function.py @@ -101,171 +101,171 @@ class CustomAllocator(object): allocator = CustomAllocator() -def get_functions(): - version = sys.version_info - python_dll = "python" + str(version.major) + str(version.minor) - - PyGILState_Ensure = windows.utils.get_func_addr(python_dll, 'PyGILState_Ensure'.encode()) - PyObject_CallObject = windows.utils.get_func_addr(python_dll, 'PyObject_CallObject'.encode()) - PyGILState_Release = windows.utils.get_func_addr(python_dll, 'PyGILState_Release'.encode()) - return [PyGILState_Ensure, PyObject_CallObject, PyGILState_Release] - - -def analyse_callback(callback): - if not callable(callback): - raise ValueError("Need a callable object :)") - obj_id = id(callback) - if not hasattr(callback, '_objects'): - raise ValueError("Need a ctypes PyCFuncPtr") - return obj_id - - -# For windows 32 bits with stdcall -def generate_stub_32(callback): - c_callback = ctypes.cast(callback, ctypes.c_void_p).value - - gstate_save_addr = x86.create_displacement(disp=allocator.reserve_int()) - return_addr_save_addr = x86.create_displacement(disp=allocator.reserve_int()) - save_ebx = x86.create_displacement(disp=allocator.reserve_int()) - save_ecx = x86.create_displacement(disp=allocator.reserve_int()) - save_edx = x86.create_displacement(disp=allocator.reserve_int()) - save_esi = x86.create_displacement(disp=allocator.reserve_int()) - save_edi = x86.create_displacement(disp=allocator.reserve_int()) - - ensure, objcall, release = get_functions() - - code = x86.MultipleInstr() - # ## Shellcode ## # - code += x86.Mov(save_ebx, 'EBX') - code += x86.Mov(save_ecx, 'ECX') - code += x86.Mov(save_edx, 'EDX') - code += x86.Mov(save_esi, 'ESI') - code += x86.Mov(save_edi, 'EDI') - - code += x86.Mov('EAX', ensure) - code += x86.Call('EAX') - code += x86.Mov(gstate_save_addr, 'EAX') - - # Save real return addr (for good argument parsing by the callback) - code += x86.Pop('EAX') - code += x86.Mov(return_addr_save_addr, 'EAX') - - code += x86.Mov('EAX', c_callback) - code += x86.Call('EAX') - - # Restore real return value - code += x86.Mov('EBX', return_addr_save_addr) - code += x86.Push('EBX') - - # Save return value - code += x86.Push('EAX') - code += x86.Mov('EBX', gstate_save_addr) - code += x86.Push('EBX') - - code += x86.Mov('EAX', release) - code += x86.Call('EAX') - - # Discard `release` argument - code += x86.Pop('EAX') - # Restore return value - code += x86.Pop('EAX') - code += x86.Mov('EBX', save_ebx) - code += x86.Mov('ECX', save_ecx) - code += x86.Mov('EDX', save_edx) - code += x86.Mov('ESI', save_esi) - code += x86.Mov('EDI', save_edi) - code += x86.Ret() - return code - - -def generate_stub_64(callback): - c_callback = ctypes.cast(callback, ctypes.c_void_p).value - REG_LEN = ctypes.sizeof(ctypes.c_void_p) - register_to_save = ("RBX", "RCX", "RDX", "RSI", "RDI", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15") - - push_all_save_register = x64.MultipleInstr([x64.Push(reg) for reg in register_to_save]) - pop_all_save_register = x64.MultipleInstr([x64.Pop(reg) for reg in reversed(register_to_save)]) - # Reserve parallel `stack` - save_register_space = allocator.reserve_int(len(register_to_save)) - save_register_space_end = save_register_space + (ctypes.sizeof(ctypes.c_void_p) * (len(register_to_save))) - - save_rbx = save_register_space_end - REG_LEN - save_rbx # Fuck the linter :D - save_rcx = save_register_space_end - REG_LEN - REG_LEN - save_rdx = save_register_space_end - REG_LEN - (REG_LEN * 2) - save_rsi = save_register_space_end - REG_LEN - (REG_LEN * 3) - save_rsi # Fuck the linter :D - save_rdi = save_register_space_end - REG_LEN - (REG_LEN * 4) - save_rdi # Fuck the linter :D - save_r8 = save_register_space_end - REG_LEN - (REG_LEN * 5) - save_r9 = save_register_space_end - REG_LEN - (REG_LEN * 6) - - gstate_save_addr = x64.create_displacement(disp=allocator.reserve_int()) - return_addr_save_addr = x64.create_displacement(disp=allocator.reserve_int()) - return_value_save_addr = x64.create_displacement(disp=allocator.reserve_int()) - - Reserve_space_for_call = x64.MultipleInstr([x64.Push('RDI')] * 4) - Clean_space_for_call = x64.MultipleInstr([x64.Pop('RDI')] * 4) - Do_stack_alignement = x64.MultipleInstr([x64.Push('RDI')] * 1) - Remove_stack_alignement = x64.MultipleInstr([x64.Pop('RDI')] * 1) - - ensure, objcall, release = get_functions() - - # ## Shellcode ## # - code = x64.MultipleInstr() - # Save all registers - code += x64.Mov('RAX', save_register_space_end) - code += x64.Xchg('RAX', 'RSP') - code += push_all_save_register - code += x64.Xchg('RAX', 'RSP') - # GOOO - code += x64.Mov('RAX', ensure) - code += Reserve_space_for_call - code += Do_stack_alignement - code += x64.Call('RAX') - code += Remove_stack_alignement - code += Clean_space_for_call - code += x64.Mov(gstate_save_addr, 'RAX') - # Save real return addr (for good argument parsing by the callback) - code += x64.Pop('RAX') - code += x64.Mov(return_addr_save_addr, 'RAX') - # Restore parameters for real function call - code += x64.Mov('RAX', save_rcx) - code += x64.Mov('RCX', x64.mem('[RAX]')) - code += x64.Mov('RAX', save_rdx) - code += x64.Mov('RDX', x64.mem('[RAX]')) - code += x64.Mov('RAX', save_r9) - code += x64.Mov('R9', x64.mem('[RAX]')) - code += x64.Mov('RAX', save_r8) - code += x64.Mov('R8', x64.mem('[RAX]')) - # Call python code - code += x64.Mov('RAX', c_callback) - # no need for stack alignement here as we poped the return addr - # no need for Reserve_space_for_call as we must use the previous one for correct argument parsing - code += x64.Call('RAX') - # Save return value - code += x64.Mov(return_value_save_addr, 'RAX') - # Repush real return value - code += x64.Mov('RAX', return_addr_save_addr) - code += x64.Push('RAX') - # Call release(gstate_save) - code += x64.Mov('RAX', gstate_save_addr) - code += x64.Mov('RCX', 'RAX') - code += x64.Mov('RAX', release) - code += Reserve_space_for_call - code += Do_stack_alignement - code += x64.Call('RAX') - code += Remove_stack_alignement - code += Clean_space_for_call - # Restore registers - code += x64.Mov('RAX', save_register_space) - code += x64.Xchg('RAX', 'RSP') - code += pop_all_save_register - code += x64.Xchg('RAX', 'RSP') - # Restore return value - code += x64.Mov('RAX', return_value_save_addr) - code += x64.Ret() - return code +#def get_functions(): +# version = sys.version_info +# python_dll = "python" + str(version.major) + str(version.minor) +# +# PyGILState_Ensure = windows.utils.get_func_addr(python_dll, 'PyGILState_Ensure'.encode()) +# PyObject_CallObject = windows.utils.get_func_addr(python_dll, 'PyObject_CallObject'.encode()) +# PyGILState_Release = windows.utils.get_func_addr(python_dll, 'PyGILState_Release'.encode()) +# return [PyGILState_Ensure, PyObject_CallObject, PyGILState_Release] +# +# +#def analyse_callback(callback): +# if not callable(callback): +# raise ValueError("Need a callable object :)") +# obj_id = id(callback) +# if not hasattr(callback, '_objects'): +# raise ValueError("Need a ctypes PyCFuncPtr") +# return obj_id +# +# +## For windows 32 bits with stdcall +#def generate_stub_32(callback): +# c_callback = ctypes.cast(callback, ctypes.c_void_p).value +# +# gstate_save_addr = x86.create_displacement(disp=allocator.reserve_int()) +# return_addr_save_addr = x86.create_displacement(disp=allocator.reserve_int()) +# save_ebx = x86.create_displacement(disp=allocator.reserve_int()) +# save_ecx = x86.create_displacement(disp=allocator.reserve_int()) +# save_edx = x86.create_displacement(disp=allocator.reserve_int()) +# save_esi = x86.create_displacement(disp=allocator.reserve_int()) +# save_edi = x86.create_displacement(disp=allocator.reserve_int()) +# +# ensure, objcall, release = get_functions() +# +# code = x86.MultipleInstr() +# # ## Shellcode ## # +# code += x86.Mov(save_ebx, 'EBX') +# code += x86.Mov(save_ecx, 'ECX') +# code += x86.Mov(save_edx, 'EDX') +# code += x86.Mov(save_esi, 'ESI') +# code += x86.Mov(save_edi, 'EDI') +# +# code += x86.Mov('EAX', ensure) +# code += x86.Call('EAX') +# code += x86.Mov(gstate_save_addr, 'EAX') +# +# # Save real return addr (for good argument parsing by the callback) +# code += x86.Pop('EAX') +# code += x86.Mov(return_addr_save_addr, 'EAX') +# +# code += x86.Mov('EAX', c_callback) +# code += x86.Call('EAX') +# +# # Restore real return value +# code += x86.Mov('EBX', return_addr_save_addr) +# code += x86.Push('EBX') +# +# # Save return value +# code += x86.Push('EAX') +# code += x86.Mov('EBX', gstate_save_addr) +# code += x86.Push('EBX') +# +# code += x86.Mov('EAX', release) +# code += x86.Call('EAX') +# +# # Discard `release` argument +# code += x86.Pop('EAX') +# # Restore return value +# code += x86.Pop('EAX') +# code += x86.Mov('EBX', save_ebx) +# code += x86.Mov('ECX', save_ecx) +# code += x86.Mov('EDX', save_edx) +# code += x86.Mov('ESI', save_esi) +# code += x86.Mov('EDI', save_edi) +# code += x86.Ret() +# return code +# +# +#def generate_stub_64(callback): +# c_callback = ctypes.cast(callback, ctypes.c_void_p).value +# REG_LEN = ctypes.sizeof(ctypes.c_void_p) +# register_to_save = ("RBX", "RCX", "RDX", "RSI", "RDI", "R8", "R9", "R10", "R11", "R12", "R13", "R14", "R15") +# +# push_all_save_register = x64.MultipleInstr([x64.Push(reg) for reg in register_to_save]) +# pop_all_save_register = x64.MultipleInstr([x64.Pop(reg) for reg in reversed(register_to_save)]) +# # Reserve parallel `stack` +# save_register_space = allocator.reserve_int(len(register_to_save)) +# save_register_space_end = save_register_space + (ctypes.sizeof(ctypes.c_void_p) * (len(register_to_save))) +# +# save_rbx = save_register_space_end - REG_LEN +# save_rbx # Fuck the linter :D +# save_rcx = save_register_space_end - REG_LEN - REG_LEN +# save_rdx = save_register_space_end - REG_LEN - (REG_LEN * 2) +# save_rsi = save_register_space_end - REG_LEN - (REG_LEN * 3) +# save_rsi # Fuck the linter :D +# save_rdi = save_register_space_end - REG_LEN - (REG_LEN * 4) +# save_rdi # Fuck the linter :D +# save_r8 = save_register_space_end - REG_LEN - (REG_LEN * 5) +# save_r9 = save_register_space_end - REG_LEN - (REG_LEN * 6) +# +# gstate_save_addr = x64.create_displacement(disp=allocator.reserve_int()) +# return_addr_save_addr = x64.create_displacement(disp=allocator.reserve_int()) +# return_value_save_addr = x64.create_displacement(disp=allocator.reserve_int()) +# +# Reserve_space_for_call = x64.MultipleInstr([x64.Push('RDI')] * 4) +# Clean_space_for_call = x64.MultipleInstr([x64.Pop('RDI')] * 4) +# Do_stack_alignement = x64.MultipleInstr([x64.Push('RDI')] * 1) +# Remove_stack_alignement = x64.MultipleInstr([x64.Pop('RDI')] * 1) +# +# ensure, objcall, release = get_functions() +# +# # ## Shellcode ## # +# code = x64.MultipleInstr() +# # Save all registers +# code += x64.Mov('RAX', save_register_space_end) +# code += x64.Xchg('RAX', 'RSP') +# code += push_all_save_register +# code += x64.Xchg('RAX', 'RSP') +# # GOOO +# code += x64.Mov('RAX', ensure) +# code += Reserve_space_for_call +# code += Do_stack_alignement +# code += x64.Call('RAX') +# code += Remove_stack_alignement +# code += Clean_space_for_call +# code += x64.Mov(gstate_save_addr, 'RAX') +# # Save real return addr (for good argument parsing by the callback) +# code += x64.Pop('RAX') +# code += x64.Mov(return_addr_save_addr, 'RAX') +# # Restore parameters for real function call +# code += x64.Mov('RAX', save_rcx) +# code += x64.Mov('RCX', x64.mem('[RAX]')) +# code += x64.Mov('RAX', save_rdx) +# code += x64.Mov('RDX', x64.mem('[RAX]')) +# code += x64.Mov('RAX', save_r9) +# code += x64.Mov('R9', x64.mem('[RAX]')) +# code += x64.Mov('RAX', save_r8) +# code += x64.Mov('R8', x64.mem('[RAX]')) +# # Call python code +# code += x64.Mov('RAX', c_callback) +# # no need for stack alignement here as we poped the return addr +# # no need for Reserve_space_for_call as we must use the previous one for correct argument parsing +# code += x64.Call('RAX') +# # Save return value +# code += x64.Mov(return_value_save_addr, 'RAX') +# # Repush real return value +# code += x64.Mov('RAX', return_addr_save_addr) +# code += x64.Push('RAX') +# # Call release(gstate_save) +# code += x64.Mov('RAX', gstate_save_addr) +# code += x64.Mov('RCX', 'RAX') +# code += x64.Mov('RAX', release) +# code += Reserve_space_for_call +# code += Do_stack_alignement +# code += x64.Call('RAX') +# code += Remove_stack_alignement +# code += Clean_space_for_call +# # Restore registers +# code += x64.Mov('RAX', save_register_space) +# code += x64.Xchg('RAX', 'RSP') +# code += pop_all_save_register +# code += x64.Xchg('RAX', 'RSP') +# # Restore return value +# code += x64.Mov('RAX', return_value_save_addr) +# code += x64.Ret() +# return code def generate_callback_stub(callback, types): diff --git a/windows/test/test_hooks.py b/windows/test/test_hooks.py index b46b772..26e2393 100644 --- a/windows/test/test_hooks.py +++ b/windows/test/test_hooks.py @@ -117,6 +117,56 @@ class HookTestCase(unittest.TestCase): with calc.allocated_memory(0x1000) as addr: calc.write_memory(addr, "DLLNOTFOUND.NOT_A_REAL_DLL" + "\x00") t = calc.create_thread(load_libraryA, addr) + t.wait() self.assertEqual(remote_ask("windows.current_thread.exit(len(calling_thread))"), 3) + def test_remote_iat_hook_64(self): + with Calc64() as calc: + calc.execute_python("import windows") + calc.execute_python("windows.utils.create_console()") + code = """ + import windows.generated_def as gdef + + cp = windows.current_process + kernelbase_mod = [m for m in cp.peb.modules if m.name == "kernelbase.dll"][0] + LdrLoadDll = [n for n in kernelbase_mod.pe.imports['ntdll.dll'] if n.name == "LdrLoadDll"][0] + + calling_thread = set([]) + hooking_thread = windows.current_thread.tid + @windows.hooks.Callback(*[gdef.PVOID] * 5) + def MyHook(*args, **kwargs): + calling_thread.add(windows.current_thread.tid) + print(windows.current_thread.tid) + return kwargs["real_function"]() + + LdrLoadDll.set_hook(MyHook) + print("Hooker = " + str(windows.current_thread.tid)) + import ctypes + try: + ctypes.WinDLL("NOT_A_REAL_DLL") + except WindowsError as e: + pass + """ + calc.execute_python_unsafe(textwrap.dedent(code)) + # Tricky part: we use an injected thread exit_value to ask stuff about the remote python + def remote_ask(request): + t = calc.execute_python_unsafe(request) + t.wait() + return t.exit_code + + self.assertEqual(remote_ask("windows.current_thread.exit(len(calling_thread))"), 1) + self.assertEqual(remote_ask("windows.current_thread.exit(calling_thread == set([hooking_thread]))"), 1) + + # Trigger hook from another Python thread + calc.execute_python_unsafe("ctypes.WinDLL('ANOTHER_FAKE_DLL')").wait() + self.assertEqual(remote_ask("windows.current_thread.exit(len(calling_thread))"), 2) + + # Trigger hook from a NONPython thread + k32 = [m for m in calc.peb.modules if m.name == "kernel32.dll"][0] + load_libraryA = k32.pe.exports["LoadLibraryA"] + with calc.allocated_memory(0x1000) as addr: + calc.write_memory(addr, "DLLNOTFOUND.NOT_A_REAL_DLL" + "\x00") + t = calc.create_thread(load_libraryA, addr) + t.wait() + self.assertEqual(remote_ask("windows.current_thread.exit(len(calling_thread))"), 3) diff --git a/windows/utils/winutils.py b/windows/utils/winutils.py index a945543..5922183 100644 --- a/windows/utils/winutils.py +++ b/windows/utils/winutils.py @@ -207,7 +207,7 @@ def get_short_path(path): return buffer[:rsize] def get_shared_mapping(name, size=0x1000): - # TODO: real cod + # TODO: real code h = windows.winproxy.CreateFileMappingA(INVALID_HANDLE_VALUE, dwMaximumSizeLow=size, lpName=name) addr = windows.winproxy.MapViewOfFile(h, dwNumberOfBytesToMap=size) return addr