diff --git a/CAPE/Trace.c b/CAPE/Trace.c index 62b45602..82d010b4 100644 --- a/CAPE/Trace.c +++ b/CAPE/Trace.c @@ -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)) @@ -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 @@ -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/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::[:]:).\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) diff --git a/config.c b/config.c index 4b5ba7bf..7b875917 100644 --- a/config.c +++ b/config.c @@ -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) diff --git a/config.h b/config.h index 2cbf2a54..0cb742b7 100644 --- a/config.h +++ b/config.h @@ -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; diff --git a/docs/configuration.md b/docs/configuration.md index 36b7750a..532ebb8e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 diff --git a/hook_misc.c b/hook_misc.c index 3522dd41..011b16a6 100644 --- a/hook_misc.c +++ b/hook_misc.c @@ -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);