execute_python doest not reinject the shellcode everytime + test hook 64b

This commit is contained in:
Clement Rouault
2016-06-08 11:01:19 +02:00
parent f23357c13e
commit e1f7f1e500
6 changed files with 240 additions and 185 deletions
+2 -1
View File
@@ -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)
* 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
+4 -6
View File
@@ -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
+18 -12
View File
@@ -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
+165 -165
View File
@@ -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):
+50
View File
@@ -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)
+1 -1
View File
@@ -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