Skip to content
Open
124 changes: 123 additions & 1 deletion CAPE/Trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ PVOID GetRegister(PCONTEXT Context, char* RegString)
else if (!strnicmp(RegString, "r11", 3))
Register = (PVOID)Context->R11;
else if (!strnicmp(RegString, "r12", 3))
Register = (PVOID)Context->R13;
Register = (PVOID)Context->R12;
else if (!strnicmp(RegString, "r13", 3))
Register = (PVOID)Context->R13;
else if (!strnicmp(RegString, "r14", 3))
Expand Down Expand Up @@ -1132,6 +1132,50 @@ BOOL DoStepOver(PCHAR FunctionName)
return FALSE;
}

// Resolve one operand token for the 'If:' conditional action to a value.
// Token may be Src/Dst (the breakpointed instruction's operands, dereferenced for memory),
// a register, [reg+off] (dereferenced), or an immediate. $string values are already resolved
// to addresses upstream by ParseOptionLine, so they arrive here as immediates.
PVOID ResolveIfOperand(PCONTEXT Context, _DecodedInst DecodedInstruction, PCHAR Token)
{
if (!Token || !*Token)
return NULL;

if (!stricmp(Token, "Src"))
{
PCHAR Comma = strchr(DecodedInstruction.operands.p, ',');
if (Comma)
{
*Comma = 0;
PVOID Value = GetOperand(Context, DecodedInstruction.operands.p);
*Comma = ',';
return Value;
}
return GetOperand(Context, DecodedInstruction.operands.p);
}
if (!stricmp(Token, "Dst"))
{
PCHAR Comma = strchr(DecodedInstruction.operands.p, ',');
if (Comma)
return GetOperand(Context, Comma + 2);
return NULL;
}
if (strchr(Token, '['))
return GetOperand(Context, Token);

PVOID Reg = GetRegister(Context, Token);
if (Reg)
return Reg;

char *endptr;
errno = 0;
unsigned long long Imm = _strtoui64(Token, &endptr, 0);
if (!errno && endptr != Token)
return (PVOID)(DWORD_PTR)Imm;

return NULL;
}

