HyperDbg/hyperdbg/libhyperdbg/code/debugger/commands/extension-commands/lbr.cpp

384 lines
13 KiB
C++

/**
* @file lbr.cpp
* @author Sina Karvandi (sina@hyperdbg.org)
* @brief !lbr command
* @details
* @version 0.19
* @date 2026-04-05
*
* @copyright This project is released under the GNU Public License v3.
*
*/
#include "pch.h"
//
// Global Variables
//
extern HANDLE g_DeviceHandle;
extern BOOLEAN g_IsHyperTraceModuleLoaded;
/**
* @brief help of the !lbr command
*
* @return VOID
*/
VOID
CommandLbrHelp()
{
ShowMessages("!lbr : performs operation for Last Branch Record (LBR).\n");
ShowMessages("for dumping LBR entries, you can use the '!lbrdump' command\n");
ShowMessages("syntax : \t!lbr [Function (string)]\n");
ShowMessages("syntax : \t!lbr [filter FilterOptions (string)]\n");
ShowMessages("\n");
ShowMessages("\t\te.g : !lbr enable\n");
ShowMessages("\t\te.g : !lbr disable\n");
ShowMessages("\t\te.g : !lbr flush\n");
ShowMessages("\n");
ShowMessages("\t\te.g : !lbr filter\n");
ShowMessages("\t\te.g : !lbr filter kernel jcc ind_jmp rel_jmp far\n");
ShowMessages("\t\te.g : !lbr filter kernel jcc return ind_jmp rel_jmp far\n");
ShowMessages("\t\te.g : !lbr filter kernel jcc ind_jmp rel_jmp\n");
ShowMessages("\t\te.g : !lbr filter user rel_call ind_call return far\n");
ShowMessages("\t\te.g : !lbr filter call_stack user\n");
ShowMessages("\t\te.g : !lbr filter call_stack kernel\n");
ShowMessages("\nlist of filter options: \n");
ShowMessages("\t kernel: do not capture at ring0\n");
ShowMessages("\t user: do not capture at ring > 0\n");
ShowMessages("\t jcc: do not capture conditional branches\n");
ShowMessages("\t rel_call: do not capture relative calls\n");
ShowMessages("\t ind_call: do not capture indirect calls\n");
ShowMessages("\t return: do not capture near returns\n");
ShowMessages("\t ind_jmp: do not capture indirect jumps\n");
ShowMessages("\t rel_jmp: do not capture relative jumps\n");
ShowMessages("\t far: do not capture far branches (only in legacy LBR. check docs for details)\n");
ShowMessages("\t other_branches: do not capture jmp/call ptr*, jmp/call m*, ret (0c8h), sys*, interrupts,\n");
ShowMessages("\t exceptions (other than debug exceptions), iret, int3, intn, into, tsx abort\n");
ShowMessages("\t eenter, eresume, eexit, aex, init, sipi, rsm (only in ARCH LBR. check docs for details)\n");
ShowMessages("\t call_stack: enable LBR stack to use LIFO filtering to capture call stack profile\n");
ShowMessages("\t not available on CPUs older than Haswell. for this item you can only specify the 'user'\n");
ShowMessages("\t or the 'kernel'. it prevents all types of branches except calls and rets\n");
ShowMessages("\t (no option): capture everything (default option)\n");
ShowMessages("\nnote 1: LBR is usually not supported (or is emulated) in nested virtualization (VM) environments\n");
ShowMessages("note 2: LBR will be disabled if there is a debug-break (#DB) condition, such as the trap flags or\n");
ShowMessages(" hardware debug registers (to learn how to mitigate this, check the documentation)\n");
}
/**
* @brief Send LBR requests
*
* @param LbrRequest
*
* @return VOID
*/
BOOLEAN
CommandLbrSendRequest(HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
BOOL Status;
ULONG ReturnedLength;
AssertShowMessageReturnStmt(g_IsHyperTraceModuleLoaded, g_DeviceHandle, ASSERT_MESSAGE_HYPERTRACE_NOT_LOADED, ASSERT_MESSAGE_DRIVER_NOT_LOADED, AssertReturnFalse);
//
// Send IOCTL
//
Status = DeviceIoControl(
g_DeviceHandle, // Handle to device
IOCTL_PERFORM_HYPERTRACE_LBR_OPERATION, // IO Control Code (IOCTL)
LbrRequest, // Input Buffer to driver.
SIZEOF_HYPERTRACE_LBR_OPERATION_PACKETS, // Input buffer length
LbrRequest, // Output Buffer from driver.
SIZEOF_HYPERTRACE_LBR_OPERATION_PACKETS, // Length of output buffer in bytes.
&ReturnedLength, // Bytes placed in buffer.
NULL // synchronous call
);
if (!Status)
{
ShowMessages("ioctl failed with code 0x%x\n", GetLastError());
return FALSE;
}
if (LbrRequest->KernelStatus == DEBUGGER_OPERATION_WAS_SUCCESSFUL)
{
return TRUE;
}
else
{
return FALSE;
}
}
/**
* @brief Request to perform an LBR operation
*
* @param LbrRequest
*
* @return BOOLEAN
*/
BOOLEAN
HyperDbgPerformLbrOperation(HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
return CommandLbrSendRequest(LbrRequest);
}
/**
* @brief Parses simple (no-argument) LBR operations: enable, disable, flush
*
* @param CommandTokens
* @param LbrRequest
*
* @return BOOLEAN TRUE if parsed successfully
*/
BOOLEAN
CommandLbrParseSimpleOperation(vector<CommandToken> CommandTokens, HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
if (CommandTokens.size() != 2)
{
return FALSE;
}
if (CompareLowerCaseStrings(CommandTokens.at(1), "enable"))
{
LbrRequest->LbrOperationType = HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_ENABLE;
}
else if (CompareLowerCaseStrings(CommandTokens.at(1), "disable"))
{
LbrRequest->LbrOperationType = HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_DISABLE;
}
else if (CompareLowerCaseStrings(CommandTokens.at(1), "flush"))
{
LbrRequest->LbrOperationType = HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_FLUSH;
}
else
{
return FALSE;
}
return TRUE;
}
/**
* @brief Check validity of the filter options in case of call_stack mode
*
* @param LbrRequest
* @return BOOLEAN TRUE if the filter options are valid in case of call_stack mode
*/
BOOLEAN
CommandLbrValidateCallStackFilterOptions(HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
if (LbrRequest->LbrFilterOptions & LBR_CALL_STACK)
{
//
// Call-stack mode should be used with branch type enabling configured to capture only CALLs (NEAR_REL_CALL and
// NEAR_IND_CALL) and RETs (NEAR_RET). if the user specifed LBR_REL_CALL | LBR_IND_CALL | LBR_RETURN then
// it is invalid
//
if ((LbrRequest->LbrFilterOptions & (LBR_REL_CALL | LBR_IND_CALL | LBR_RETURN)))
{
ShowMessages("err, invalid filter options for 'call_stack' mode. when the 'call_stack' is enabled,"
" 'rel_call', 'ind_call', and 'return' could not be specified as filter options\n\n");
return FALSE;
}
}
return TRUE;
}
/**
* @brief Parses the LBR filter operation and accumulates filter option flags
*
* @param CommandTokens
* @param LbrRequest
*
* @return BOOLEAN TRUE if parsed successfully
*/
BOOLEAN
CommandLbrParseFilterOperation(vector<CommandToken> CommandTokens, HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
LbrRequest->LbrOperationType = HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_FILTER;
LbrRequest->LbrFilterOptions = 0; // no options = capture everything
for (SIZE_T i = 2; i < CommandTokens.size(); i++)
{
if (CompareLowerCaseStrings(CommandTokens.at(i), "kernel"))
{
LbrRequest->LbrFilterOptions |= LBR_KERNEL;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "user"))
{
LbrRequest->LbrFilterOptions |= LBR_USER;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "jcc"))
{
LbrRequest->LbrFilterOptions |= LBR_JCC;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "rel_call"))
{
LbrRequest->LbrFilterOptions |= LBR_REL_CALL;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "ind_call"))
{
LbrRequest->LbrFilterOptions |= LBR_IND_CALL;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "return"))
{
LbrRequest->LbrFilterOptions |= LBR_RETURN;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "ind_jmp"))
{
LbrRequest->LbrFilterOptions |= LBR_IND_JMP;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "rel_jmp"))
{
LbrRequest->LbrFilterOptions |= LBR_REL_JMP;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "far") ||
CompareLowerCaseStrings(CommandTokens.at(i), "other_branch") ||
CompareLowerCaseStrings(CommandTokens.at(i), "other_branches"))
{
LbrRequest->LbrFilterOptions |= LBR_FAR_OTHER_BRANCHES;
}
else if (CompareLowerCaseStrings(CommandTokens.at(i), "call_stack"))
{
LbrRequest->LbrFilterOptions |= LBR_CALL_STACK;
}
else
{
ShowMessages("unknown filter option '%s'\n\n",
GetCaseSensitiveStringFromCommandToken(CommandTokens.at(i)).c_str());
return FALSE;
}
}
//
// Call-stack mode should be used with branch type enabling configured to capture only CALLs (NEAR_REL_CALL and
// NEAR_IND_CALL) and RETs (NEAR_RET). When configured in this manner, the LBR array emulates a call stack,
// where CALLs are "pushed" and RETs "pop" them off the stack. If other branch types (JCC, NEAR_*_JMP, or
// OTHER_BRANCH) are enabled for recording with call-stack mode, LBR behavior may be undefined, so we will
// mask out any branch type filters that are not CALLs or RETs when call-stack mode is requested to ensure
// we are correctly emulating a call stack and avoiding undefined behavior
//
//
// If it is call_stack then we only keep the user and kernel bit and filter all branch types
// except calls and rets to ensure we are only capturing call stack profile
//
if (LbrRequest->LbrFilterOptions & LBR_CALL_STACK)
{
//
// Validate the filter options in case of call_stack mode
//
if (CommandLbrValidateCallStackFilterOptions(LbrRequest) == FALSE)
{
return FALSE;
}
//
// Preserve only the user/kernel privilege bits from the original options,
// then apply the mandatory call stack base flags
//
LbrRequest->LbrFilterOptions = LBR_CALL_STACK_BASE_FLAGS | (LbrRequest->LbrFilterOptions & (LBR_KERNEL | LBR_USER));
}
return TRUE;
}
/**
* @brief Displays the success message appropriate for the completed LBR operation
*
* @param LbrRequest
*
* @return VOID
*/
VOID
CommandLbrShowSuccessMessage(const HYPERTRACE_LBR_OPERATION_PACKETS * LbrRequest)
{
switch (LbrRequest->LbrOperationType)
{
case HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_ENABLE:
ShowMessages("LBR enabled successfully\n");
break;
case HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_DISABLE:
ShowMessages("LBR disabled successfully\n");
break;
case HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_FLUSH:
ShowMessages("LBR branches are flushed\n");
break;
case HYPERTRACE_LBR_OPERATION_REQUEST_TYPE_FILTER:
ShowMessages("LBR filter options are updated successfully\n");
break;
default:
ShowMessages("unknown LBR operation type\n");
break;
}
}
/**
* @brief !lbr command handler
*
* @param CommandTokens
* @param Command
*
* @return VOID
*/
VOID
CommandLbr(vector<CommandToken> CommandTokens, string Command)
{
HYPERTRACE_LBR_OPERATION_PACKETS LbrRequest = {0};
BOOLEAN ParseResult = FALSE;
if (CommandTokens.size() == 1)
{
ShowMessages("incorrect use of the '%s'\n\n",
GetCaseSensitiveStringFromCommandToken(CommandTokens.at(0)).c_str());
CommandLbrHelp();
return;
}
//
// Dispatch to the appropriate parser based on the subcommand
//
if (CompareLowerCaseStrings(CommandTokens.at(1), "enable") ||
CompareLowerCaseStrings(CommandTokens.at(1), "disable") ||
CompareLowerCaseStrings(CommandTokens.at(1), "flush"))
{
ParseResult = CommandLbrParseSimpleOperation(CommandTokens, &LbrRequest);
}
else if (CompareLowerCaseStrings(CommandTokens.at(1), "filter"))
{
ParseResult = CommandLbrParseFilterOperation(CommandTokens, &LbrRequest);
}
else
{
ParseResult = FALSE;
}
if (!ParseResult)
{
ShowMessages("incorrect use of the '%s'\n\n",
GetCaseSensitiveStringFromCommandToken(CommandTokens.at(0)).c_str());
CommandLbrHelp();
return;
}
//
// Check if the HyperTrace module is loaded, as it is required for LBR operations
//
AssertShowMessageReturnStmt(g_IsHyperTraceModuleLoaded, g_DeviceHandle, ASSERT_MESSAGE_HYPERTRACE_NOT_LOADED, ASSERT_MESSAGE_DRIVER_NOT_LOADED, AssertReturn);
//
// Send the LBR operation request
//
if (CommandLbrSendRequest(&LbrRequest))
{
CommandLbrShowSuccessMessage(&LbrRequest);
}
else
{
ShowErrorMessage(LbrRequest.KernelStatus);
}
}