From 5f5ad2531bedf734be718986c0ae926a52a9306d Mon Sep 17 00:00:00 2001 From: hakril Date: Fri, 1 Aug 2025 09:21:07 +0200 Subject: [PATCH 1/6] Fixing a bug in FunctionParamDumpBPAbstract triggering remote null deref --- windows/debug/breakpoints.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/windows/debug/breakpoints.py b/windows/debug/breakpoints.py index 1dc5d0ba..8e9d308a 100644 --- a/windows/debug/breakpoints.py +++ b/windows/debug/breakpoints.py @@ -126,14 +126,15 @@ def extract_arguments_32bits(self, cproc, cthread): def extract_arguments_64bits(self, cproc, cthread): x = windows.debug.X64ArgumentRetriever() res = OrderedDict() - for i, (name, type) in enumerate(zip(self.target_params, self.target_args)): + for i, (name, atype) in enumerate(zip(self.target_params, self.target_args)): value = x.get_arg(i, cproc, cthread) - rt = windows.remotectypes.transform_type_to_remote64bits(type) + rt = windows.remotectypes.transform_type_to_remote64bits(atype) if issubclass(rt, windows.remotectypes.RemoteValue): t = rt(value, cproc) else: t = rt(value) - if not hasattr(t, "contents"): + + if not isinstance(t, (windows.remotectypes.RemotePtr64, windows.remotectypes.RemotePtr32)): try: t = t.value except AttributeError: From 426d28ded95d5d58840e51d02b2b5c26ba3c9646 Mon Sep 17 00:00:00 2001 From: hakril Date: Fri, 1 Aug 2025 11:15:05 +0200 Subject: [PATCH 2/6] more fix in breakpoints extract_arguments_64bits --- windows/debug/breakpoints.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/windows/debug/breakpoints.py b/windows/debug/breakpoints.py index 8e9d308a..a7cf3841 100644 --- a/windows/debug/breakpoints.py +++ b/windows/debug/breakpoints.py @@ -134,7 +134,8 @@ def extract_arguments_64bits(self, cproc, cthread): else: t = rt(value) - if not isinstance(t, (windows.remotectypes.RemotePtr64, windows.remotectypes.RemotePtr32)): + if (not isinstance(t, (windows.remotectypes.RemotePtr64, windows.remotectypes.RemotePtr32)) or + isinstance(t, (ctypes.c_char_p, ctypes.c_wchar_p))): try: t = t.value except AttributeError: From bef410a1c38441531740c96c0b7d378c88301bd8 Mon Sep 17 00:00:00 2001 From: hakril Date: Fri, 1 Aug 2025 11:32:48 +0200 Subject: [PATCH 3/6] Fix windows tested version with server-2019 deprecation: https://github.com/actions/runner-images/issues/12045 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a11bca06..bcc801f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - runs-on: [windows-2019, windows-latest] + runs-on: [windows-2022, windows-latest] python-version: [2.7, 3.6, 3.11] python-architecture: [x86, x64] include: From c46d76a8468e9a91df794624be58a0a69e408779 Mon Sep 17 00:00:00 2001 From: hakril Date: Thu, 7 Aug 2025 16:59:02 +0200 Subject: [PATCH 4/6] Fixing pass_memory-breakpoint logic --- windows/debug/breakpoints.py | 1 + windows/debug/debugger.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/windows/debug/breakpoints.py b/windows/debug/breakpoints.py index a7cf3841..cbfdb1ce 100644 --- a/windows/debug/breakpoints.py +++ b/windows/debug/breakpoints.py @@ -55,6 +55,7 @@ def __init__(self, addr, size=None, events=None): self.size = size if size is not None else self.DEFAULT_SIZE events = events if events is not None else self.DEFAULT_EVENTS self.events = set(events) + self._reput_pages = [] # The current memory BP page that is passed def trigger(self, dbg, exception): """Called when breakpoint is hit""" diff --git a/windows/debug/debugger.py b/windows/debug/debugger.py index 43c30611..47b067ef 100644 --- a/windows/debug/debugger.py +++ b/windows/debug/debugger.py @@ -422,8 +422,9 @@ def _setup_breakpoint_MEMBP(self, bp, target): return True def _restore_breakpoint_MEMBP(self, bp, target): - (page_addr, page_prot) = bp._reput_page - return target.virtual_protect(page_addr, PAGE_SIZE, page_prot, None) + for (page_addr, page_prot) in bp._reput_pages: + target.virtual_protect(page_addr, PAGE_SIZE, page_prot, None) + bp._reput_pages.clear() def _remove_breakpoint_MEMBP(self, bp, target): @@ -542,7 +543,7 @@ def _pass_memory_breakpoint(self, bp, page_protect, fault_page): ctx = thread.context ctx.EEFlags.TF = 1 thread.set_context(ctx) - bp._reput_page = (fault_page, page_prot.value) + bp._reput_pages.append((fault_page, page_prot.value)) self._breakpoint_to_reput[cp.pid].add(bp) # debug event handlers @@ -665,6 +666,7 @@ def _handle_exception_access_violation(self, exception, excp_addr): fault_type = exception.ExceptionRecord.ExceptionInformation[0] fault_addr = exception.ExceptionRecord.ExceptionInformation[1] pc_addr = self.current_thread.context.pc + dbgprint("Handling access_violation at pc={0:#x} addr={1:#x}".format(pc_addr, fault_addr), "DBG") if fault_addr == pc_addr: fault_type = EXEC event = EVENT_STR[fault_type] From 35e7a8da4a986b33fa542fa932d3073e409f7c5b Mon Sep 17 00:00:00 2001 From: hakril Date: Mon, 11 Aug 2025 13:11:38 +0200 Subject: [PATCH 5/6] Add fix + tests to handle debugger memory-bp triggering twice on same instruction (when instruction write on 2 != pages) --- tests/test_debugger.py | 52 ++++++++++++++++++++++++++++++++++++++- windows/debug/debugger.py | 15 ++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/tests/test_debugger.py b/tests/test_debugger.py index 9035ffc0..b57cd840 100644 --- a/tests/test_debugger.py +++ b/tests/test_debugger.py @@ -367,6 +367,54 @@ def trigger(self, dbg, exc): for i in range(NB_NOP_IN_PAGE + 1): assert TSTBP.DATA[i] == addr + i +def test_memory_breakpoint_trigger_multipage(proc32_64_debug): + """Check that a memory breakpoint triggering on multiple page on the same instruction restore are correctly restored on all pages""" + + class MultiPageMemBP(windows.debug.MemoryBreakpoint): + ALL_TRIGGER_ADDR = [] + + def trigger(self, dbg, exc): + pc_fault_addr = dbg.current_thread.context.pc + self.ALL_TRIGGER_ADDR.append(pc_fault_addr) + print(hex(pc_fault_addr)) + # Stop when we have a breakpoint in the 2nd page of the alloc + if shellcodeaddr + 0x1000 <= pc_fault_addr <= shellcodeaddr + 0x2000: + dbg.current_process.exit() + + # Trigger a write that will write on both page at once, + # Triggering both pages mem-bp on the same instruction. + # Then call an instruction at the end of page to see if both page of mem-bp still trigger the BP + + shellcodeaddr = proc32_64_debug.virtual_alloc(0x2000) + + if proc32_64_debug.bitness == 64: + shellcode = x64.MultipleInstr() + shellcode += x64.Mov("RAX", shellcodeaddr + 0xffe) + shellcode += x64.Mov("RCX", 0xc3909090) # Nopnopnopret + shellcode += x64.Mov(x64.mem("[RAX]"), "ECX") # Will write on both page at once + shellcode += x64.Push("RAX") + shellcode += x64.Ret() # Jump on the nop + ret + else: + shellcode = x86.MultipleInstr() + shellcode += x86.Mov("EAX", shellcodeaddr + 0xffe) + shellcode += x86.Mov("ECX", 0xc3909090) # Nopnopnopret + shellcode += x86.Mov(x86.mem("[EAX]"), "ECX") # Will write on both page at once + shellcode += x86.Push("EAX") + shellcode += x86.Ret() # Jump on the nop + ret + + d = windows.debug.Debugger(proc32_64_debug) + bp = MultiPageMemBP(addr=shellcodeaddr, size=0x2000, events="XW") + proc32_64_debug.write_memory(shellcodeaddr, shellcode.get_code()) + d.add_bp(bp) + proc32_64_debug.create_thread(shellcodeaddr, 0) + + d.loop() + # Check that the 2 nop at the end of the first page of the membp trigger + # If access right where not correctly restored: this would not trigger on the first page + assert shellcodeaddr + 0xffe in bp.ALL_TRIGGER_ADDR + assert shellcodeaddr + 0xfff in bp.ALL_TRIGGER_ADDR + assert shellcodeaddr + 0x1000 in bp.ALL_TRIGGER_ADDR + # breakpoint remove import threading @@ -700,4 +748,6 @@ def WaitForDebugEvent_KeyboardInterrupt(debug_event): assert proc32_64_debug.read_memory(addr, len(TEST_CODE)) == TEST_CODE assert bad_thread.context.pc == addr else: - raise ValueError("Should have raised") \ No newline at end of file + raise ValueError("Should have raised") + + diff --git a/windows/debug/debugger.py b/windows/debug/debugger.py index 47b067ef..16bfdc31 100644 --- a/windows/debug/debugger.py +++ b/windows/debug/debugger.py @@ -685,11 +685,24 @@ def _handle_exception_access_violation(self, exception, excp_addr): self._pass_memory_breakpoint(bp, original_prot, fault_page) return DBG_CONTINUE + # We may have setup the "EEFlags.TF" ourself if the membreakpoint triggered twice on the same instruction + # Ex: write on two pages handled by our breakpoint (unaligned write on 0xfff-0x1000) + + originalctx = self.current_thread.context + original_tf = originalctx.EEFlags.TF + # Temporary disable EEFlags.TF to see if user callback explicit ask for it + originalctx.EEFlags.TF = 0 + self.current_thread.set_context(originalctx) + with self.DisabledMemoryBreakpoint(): continue_flag = mem_bp.trigger(self, exception) if self._killed_in_action(): return continue_flag - self._explicit_single_step[self.current_thread.tid] = self.current_thread.context.EEFlags.TF + # Update explicit trigger based on new value of EEFlags.TF + self._explicit_single_step[self.current_thread.tid] |= self.current_thread.context.EEFlags.TF + # Reupdate the real EEFlags.TF based on its current value and the original one + if original_tf != 0 and not self.current_thread.context.EEFlags.TF: + self.single_step() if self._explicit_single_step[self.current_thread.tid]: dbgprint("Someone ask for an explicit Single step - 5", "DBG") # If BP has not been removed in trigger, pas it From 4b71ebb17436fbb5a2b411b1957613cf340d3ff5 Mon Sep 17 00:00:00 2001 From: hakril Date: Mon, 11 Aug 2025 15:01:10 +0200 Subject: [PATCH 6/6] DBG: fix py2 compat --- windows/debug/debugger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/debug/debugger.py b/windows/debug/debugger.py index 16bfdc31..e1e030a0 100644 --- a/windows/debug/debugger.py +++ b/windows/debug/debugger.py @@ -424,7 +424,7 @@ def _setup_breakpoint_MEMBP(self, bp, target): def _restore_breakpoint_MEMBP(self, bp, target): for (page_addr, page_prot) in bp._reput_pages: target.virtual_protect(page_addr, PAGE_SIZE, page_prot, None) - bp._reput_pages.clear() + del bp._reput_pages[:] def _remove_breakpoint_MEMBP(self, bp, target):