Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -1403,6 +1403,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 @@ -284,6 +284,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 @@ -124,6 +124,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