void ActionDispatcher(struct _EXCEPTION_POINTERS* ExceptionInfo, _DecodedInst DecodedInstruction, PCHAR Action)
{
// This could be further optimised per action but this is safe at least
Expand Down Expand Up @@ -1548,6 +1592,84 @@ void ActionDispatcher(struct _EXCEPTION_POINTERS* ExceptionInfo, _DecodedInst De
WriteRet(ExceptionInfo->ContextRecord);
DebuggerOutput("\nActionDispatcher: ret written.\n");
}
else if (!strnicmp(Action, "If:", 3))
{
// Generic conditional action: If:<lhs>:<op>[:<rhs>]:<action>
// lhs/rhs : Src|Dst (this instruction's operands), a register, [reg+off], $string (a VA), or immediate
// op : ptr z nz (unary) | eq ne gt lt ge le (binary)
// action : any existing cape action + its own :param, run only when the condition holds
// If the wrapped action does not itself redirect control flow, the guarded instruction is skipped
// so a flag/register change stands in for it (e.g. a set ZF survives to a following jz).
char Buf[MAX_PATH];
strncpy(Buf, Action + 3, sizeof(Buf) - 1);
Buf[sizeof(Buf) - 1] = 0;

PCHAR LhsTok = Buf;
PCHAR OpTok = strchr(LhsTok, ':');
PCHAR RhsTok = NULL, ActionTok = NULL;
if (OpTok)
{
*OpTok++ = 0;
PCHAR Rest = strchr(OpTok, ':');
if (Rest)
{
*Rest++ = 0;
BOOL Binary = stricmp(OpTok, "ptr") && stricmp(OpTok, "z") && stricmp(OpTok, "nz");
if (Binary)
{
PCHAR AfterRhs = strchr(Rest, ':');
if (AfterRhs)
{
*AfterRhs++ = 0;
RhsTok = Rest;
ActionTok = AfterRhs;
}
}
else
ActionTok = Rest;
}
}

if (!ActionTok)
DebuggerOutput("ActionDispatcher: If - malformed '%s' (expected If:<lhs>:<op>[:<rhs>]:<action>).\n", Action);
else
{
ULONG_PTR L = (ULONG_PTR)ResolveIfOperand(ExceptionInfo->ContextRecord, DecodedInstruction, LhsTok);
ULONG_PTR R = RhsTok ? (ULONG_PTR)ResolveIfOperand(ExceptionInfo->ContextRecord, DecodedInstruction, RhsTok) : 0;
BOOL Cond = FALSE;
#ifdef _WIN64
if (!stricmp(OpTok, "ptr")) Cond = (L > 0x10000 && L < 0x00007FFFFFFFFFFFULL);
#else
if (!stricmp(OpTok, "ptr")) Cond = (L > 0x10000 && L < 0x80000000UL);
#endif
else if (!stricmp(OpTok, "z")) Cond = (L == 0);
else if (!stricmp(OpTok, "nz")) Cond = (L != 0);
else if (!stricmp(OpTok, "eq")) Cond = (L == R);
else if (!stricmp(OpTok, "ne")) Cond = (L != R);
else if (!stricmp(OpTok, "gt")) Cond = (L > R);
else if (!stricmp(OpTok, "lt")) Cond = (L < R);
else if (!stricmp(OpTok, "ge")) Cond = (L >= R);
else if (!stricmp(OpTok, "le")) Cond = (L <= R);
else DebuggerOutput("ActionDispatcher: If - unknown op '%s'.\n", OpTok);

DebuggerOutput("ActionDispatcher: If %s(0x%p, 0x%p) -> %d, action '%s'.\n", OpTok, (PVOID)L, (PVOID)R, Cond, ActionTok);
if (Cond)
{
#ifdef _WIN64
QWORD RipBefore = ExceptionInfo->ContextRecord->Rip;
#else
DWORD RipBefore = ExceptionInfo->ContextRecord->Eip;
#endif
ActionDispatcher(ExceptionInfo, DecodedInstruction, ActionTok);
#ifdef _WIN64
if (ExceptionInfo->ContextRecord->Rip == RipBefore)
#else
if (ExceptionInfo->ContextRecord->Eip == RipBefore)
#endif
SkipInstruction(ExceptionInfo->ContextRecord);
}
}
}
else if (!strnicmp(Action, "GoTo", 4))
{
if (Target)
Expand Down
5 changes: 5 additions & 0 deletions config.c
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,11 @@ void parse_config_line(char* line)
else
DebugOutput("Scans/dumps while loader lock held disabled.\n");
}
else if (!stricmp(key, "loaderlock-settle")) {
g_config.loaderlock_settle = value[0] == '1';
if (g_config.loaderlock_settle)
DebugOutput("Loader-lock settle (yield in loader hooks) enabled.\n");
}
else if (!stricmp(key, "syscall")) {
g_config.syscall = value[0] == '1';
if (g_config.syscall)
Expand Down
3 changes: 3 additions & 0 deletions config.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ struct _g_config {
// Allow scans/dumps with loader lock held
int loaderlock_scans;

// Yield in loader hooks while loader lock held (timing fix for trojanized sideload DLLs)
int loaderlock_settle;

// Specify custom trace stepping behavior
int stepmode;

Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ They are typically defined in the analysis configuration file (e.g., `config.ini
| `base-on-caller` | Boolean | Base breakpoints on new calling regions. |
| `file-offsets` | Boolean | Interpret breakpoints as file offsets instead of RVAs. |
| `loaderlock` | Boolean | Allow scans/dumps while the Loader Lock is held. |
| `loaderlock-settle` | Boolean | Yield in loader hooks while the Loader Lock is held. Timing fix for trojanized sideload DLLs (e.g. AxolotlLoader) whose DllMain bootstrap races a dispatch-table slot; opt-in per-sample. |
| `snaps` | Boolean | Enable Windows Loader Snaps output (LdrSnap). |

## Target Specific
Expand Down
8 changes: 8 additions & 0 deletions hook_misc.c
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,14 @@ HOOKDEF(NTSTATUS, WINAPI, LdrGetProcedureAddressForCaller,
ret = 0;
}

// Opt-in per-sample via YARA cape_options (loaderlock-settle=1). Trojanized sideload DLLs
// (AxolotlLoader/dui70.dll) crash under monitoring because a reentrant DllMain bootstrap re-clobbers a
// dispatch-table slot back to a -1 sentinel mid-resolver, so the consumer calls slot[0]==-1 (RIP=~0).
// The resolver runs with the loader lock released, so a loader_lock_held() gate never covered it; yield
// on every resolution while active to spread the bootstrap/resolver timing apart and avoid the clobber.
if (g_config.loaderlock_settle)
Sleep(1);

LOQ_ntstatus("system", "opSiP", "ModuleName", get_basename_of_module(ModuleHandle), "ModuleHandle", ModuleHandle,
"FunctionName", FunctionName != NULL ? FunctionName->Length : 0, FunctionName != NULL ? FunctionName->Buffer : NULL,
"Ordinal", Ordinal, "FunctionAddress", FunctionAddress);
Expand Down