mirror of
https://github.com/HyperDbg/HyperDbg.git
synced 2026-07-13 02:59:30 +00:00
1283 lines
40 KiB
C
1283 lines
40 KiB
C
/**
|
|
* @file BreakpointCommands.c
|
|
* @author Sina Karvandi (sina@hyperdbg.org)
|
|
* @brief Routines for breakpoint commands
|
|
* @details
|
|
* @version 0.1
|
|
* @date 2021-03-12
|
|
*
|
|
* @copyright This project is released under the GNU Public License v3.
|
|
*
|
|
*/
|
|
#include "pch.h"
|
|
|
|
/**
|
|
* @brief Check and perform actions on RFLAGS.TF
|
|
* @param ProcessId
|
|
* @param ThreadId
|
|
* @param TrapSetByDebugger
|
|
*
|
|
* @return BOOLEAN Shows whether the #DB should be handled by the debugger or re-injected
|
|
*/
|
|
BOOLEAN
|
|
BreakpointCheckAndPerformActionsOnTrapFlags(UINT32 ProcessId, UINT32 ThreadId, BOOLEAN * TrapSetByDebugger)
|
|
{
|
|
UINT32 Index;
|
|
DEBUGGER_PROCESS_THREAD_INFORMATION ProcThrdInfo = {0};
|
|
BOOLEAN Result;
|
|
BOOLEAN ResultToReturn;
|
|
RFLAGS Rflags = {0};
|
|
|
|
//
|
|
// Read the RFLAGS
|
|
//
|
|
Rflags.AsUInt = VmFuncGetRflags();
|
|
|
|
//
|
|
// Form the process id and thread id into a 64-bit value
|
|
//
|
|
ProcThrdInfo.Fields.ProcessId = ProcessId;
|
|
ProcThrdInfo.Fields.ThreadId = ThreadId;
|
|
|
|
//
|
|
// Make sure, nobody is in the middle of modifying the list
|
|
//
|
|
SpinlockLock(&BreakpointCommandTrapListLock);
|
|
|
|
//
|
|
// *** Search the list of processes/threads for the current process's trap flag state ***
|
|
//
|
|
Result = BinarySearchPerformSearchItem((UINT64 *)&g_TrapFlagState.ThreadInformation[0],
|
|
g_TrapFlagState.NumberOfItems,
|
|
&Index,
|
|
ProcThrdInfo.AsUInt);
|
|
|
|
//
|
|
// Indicate whether the trap flag is set by the debugger or not
|
|
//
|
|
*TrapSetByDebugger = Result;
|
|
|
|
//
|
|
// We check the trap flag after the results because we might set the trap flag
|
|
// for the thread but the thread might run 'popfq' removing our trap flag
|
|
// so, we both check whether thread is expected to have trap flag, if not
|
|
// we check whether the trap flag is available or not
|
|
//
|
|
if (!Result && !Rflags.TrapFlag)
|
|
{
|
|
//
|
|
// It's not related to a TRAP FLAG, and we didn't previously set trap flag for this thread
|
|
// So, probably other events like setting hardware debug breakpoints caused this #DB
|
|
// which means that it should be handled by the debugger
|
|
//
|
|
ResultToReturn = TRUE;
|
|
goto Return;
|
|
}
|
|
else if (!Result && Rflags.TrapFlag)
|
|
{
|
|
//
|
|
// As it's not set by the debugger (not found in our list), it means the program or
|
|
// a debugger already set the trap flag, we'll return FALSE
|
|
//
|
|
// LogInfo("Caution: The process (pid:%x, tid:%x, name:%s) is utilizing a trap flag, "
|
|
// "which was not previously adjusted by HyperDbg. This occurrence could indicate "
|
|
// "the employment of an anti-debugging technique by the process or the involvement "
|
|
// "of another debugger. By default, HyperDbg automatically manages these #DB events "
|
|
// "and halt the debugger; however, if you wish to redirect them to the debugger, "
|
|
// "you can utilize 'test trap off'. Alternatively, you can use the transparent-mode "
|
|
// "to mitigate these situations",
|
|
// PsGetCurrentProcessId(),
|
|
// PsGetCurrentThreadId(),
|
|
// CommonGetProcessNameFromProcessControlBlock(PsGetCurrentProcess()));
|
|
|
|
//
|
|
// Returning false means that it should be re-injected into the debuggee
|
|
//
|
|
ResultToReturn = FALSE;
|
|
goto Return;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// *** being here means the thread is found in the list of threads that we set TRAP FLAG on it ***
|
|
//
|
|
|
|
//
|
|
// Uset or set the TRAP flag
|
|
//
|
|
VmFuncSetRflagTrapFlag(FALSE);
|
|
|
|
//
|
|
// Remove the thread/process from the list
|
|
// We're sure the Result is TRUE
|
|
//
|
|
InsertionSortDeleteItem((UINT64 *)&g_TrapFlagState.ThreadInformation[0],
|
|
&g_TrapFlagState.NumberOfItems,
|
|
Index);
|
|
|
|
//
|
|
// Handled #DB by debugger
|
|
//
|
|
ResultToReturn = TRUE;
|
|
goto Return;
|
|
}
|
|
|
|
Return:
|
|
|
|
//
|
|
// Unlock the list modification lock
|
|
//
|
|
SpinlockUnlock(&BreakpointCommandTrapListLock);
|
|
|
|
//
|
|
// By default, #DBs are managed by HyperDbg
|
|
//
|
|
return ResultToReturn;
|
|
}
|
|
|
|
/**
|
|
* @brief Trigger callback for breakpoint hit
|
|
*
|
|
* @param DbgState The state of the debugger on the current core
|
|
* @param ProcessId
|
|
* @param ThreadId
|
|
*
|
|
* @return BOOLEAN If true, it won't halt the debugger, but if false will halt the debugger
|
|
*/
|
|
BOOLEAN
|
|
BreakpointTriggerCallbacks(PROCESSOR_DEBUGGING_STATE * DbgState, UINT32 ProcessId, UINT32 ThreadId)
|
|
{
|
|
UNREFERENCED_PARAMETER(DbgState);
|
|
UNREFERENCED_PARAMETER(ProcessId);
|
|
UNREFERENCED_PARAMETER(ThreadId);
|
|
|
|
//
|
|
// Add the process/thread to the watching list
|
|
//
|
|
// LogInfo("Adding to watch list: Process Id: %x, Thread Id: %x", ProcessId, ThreadId);
|
|
|
|
//
|
|
// By default return FALSE to set handling the breakpoint to the user to the debugger
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* @brief This function makes sure to unset the RFLAGS.TF on next trigger of #DB
|
|
* on the target process/thread
|
|
* @param ProcessId
|
|
* @param ThreadId
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointRestoreTheTrapFlagOnceTriggered(UINT32 ProcessId, UINT32 ThreadId)
|
|
{
|
|
UINT32 Index;
|
|
BOOLEAN Result;
|
|
BOOLEAN SuccessfullyStored;
|
|
DEBUGGER_PROCESS_THREAD_INFORMATION ProcThrdInfo = {0};
|
|
|
|
//
|
|
// Form the process id and thread id into a 64-bit value
|
|
//
|
|
ProcThrdInfo.Fields.ProcessId = ProcessId;
|
|
ProcThrdInfo.Fields.ThreadId = ThreadId;
|
|
|
|
//
|
|
// Make sure, nobody is in the middle of modifying the list
|
|
//
|
|
SpinlockLock(&BreakpointCommandTrapListLock);
|
|
|
|
//
|
|
// *** Search the list of processes/threads for the current process's trap flag state ***
|
|
//
|
|
Result = BinarySearchPerformSearchItem((UINT64 *)&g_TrapFlagState.ThreadInformation[0],
|
|
g_TrapFlagState.NumberOfItems,
|
|
&Index,
|
|
ProcThrdInfo.AsUInt);
|
|
|
|
if (Result)
|
|
{
|
|
//
|
|
// It means that we already find this entry in the stored list
|
|
// so, just imply that the addition was successful (no need for extra addition)
|
|
//
|
|
SuccessfullyStored = TRUE;
|
|
goto Return;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Insert the thread into the list as the item is not already present
|
|
//
|
|
SuccessfullyStored = InsertionSortInsertItem((UINT64 *)&g_TrapFlagState.ThreadInformation[0],
|
|
&g_TrapFlagState.NumberOfItems,
|
|
MAXIMUM_NUMBER_OF_THREAD_INFORMATION_FOR_TRAPS,
|
|
&Index, // not used
|
|
ProcThrdInfo.AsUInt);
|
|
goto Return;
|
|
}
|
|
|
|
Return:
|
|
//
|
|
// Unlock the list modification lock
|
|
//
|
|
SpinlockUnlock(&BreakpointCommandTrapListLock);
|
|
|
|
return SuccessfullyStored;
|
|
}
|
|
|
|
/**
|
|
* @brief Check and handle debug breakpoint exceptions
|
|
*
|
|
* @param CoreId
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointCheckAndHandleDebugBreakpoint(UINT32 CoreId)
|
|
{
|
|
BOOLEAN TrapSetByDebugger;
|
|
PROCESSOR_DEBUGGING_STATE * DbgState = &g_DbgState[CoreId];
|
|
BOOLEAN HandledByDebuggerRoutines = TRUE;
|
|
|
|
//
|
|
// *** Check whether anything should be changed with trap-flags
|
|
// and also it indicates whether the debugger itself set this trap
|
|
// flag or it's not supposed to be set by the debugger ***
|
|
//
|
|
if (BreakpointCheckAndPerformActionsOnTrapFlags(HANDLE_TO_UINT32(PsGetCurrentProcessId()),
|
|
HANDLE_TO_UINT32(PsGetCurrentThreadId()),
|
|
&TrapSetByDebugger))
|
|
{
|
|
if (DbgState->ThreadOrProcessTracingDetails.DebugRegisterInterceptionState)
|
|
{
|
|
//
|
|
// This check was to show whether it is because of thread change detection or not
|
|
//
|
|
|
|
// This way of handling has a problem, if the user set to change
|
|
// the thread and instead of using 'g', it pressed the 'p' to
|
|
// set or a trap happens somewhere then will be ignored
|
|
// it because we don't know the origin of this debug breakpoint
|
|
// and it only happens on '.thread2' command, the correct way
|
|
// to handle it is to find the exact hw debug register that caused
|
|
// this vm-exit, but it's a really rare case, so we left it without
|
|
// handling this case
|
|
//
|
|
ThreadHandleThreadChange(DbgState);
|
|
}
|
|
else if (g_UserDebuggerState == TRUE &&
|
|
(g_IsWaitingForUserModeProcessEntryToBeCalled || g_IsWaitingForReturnAndRunFromPageFault))
|
|
{
|
|
//
|
|
// Handle for user-mode attaching mechanism
|
|
//
|
|
AttachingHandleEntrypointInterception(DbgState);
|
|
}
|
|
else if (g_KernelDebuggerState == TRUE)
|
|
{
|
|
//
|
|
// Here we added the handler for the kernel because we want
|
|
// stepping routines to work, even if the debugger masks the
|
|
// traps by using 'test trap off', so stepping still works
|
|
//
|
|
|
|
//
|
|
// Handle debug events (breakpoint, traps, hardware debug register when kernel
|
|
// debugger is attached)
|
|
//
|
|
KdHandleDebugEventsWhenKernelDebuggerIsAttached(DbgState, TrapSetByDebugger);
|
|
}
|
|
else if (g_UserDebuggerState == TRUE &&
|
|
UdHandleDebugEventsWhenUserDebuggerIsAttached(DbgState, TrapSetByDebugger))
|
|
{
|
|
//
|
|
// if the above function returns true, no need for further action
|
|
// it's handled in the user debugger
|
|
//
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Here it means that the trap is supposed to be handled by
|
|
// HyperDbg but, we couldn't find any routines that gonna
|
|
// handle it (it's probably an error)
|
|
//
|
|
HandledByDebuggerRoutines = FALSE;
|
|
LogError("Err, trap is supposed to be handled by the debugger, but none of routines handled it");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// *** it's not supposed to be handled by the debugger routines, the guest
|
|
// or the target debuggee throws a debug break (#DB) ***
|
|
//
|
|
|
|
//
|
|
// It means that it's not handled by the debugger routines
|
|
// By default HyperDbg intercepts all #DBs and break the debugger if
|
|
// it's attached to the debugger, otherwise injects to the guest VM
|
|
//
|
|
if (g_InterceptDebugBreaks)
|
|
{
|
|
//
|
|
// The user explicitly told the debugger not to intercept any
|
|
// traps (e.g., by using 'test trap off')
|
|
//
|
|
HandledByDebuggerRoutines = FALSE;
|
|
}
|
|
else if (g_KernelDebuggerState == TRUE)
|
|
{
|
|
//
|
|
// Handle debug events (breakpoint, traps, hardware debug register when kernel
|
|
// debugger is attached)
|
|
//
|
|
KdHandleDebugEventsWhenKernelDebuggerIsAttached(DbgState, TrapSetByDebugger);
|
|
}
|
|
else if (g_UserDebuggerState == TRUE &&
|
|
UdHandleDebugEventsWhenUserDebuggerIsAttached(DbgState, TrapSetByDebugger))
|
|
{
|
|
//
|
|
// if the above function returns true, no need for further action
|
|
// it's handled in the user debugger
|
|
//
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Inject to back to the guest as it's not either handled by the kernel debugger
|
|
// routines or the user debugger
|
|
//
|
|
HandledByDebuggerRoutines = FALSE;
|
|
}
|
|
}
|
|
|
|
return HandledByDebuggerRoutines;
|
|
}
|
|
|
|
/**
|
|
* @brief clears the 0xcc and removes the breakpoint
|
|
* @detail this function won't remove the descriptor from the list
|
|
* @param BreakpointDescriptor
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointClear(PDEBUGGEE_BP_DESCRIPTOR BreakpointDescriptor)
|
|
{
|
|
BYTE TargetMem = NULL_ZERO;
|
|
|
|
//
|
|
// Check if address is safe (only one byte for 0xcc)
|
|
//
|
|
if (!CheckAccessValidityAndSafety(BreakpointDescriptor->Address, sizeof(BYTE)))
|
|
{
|
|
//
|
|
// Double check if we can access it by physical address
|
|
//
|
|
MemoryMapperReadMemorySafeByPhysicalAddress(BreakpointDescriptor->PhysAddress,
|
|
(UINT64)&TargetMem,
|
|
sizeof(BYTE));
|
|
|
|
if (TargetMem != 0xcc)
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Apply the previous byte
|
|
//
|
|
MemoryMapperWriteMemorySafeByPhysicalAddress(BreakpointDescriptor->PhysAddress,
|
|
(UINT64)&BreakpointDescriptor->PreviousByte,
|
|
sizeof(BYTE));
|
|
|
|
//
|
|
// Set breakpoint to disabled
|
|
//
|
|
BreakpointDescriptor->Enabled = FALSE;
|
|
BreakpointDescriptor->AvoidReApplyBreakpoint = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* @brief Clears the breakpoint and remove the entry from the breakpoint list
|
|
* @param
|
|
*
|
|
* @return VOID
|
|
*/
|
|
VOID
|
|
BreakpointClearAndDeallocateMemory(PDEBUGGEE_BP_DESCRIPTOR BreakpointDesc)
|
|
{
|
|
//
|
|
// Clear the breakpoint
|
|
//
|
|
BreakpointClear(BreakpointDesc);
|
|
|
|
//
|
|
// Remove breakpoint from the list of breakpoints
|
|
//
|
|
RemoveEntryList(&BreakpointDesc->BreakpointsList);
|
|
|
|
//
|
|
// Uninitialize the breakpoint descriptor (safely)
|
|
//
|
|
PoolManagerFreePool((UINT64)BreakpointDesc);
|
|
}
|
|
|
|
/**
|
|
* @brief Check and reapply breakpoint
|
|
*
|
|
* @param DbgState The state of the debugger on the current core
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointCheckAndHandleReApplyingBreakpoint(PROCESSOR_DEBUGGING_STATE * DbgState)
|
|
{
|
|
BOOLEAN Result = FALSE;
|
|
|
|
if (DbgState->SoftwareBreakpointState != NULL)
|
|
{
|
|
BYTE BreakpointByte = 0xcc;
|
|
|
|
//
|
|
// MTF is handled
|
|
//
|
|
Result = TRUE;
|
|
|
|
//
|
|
// Restore previous breakpoint byte
|
|
//
|
|
MemoryMapperWriteMemorySafeByPhysicalAddress(
|
|
DbgState->SoftwareBreakpointState->PhysAddress,
|
|
(UINT64)&BreakpointByte,
|
|
sizeof(BYTE));
|
|
|
|
DbgState->SoftwareBreakpointState = NULL;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
/**
|
|
* @brief Check if the breakpoint vm-exit relates to 'bp' command or not
|
|
*
|
|
* @param DbgState The state of the debugger on the current core
|
|
* @param GuestRip
|
|
* @param Reason
|
|
* @param ChangeMtfState
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointCheckAndHandleDebuggerDefinedBreakpoints(PROCESSOR_DEBUGGING_STATE * DbgState,
|
|
UINT64 GuestRip,
|
|
DEBUGGEE_PAUSING_REASON Reason,
|
|
BOOLEAN ChangeMtfState)
|
|
{
|
|
CR3_TYPE GuestCr3 = {0};
|
|
BOOLEAN IsHandledByBpRoutines = FALSE;
|
|
PLIST_ENTRY TempList = 0;
|
|
UINT64 GuestRipPhysical = (UINT64)NULL;
|
|
DEBUGGER_TRIGGERED_EVENT_DETAILS TargetContext = {0};
|
|
BOOLEAN AvoidUnsetMtf = FALSE;
|
|
BOOLEAN IgnoreUserHandling = FALSE;
|
|
|
|
//
|
|
// ***** Check breakpoint for 'bp' command *****
|
|
//
|
|
|
|
//
|
|
// Find the current process cr3
|
|
//
|
|
GuestCr3.Flags = LayoutGetCurrentProcessCr3().Flags;
|
|
|
|
//
|
|
// Convert breakpoint to physical address
|
|
//
|
|
GuestRipPhysical = VirtualAddressToPhysicalAddressByProcessCr3((PVOID)GuestRip, GuestCr3);
|
|
|
|
//
|
|
// Iterate through the list of breakpoints
|
|
//
|
|
TempList = &g_BreakpointsListHead;
|
|
|
|
while (&g_BreakpointsListHead != TempList->Flink)
|
|
{
|
|
TempList = TempList->Flink;
|
|
PDEBUGGEE_BP_DESCRIPTOR CurrentBreakpointDesc = CONTAINING_RECORD(TempList, DEBUGGEE_BP_DESCRIPTOR, BreakpointsList);
|
|
|
|
if (CurrentBreakpointDesc->PhysAddress == GuestRipPhysical)
|
|
{
|
|
//
|
|
// It's a breakpoint by 'bp' command
|
|
//
|
|
IsHandledByBpRoutines = TRUE;
|
|
|
|
//
|
|
// First, we remove the breakpoint
|
|
//
|
|
MemoryMapperWriteMemorySafeByPhysicalAddress(GuestRipPhysical,
|
|
(UINT64)&CurrentBreakpointDesc->PreviousByte,
|
|
sizeof(BYTE));
|
|
|
|
//
|
|
// Now, halt the debuggee
|
|
//
|
|
TargetContext.Context = (PVOID)VmFuncGetLastVmexitRip(DbgState->CoreId);
|
|
|
|
//
|
|
// In breakpoints tag is breakpoint id, not event tag
|
|
//
|
|
if (Reason == DEBUGGEE_PAUSING_REASON_DEBUGGEE_SOFTWARE_BREAKPOINT_HIT)
|
|
{
|
|
TargetContext.Tag = CurrentBreakpointDesc->BreakpointId;
|
|
}
|
|
|
|
//
|
|
// Hint the debuggee about the length
|
|
//
|
|
DbgState->InstructionLengthHint = CurrentBreakpointDesc->InstructionLength;
|
|
|
|
//
|
|
// Check constraints
|
|
//
|
|
if ((CurrentBreakpointDesc->Pid == DEBUGGEE_BP_APPLY_TO_ALL_PROCESSES || CurrentBreakpointDesc->Pid == HANDLE_TO_UINT32(PsGetCurrentProcessId())) &&
|
|
(CurrentBreakpointDesc->Tid == DEBUGGEE_BP_APPLY_TO_ALL_THREADS || CurrentBreakpointDesc->Tid == HANDLE_TO_UINT32(PsGetCurrentThreadId())) &&
|
|
(CurrentBreakpointDesc->Core == DEBUGGEE_BP_APPLY_TO_ALL_CORES || CurrentBreakpointDesc->Core == DbgState->CoreId))
|
|
{
|
|
//
|
|
// Check if breakpoint should be removed after this hit or not
|
|
//
|
|
if (CurrentBreakpointDesc->RemoveAfterHit)
|
|
{
|
|
//
|
|
// One hit, we have to remove it
|
|
//
|
|
BreakpointClearAndDeallocateMemory(CurrentBreakpointDesc);
|
|
}
|
|
|
|
//
|
|
// Check if it needs to check for callbacks or not
|
|
//
|
|
if (CurrentBreakpointDesc->CheckForCallbacks)
|
|
{
|
|
//
|
|
// check callbacks
|
|
//
|
|
IgnoreUserHandling = BreakpointTriggerCallbacks(DbgState, HANDLE_TO_UINT32(PsGetCurrentProcessId()), HANDLE_TO_UINT32(PsGetCurrentThreadId()));
|
|
}
|
|
|
|
//
|
|
// Check if we need to handle the breakpoint by user or just ignore handling it
|
|
//
|
|
if (!IgnoreUserHandling && !g_InterceptBreakpoints && !g_InterceptBreakpointsAndEventsForCommandsInRemoteComputer)
|
|
{
|
|
//
|
|
// *** It's not safe to access CurrentBreakpointDesc anymore as the
|
|
// breakpoint might be removed ***
|
|
//
|
|
if (g_KernelDebuggerState)
|
|
{
|
|
KdHandleBreakpointAndDebugBreakpoints(DbgState,
|
|
Reason,
|
|
&TargetContext);
|
|
}
|
|
else if (g_UserDebuggerState)
|
|
{
|
|
UdHandleInstantBreak(DbgState, Reason, NULL);
|
|
}
|
|
else
|
|
{
|
|
LogInfo("Err, no debugger is attached to handle the breakpoint");
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Reset hint to instruction length
|
|
//
|
|
DbgState->InstructionLengthHint = 0;
|
|
|
|
//
|
|
// Check if we should re-apply the breakpoint after this instruction
|
|
// or not (in other words, is breakpoint still valid)
|
|
//
|
|
if (!CurrentBreakpointDesc->AvoidReApplyBreakpoint)
|
|
{
|
|
//
|
|
// We should re-apply the breakpoint on next mtf
|
|
//
|
|
DbgState->SoftwareBreakpointState = CurrentBreakpointDesc;
|
|
|
|
//
|
|
// As we want to continue debuggee, the MTF might arrive when the
|
|
// host finish executing it's time slice; thus, a clock interrupt
|
|
// or an IPI might be arrived and the next instruction is not what
|
|
// we expect. The following codes are added because we realized if the execution takes long then
|
|
// the execution might be switched to another routines, thus, MTF might conclude on
|
|
// another routine and we might (and will) trigger the same instruction soon
|
|
//
|
|
VmFuncEnableMtfAndChangeExternalInterruptState(DbgState->CoreId);
|
|
|
|
//
|
|
// Avoid unsetting MTF
|
|
//
|
|
AvoidUnsetMtf = TRUE;
|
|
}
|
|
|
|
//
|
|
// Do not increment rip
|
|
//
|
|
VmFuncSuppressRipIncrement(DbgState->CoreId);
|
|
|
|
//
|
|
// No need to iterate anymore
|
|
//
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (IsHandledByBpRoutines && ChangeMtfState)
|
|
{
|
|
VmFuncChangeMtfUnsettingState(DbgState->CoreId, AvoidUnsetMtf);
|
|
}
|
|
|
|
return IsHandledByBpRoutines;
|
|
}
|
|
|
|
/**
|
|
* @brief Handle breakpoint vm-exits (#BP)
|
|
*
|
|
* @param CoreId
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointHandleBreakpoints(UINT32 CoreId)
|
|
{
|
|
DEBUGGER_TRIGGERED_EVENT_DETAILS TargetContext = {0};
|
|
UINT64 GuestRip = 0;
|
|
PROCESSOR_DEBUGGING_STATE * DbgState = &g_DbgState[CoreId];
|
|
|
|
GuestRip = VmFuncGetRip();
|
|
|
|
//
|
|
// A breakpoint triggered and two things might be happened,
|
|
// first, a breakpoint is triggered randomly in the computer and
|
|
// we shouldn't do anything on it (won't change the instruction)
|
|
// second, the breakpoint is because of the 'bp' command, we should
|
|
// replace it with exact byte
|
|
//
|
|
|
|
//
|
|
// Check if the breakpoint is handled by the debugger routines
|
|
//
|
|
if (BreakpointCheckAndHandleDebuggerDefinedBreakpoints(DbgState,
|
|
GuestRip,
|
|
DEBUGGEE_PAUSING_REASON_DEBUGGEE_SOFTWARE_BREAKPOINT_HIT,
|
|
FALSE))
|
|
{
|
|
//
|
|
// The breakpoint is handled by the debugger routines
|
|
// so, we don't need to do anything else
|
|
//
|
|
return TRUE;
|
|
}
|
|
|
|
//
|
|
// re-inject #BP back to the guest if not handled by the hidden breakpoint
|
|
//
|
|
if (g_KernelDebuggerState)
|
|
{
|
|
//
|
|
// *** Kernel debugger is attached, let's halt everything ***
|
|
//
|
|
|
|
//
|
|
// To avoid the computer crash situation from the HyperDbg's breakpoint hitting while the interception is on
|
|
// we should always call BreakpointCheckAndHandleDebuggerDefinedBreakpoints first to handle the breakpoint
|
|
//
|
|
|
|
if (g_InterceptBreakpoints || g_InterceptBreakpointsAndEventsForCommandsInRemoteComputer)
|
|
{
|
|
//
|
|
// re-inject back to the guest as not handled if the interception is on and the breakpoint is not from the Hyperdbg's breakpoints
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// It's a random breakpoint byte
|
|
//
|
|
TargetContext.Context = (PVOID)GuestRip;
|
|
KdHandleBreakpointAndDebugBreakpoints(DbgState,
|
|
DEBUGGEE_PAUSING_REASON_DEBUGGEE_SOFTWARE_BREAKPOINT_HIT,
|
|
&TargetContext);
|
|
|
|
//
|
|
// Increment rip
|
|
//
|
|
VmFuncPerformRipIncrement(DbgState->CoreId);
|
|
|
|
//
|
|
// By default, we handle the random breakpoints if the kernel debugger is attached
|
|
//
|
|
return TRUE;
|
|
}
|
|
else if (g_UserDebuggerState)
|
|
{
|
|
//
|
|
// *** User debugger is attached, let's halt the process ***
|
|
//
|
|
|
|
//
|
|
// Check if it's a random breakpoint byte
|
|
//
|
|
if (UdHandleInstantBreak(DbgState,
|
|
DEBUGGEE_PAUSING_REASON_DEBUGGEE_SOFTWARE_BREAKPOINT_HIT,
|
|
NULL))
|
|
{
|
|
//
|
|
// if the above function returns true, it's handled in the user debugger
|
|
//
|
|
|
|
//
|
|
// Increment rip
|
|
//
|
|
VmFuncPerformRipIncrement(DbgState->CoreId);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
//
|
|
// By default, we won't handle the random (unrelated) breakpoints in the user debugger
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// *** re-inject back to the guest as not handled here ***
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
/**
|
|
* @brief writes the 0xcc and applies the breakpoint
|
|
* @detail this function won't remove the descriptor from the list
|
|
*
|
|
* @param BreakpointDescriptor
|
|
* @param SwitchToTargetMemoryLayout If TRUE, it will switch to the target memory layout
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointWrite(PDEBUGGEE_BP_DESCRIPTOR BreakpointDescriptor, BOOLEAN SwitchToTargetMemoryLayout)
|
|
{
|
|
BYTE PreviousByte = NULL_ZERO;
|
|
BYTE BreakpointByte = 0xcc; // int 3
|
|
|
|
//
|
|
// Check if address is safe (only one byte for 0xcc)
|
|
//
|
|
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
if (!CheckAccessValidityAndSafetyByProcessId(BreakpointDescriptor->Address, sizeof(BYTE), BreakpointDescriptor->Pid))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!CheckAccessValidityAndSafety(BreakpointDescriptor->Address, sizeof(BYTE)))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Read and save previous byte and save it to the descriptor
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
MemoryMapperReadMemoryUnsafe(
|
|
BreakpointDescriptor->Address,
|
|
&PreviousByte,
|
|
sizeof(BYTE),
|
|
BreakpointDescriptor->Pid);
|
|
}
|
|
else
|
|
{
|
|
MemoryMapperReadMemorySafeOnTargetProcess(
|
|
BreakpointDescriptor->Address,
|
|
&PreviousByte,
|
|
sizeof(BYTE));
|
|
}
|
|
|
|
//
|
|
// Store the previous byte
|
|
//
|
|
BreakpointDescriptor->PreviousByte = PreviousByte;
|
|
|
|
//
|
|
// Set breakpoint to enabled
|
|
//
|
|
BreakpointDescriptor->Enabled = TRUE;
|
|
BreakpointDescriptor->AvoidReApplyBreakpoint = FALSE;
|
|
|
|
//
|
|
// Apply the breakpoint
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
MemoryMapperWriteMemorySafeFromVmxNonRootyPhysicalAddress(BreakpointDescriptor->PhysAddress,
|
|
(PVOID)&BreakpointByte,
|
|
sizeof(BYTE));
|
|
}
|
|
else
|
|
{
|
|
MemoryMapperWriteMemorySafeByPhysicalAddress(BreakpointDescriptor->PhysAddress,
|
|
(UINT64)&BreakpointByte,
|
|
sizeof(BYTE));
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* @brief Remove all the breakpoints if possible
|
|
*
|
|
* @return VOID
|
|
*/
|
|
VOID
|
|
BreakpointRemoveAllBreakpoints()
|
|
{
|
|
PLIST_ENTRY TempList = 0;
|
|
|
|
//
|
|
// Iterate through the list of breakpoints
|
|
//
|
|
TempList = &g_BreakpointsListHead;
|
|
|
|
while (&g_BreakpointsListHead != TempList->Flink)
|
|
{
|
|
TempList = TempList->Flink;
|
|
PDEBUGGEE_BP_DESCRIPTOR CurrentBreakpointDesc = CONTAINING_RECORD(TempList, DEBUGGEE_BP_DESCRIPTOR, BreakpointsList);
|
|
|
|
//
|
|
// Clear and deallocate the breakpoint
|
|
//
|
|
BreakpointClearAndDeallocateMemory(CurrentBreakpointDesc);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Find entry of breakpoint descriptor from list
|
|
* of breakpoints by breakpoint id
|
|
* @param BreakpointId
|
|
*
|
|
* @return PDEBUGGEE_BP_DESCRIPTOR
|
|
*/
|
|
PDEBUGGEE_BP_DESCRIPTOR
|
|
BreakpointGetEntryByBreakpointId(UINT64 BreakpointId)
|
|
{
|
|
PLIST_ENTRY TempList = 0;
|
|
|
|
TempList = &g_BreakpointsListHead;
|
|
|
|
while (&g_BreakpointsListHead != TempList->Flink)
|
|
{
|
|
TempList = TempList->Flink;
|
|
PDEBUGGEE_BP_DESCRIPTOR CurrentBreakpointDesc = CONTAINING_RECORD(TempList, DEBUGGEE_BP_DESCRIPTOR, BreakpointsList);
|
|
|
|
if (CurrentBreakpointDesc->BreakpointId == BreakpointId)
|
|
{
|
|
return CurrentBreakpointDesc;
|
|
}
|
|
}
|
|
|
|
//
|
|
// We didn't find anything, so return null
|
|
//
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Find entry of breakpoint descriptor from list
|
|
* of breakpoints by address
|
|
* @param Address
|
|
*
|
|
* @return PDEBUGGEE_BP_DESCRIPTOR
|
|
*/
|
|
PDEBUGGEE_BP_DESCRIPTOR
|
|
BreakpointGetEntryByAddress(UINT64 Address)
|
|
{
|
|
PLIST_ENTRY TempList = 0;
|
|
|
|
TempList = &g_BreakpointsListHead;
|
|
|
|
while (&g_BreakpointsListHead != TempList->Flink)
|
|
{
|
|
TempList = TempList->Flink;
|
|
PDEBUGGEE_BP_DESCRIPTOR CurrentBreakpointDesc = CONTAINING_RECORD(TempList, DEBUGGEE_BP_DESCRIPTOR, BreakpointsList);
|
|
|
|
if (CurrentBreakpointDesc->Address == Address)
|
|
{
|
|
return CurrentBreakpointDesc;
|
|
}
|
|
}
|
|
|
|
//
|
|
// We didn't find anything, so return null
|
|
//
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* @brief Add new breakpoints
|
|
* @param BpDescriptor
|
|
* @param SwitchToTargetMemoryLayout
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointAddNew(PDEBUGGEE_BP_PACKET BpDescriptorArg, BOOLEAN SwitchToTargetMemoryLayout)
|
|
{
|
|
CR3_TYPE GuestCr3 = {0};
|
|
PDEBUGGEE_BP_DESCRIPTOR BreakpointDescriptor = NULL;
|
|
BOOLEAN IsAddress32Bit = FALSE;
|
|
|
|
//
|
|
// Find the current process cr3
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
//
|
|
// Check if the process id is valid or not
|
|
//
|
|
if (BpDescriptorArg->Pid != DEBUGGEE_BP_APPLY_TO_ALL_PROCESSES &&
|
|
!CommonIsProcessExist(BpDescriptorArg->Pid))
|
|
{
|
|
//
|
|
// Process id is invalid (Set the error)
|
|
//
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_INVALID_PROCESS_ID;
|
|
return FALSE;
|
|
}
|
|
|
|
GuestCr3.Flags = LayoutGetCr3ByProcessId(BpDescriptorArg->Pid).Flags;
|
|
}
|
|
else
|
|
{
|
|
GuestCr3.Flags = LayoutGetCurrentProcessCr3().Flags;
|
|
}
|
|
|
|
//
|
|
// *** Validate arguments ***
|
|
//
|
|
|
|
//
|
|
// Check if the core number is not invalid
|
|
//
|
|
if (BpDescriptorArg->Core != DEBUGGEE_BP_APPLY_TO_ALL_CORES &&
|
|
!CommonValidateCoreNumber(BpDescriptorArg->Core))
|
|
{
|
|
//
|
|
// Core is invalid (Set the error)
|
|
//
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_INVALID_CORE_ID;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Check if breakpoint already exists on list or not
|
|
//
|
|
if (BreakpointGetEntryByAddress(BpDescriptorArg->Address) != NULL)
|
|
{
|
|
//
|
|
// Address is already on the list (Set the error)
|
|
//
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_BREAKPOINT_ALREADY_EXISTS_ON_THE_ADDRESS;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Check if address is safe (only one byte for 0xcc)
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
if (!CheckAccessValidityAndSafetyByProcessId(BpDescriptorArg->Address, sizeof(BYTE), BpDescriptorArg->Pid))
|
|
{
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_EDIT_MEMORY_STATUS_INVALID_ADDRESS_BASED_ON_CURRENT_PROCESS;
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!CheckAccessValidityAndSafety(BpDescriptorArg->Address, sizeof(BYTE)))
|
|
{
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_EDIT_MEMORY_STATUS_INVALID_ADDRESS_BASED_ON_CURRENT_PROCESS;
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// On the debugger mode, we won't check for process id and thread id, if these arguments are invalid
|
|
// then the HyperDbg simply ignores the breakpoints but it makes the computer slow
|
|
// it just won't be triggered
|
|
//
|
|
|
|
//
|
|
// When we reach here means that the arguments are valid and address is
|
|
// safe to access (put 0xcc)
|
|
//
|
|
|
|
//
|
|
// Get the pre-allocated buffer
|
|
//
|
|
BreakpointDescriptor = (DEBUGGEE_BP_DESCRIPTOR *)
|
|
PoolManagerRequestPool(BREAKPOINT_DEFINITION_STRUCTURE, TRUE, sizeof(DEBUGGEE_BP_DESCRIPTOR));
|
|
|
|
if (BreakpointDescriptor == NULL)
|
|
{
|
|
//
|
|
// No pool ! Probably the user set more than MAXIMUM_BREAKPOINTS_WITHOUT_CONTINUE
|
|
// pools without IOCTL (continue)
|
|
//
|
|
BpDescriptorArg->Result = DEBUGGER_ERROR_MAXIMUM_BREAKPOINT_WITHOUT_CONTINUE;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Copy details of breakpoint to the descriptor structure
|
|
//
|
|
g_MaximumBreakpointId++;
|
|
BreakpointDescriptor->BreakpointId = g_MaximumBreakpointId;
|
|
BreakpointDescriptor->Address = BpDescriptorArg->Address;
|
|
BreakpointDescriptor->PhysAddress = VirtualAddressToPhysicalAddressByProcessCr3((PVOID)BpDescriptorArg->Address,
|
|
GuestCr3);
|
|
BreakpointDescriptor->Core = BpDescriptorArg->Core;
|
|
BreakpointDescriptor->Pid = BpDescriptorArg->Pid;
|
|
BreakpointDescriptor->Tid = BpDescriptorArg->Tid;
|
|
BreakpointDescriptor->RemoveAfterHit = BpDescriptorArg->RemoveAfterHit;
|
|
BreakpointDescriptor->CheckForCallbacks = BpDescriptorArg->CheckForCallbacks;
|
|
|
|
//
|
|
// Check whether address is 32-bit or 64-bit
|
|
//
|
|
if (BpDescriptorArg->Address & 0xff00000000000000)
|
|
{
|
|
//
|
|
// This is a kernel-base address and as the kernel is 64-bit, we assume it's a 64-bit address
|
|
//
|
|
IsAddress32Bit = FALSE;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// The address is not a kernel address, thus, we check whether the debuggee is running on user-mode
|
|
// or not
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
UserAccessIsWow64Process((HANDLE)BpDescriptorArg->Pid, &IsAddress32Bit);
|
|
}
|
|
else
|
|
{
|
|
IsAddress32Bit = KdIsGuestOnUsermode32Bit();
|
|
}
|
|
}
|
|
|
|
//
|
|
// Use length disassembler engine to get the instruction length
|
|
//
|
|
if (SwitchToTargetMemoryLayout)
|
|
{
|
|
BreakpointDescriptor->InstructionLength = (UINT16)DisassemblerLengthDisassembleEngineByProcessId(
|
|
(PVOID)BpDescriptorArg->Address,
|
|
IsAddress32Bit,
|
|
BpDescriptorArg->Pid);
|
|
}
|
|
else
|
|
{
|
|
BreakpointDescriptor->InstructionLength = (UINT16)DisassemblerLengthDisassembleEngineInVmxRootOnTargetProcess(
|
|
(PVOID)BpDescriptorArg->Address,
|
|
IsAddress32Bit);
|
|
}
|
|
|
|
//
|
|
// Breakpoints are enabled by default
|
|
//
|
|
BreakpointDescriptor->Enabled = TRUE;
|
|
|
|
//
|
|
// Now we should add the breakpoint to the list of breakpoints (LIST_ENTRY)
|
|
//
|
|
InsertHeadList(&g_BreakpointsListHead, &(BreakpointDescriptor->BreakpointsList));
|
|
|
|
//
|
|
// Apply the breakpoint
|
|
//
|
|
BreakpointWrite(BreakpointDescriptor, SwitchToTargetMemoryLayout);
|
|
|
|
//
|
|
// Show that operation was successful
|
|
//
|
|
BpDescriptorArg->Result = DEBUGGER_OPERATION_WAS_SUCCESSFUL;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* @brief List all breakpoints
|
|
*
|
|
* @return VOID
|
|
*/
|
|
VOID
|
|
BreakpointListAllBreakpoint()
|
|
{
|
|
BOOLEAN IsListEmpty = TRUE;
|
|
PLIST_ENTRY TempList = 0;
|
|
|
|
TempList = &g_BreakpointsListHead;
|
|
|
|
while (&g_BreakpointsListHead != TempList->Blink)
|
|
{
|
|
TempList = TempList->Blink;
|
|
PDEBUGGEE_BP_DESCRIPTOR CurrentBreakpointDesc = CONTAINING_RECORD(TempList, DEBUGGEE_BP_DESCRIPTOR, BreakpointsList);
|
|
|
|
if (IsListEmpty)
|
|
{
|
|
Log("Id Address Status\n");
|
|
Log("-- --------------- --------");
|
|
|
|
IsListEmpty = FALSE;
|
|
}
|
|
|
|
Log("\n%02x %016llx %s", CurrentBreakpointDesc->BreakpointId, CurrentBreakpointDesc->Address, CurrentBreakpointDesc->Enabled ? "enabled" : "disabled");
|
|
|
|
if (CurrentBreakpointDesc->Core != DEBUGGEE_BP_APPLY_TO_ALL_CORES)
|
|
{
|
|
Log(" core = %x ", CurrentBreakpointDesc->Core);
|
|
}
|
|
if (CurrentBreakpointDesc->Pid != DEBUGGEE_BP_APPLY_TO_ALL_PROCESSES)
|
|
{
|
|
Log(" pid = %x ", CurrentBreakpointDesc->Pid);
|
|
}
|
|
if (CurrentBreakpointDesc->Tid != DEBUGGEE_BP_APPLY_TO_ALL_THREADS)
|
|
{
|
|
Log(" tid = %x ", CurrentBreakpointDesc->Tid);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Check if the list is empty or not
|
|
//
|
|
if (IsListEmpty)
|
|
{
|
|
Log("Breakpoints list is empty");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief List of modify breakpoints
|
|
* @param ListOrModifyBreakpoints
|
|
* @param SwitchToTargetMemoryLayout
|
|
*
|
|
* @return BOOLEAN
|
|
*/
|
|
BOOLEAN
|
|
BreakpointListOrModify(PDEBUGGEE_BP_LIST_OR_MODIFY_PACKET ListOrModifyBreakpoints, BOOLEAN SwitchToTargetMemoryLayout)
|
|
{
|
|
PDEBUGGEE_BP_DESCRIPTOR BreakpointDescriptor = NULL;
|
|
|
|
if (ListOrModifyBreakpoints->Request == DEBUGGEE_BREAKPOINT_MODIFICATION_REQUEST_LIST_BREAKPOINTS)
|
|
{
|
|
BreakpointListAllBreakpoint();
|
|
}
|
|
else if (ListOrModifyBreakpoints->Request == DEBUGGEE_BREAKPOINT_MODIFICATION_REQUEST_ENABLE)
|
|
{
|
|
BreakpointDescriptor = BreakpointGetEntryByBreakpointId(ListOrModifyBreakpoints->BreakpointId);
|
|
|
|
if (BreakpointDescriptor == NULL)
|
|
{
|
|
//
|
|
// Breakpoint id is invalid
|
|
//
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_ERROR_BREAKPOINT_ID_NOT_FOUND;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Check to make sure that breakpoint is not already enabled
|
|
//
|
|
if (BreakpointDescriptor->Enabled)
|
|
{
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_ERROR_BREAKPOINT_ALREADY_ENABLED;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Set the breakpoint (without removing from list)
|
|
//
|
|
BreakpointWrite(BreakpointDescriptor, SwitchToTargetMemoryLayout);
|
|
}
|
|
else if (ListOrModifyBreakpoints->Request == DEBUGGEE_BREAKPOINT_MODIFICATION_REQUEST_DISABLE)
|
|
{
|
|
BreakpointDescriptor = BreakpointGetEntryByBreakpointId(ListOrModifyBreakpoints->BreakpointId);
|
|
|
|
if (BreakpointDescriptor == NULL)
|
|
{
|
|
//
|
|
// Breakpoint id is invalid
|
|
//
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_ERROR_BREAKPOINT_ID_NOT_FOUND;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Check to make sure that breakpoint is not already disabled
|
|
//
|
|
if (!BreakpointDescriptor->Enabled)
|
|
{
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_ERROR_BREAKPOINT_ALREADY_DISABLED;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Unset the breakpoint (without removing from list)
|
|
//
|
|
BreakpointClear(BreakpointDescriptor);
|
|
}
|
|
else if (ListOrModifyBreakpoints->Request == DEBUGGEE_BREAKPOINT_MODIFICATION_REQUEST_CLEAR)
|
|
{
|
|
BreakpointDescriptor = BreakpointGetEntryByBreakpointId(ListOrModifyBreakpoints->BreakpointId);
|
|
|
|
if (BreakpointDescriptor == NULL)
|
|
{
|
|
//
|
|
// Breakpoint id is invalid
|
|
//
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_ERROR_BREAKPOINT_ID_NOT_FOUND;
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Clear and deallocate the breakpoint
|
|
//
|
|
BreakpointClearAndDeallocateMemory(BreakpointDescriptor);
|
|
}
|
|
|
|
//
|
|
// Operation was successful
|
|
//
|
|
ListOrModifyBreakpoints->Result = DEBUGGER_OPERATION_WAS_SUCCESSFUL;
|
|
|
|
return TRUE;
|
|
}
|