diff --git a/CAPE/YaraHarness.h b/CAPE/YaraHarness.h index c5ebcafa..31993194 100644 --- a/CAPE/YaraHarness.h +++ b/CAPE/YaraHarness.h @@ -2,7 +2,12 @@ #include +// The libyara backend (YaraHarness.c) needs libyara's header; the YARA-X +// backend (YaraHarnessX.c, built when CAPE_USE_YARA_X is defined) does not, +// and nothing in this header depends on a YR_* type. +#ifndef CAPE_USE_YARA_X #include "yara.h" +#endif typedef struct { diff --git a/CAPE/YaraHarnessX.c b/CAPE/YaraHarnessX.c new file mode 100644 index 00000000..9eb2b5bf --- /dev/null +++ b/CAPE/YaraHarnessX.c @@ -0,0 +1,1013 @@ +/* +CAPE - Config And Payload Extraction + +This program is free software : you can redistribute it and / or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program.If not, see . +*/ + +// +// YARA-X backend for the CAPE in-monitor scanner. +// +// Drop-in replacement for YaraHarness.c: exposes the identical public API +// (YaraHarness.h) but is implemented against the YARA-X C API (yara_x.h). +// Exactly ONE of YaraHarness.c / YaraHarnessX.c is compiled into capemon - +// selected in the project file. This file is built when the project defines +// CAPE_USE_YARA_X. +// +// Behavioural parity notes vs the libyara backend: +// * "cape_options" string metadata drives dynamic config exactly as before; +// $pattern references in option lines are resolved to matched offsets. +// * Compiled-rule cache uses a distinct filename (capemon.yrx) because the +// serialised blob format differs and is locked to the YARA-X version. +// * YARA-X scanners are single-threaded objects, so this backend keeps one +// scanner per thread (YARA-X explicitly supports sharing the YRX_RULES +// across threads) plus a re-entrancy guard. +// + +//#define DEBUG_COMMENTS +#include +#include +#include +#include +#include "Shlwapi.h" +#include "CAPE.h" +#include "Debugger.h" +#include "YaraHarness.h" +#include "..\config.h" +#include "..\alloc.h" + +#include "yara_x.h" + +extern void DebugOutput(_In_ LPCTSTR lpOutputString, ...); +extern void ErrorOutput(_In_ LPCTSTR lpOutputString, ...); +extern BOOL SetInitialBreakpoints(PVOID ImageBase), DumpRegion(PVOID Address); +extern BOOL remove_dll_range(ULONG_PTR addr); +extern char Action0[MAX_PATH], Action1[MAX_PATH], Action2[MAX_PATH], Action3[MAX_PATH]; +extern void parse_config_line(char* line); +extern int ReverseScanForNonZero(PVOID Buffer, SIZE_T Size); +extern SIZE_T GetAccessibleSize(PVOID Buffer); +extern char *our_dll_path; +extern BOOL BreakpointsHit, TraceRunning; + +static YRX_RULES* Rules = NULL; +BOOL YaraActivated, YaraLogging; +#ifdef _WIN64 +extern PVOID LdrpInvertedFunctionTableSRWLock; +#endif + +static char NewLine[MAX_PATH]; + +// --- per-thread scanner state ------------------------------------------------ +// YRX_SCANNER is not thread-safe and is stateful across a scan, so each thread +// gets its own, lazily created from the shared YRX_RULES. A registry lets +// YaraShutdown() destroy them before the rules (required ordering). +typedef struct _ScannerNode { + YRX_SCANNER* Scanner; + struct _ScannerNode* Next; +} ScannerNode; + +static ScannerNode* ScannerList = NULL; +static CRITICAL_SECTION ScannerLock; +static BOOL ScannerLockInit = FALSE; + +static __declspec(thread) YRX_SCANNER* t_Scanner = NULL; +static __declspec(thread) int t_Scanning = 0; + +// t_Scanner is thread-local, so YaraShutdown() (running on one thread) cannot +// clear other threads' cached pointers before it destroys the YRX_SCANNER / +// YRX_RULES they point at. Every scan takes this lock shared for its duration; +// YaraShutdown() takes it exclusive before destroying anything, which blocks +// until all in-flight scans on every thread have returned. +static SRWLOCK ScanShutdownLock = SRWLOCK_INIT; + +// Upper bound on the pattern matches materialised per rule hit. Config rules +// carry only a handful; this just caps a pathological rule. +#define MAX_RULE_MATCHES 512 +#define MATCH_IDENT_MAX 96 + +typedef struct { + char Identifier[MATCH_IDENT_MAX]; + size_t Offset; + size_t Length; +} MatchInfo; + +typedef struct { + MatchInfo Items[MAX_RULE_MATCHES]; + int Count; + char CurIdentifier[MATCH_IDENT_MAX]; +} MatchCollector; + +char InternalYara[] = + "rule capemon" + "{strings:$hash = {d3 b9 46 1d 9a 14 bc 44 a1 61 c3 47 6a 0e 35 90 00 2c 28 81 dc a0 36 dc 2c 92 0c 7c b6 84 39 59}" + "condition:all of them}" +#ifdef _WIN64 + "rule vDbgPrintExWithPrefixInternal" + "{strings:$10_0_26100_3476 = {48 8B C4 48 89 58 08 48 89 68 10 48 89 70 18 48 89 78 20 41 54 41 56 41 57 48 83 EC 40 44 8A BC 24}" + "$function = {40 55 53 56 41 54 41 55 41 56 41 57 48 81 EC 20 01 00 00 48 8D 6C 24 20 48 8B 05 [4] 48 33 C5 48 89 85 ?? 00 00 00 4C 89 4D ?? 44 89 45 ?? 44 8B E2 89 55 ?? 48 8B D1 48 89 4D ?? 48 8B 85}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule FindFixAndRun" + "{strings:$function = {48 89 5C 24 10 48 89 74 24 18 57 41 54 41 55 41 56 41 57 48 81 EC [80-110] 85 C0 0F 88 [2] 00 00 49 8B 4? 70 66 83 (78|79) 02 3A 0F 84 [2] 00 00 48 8D 54 24 20 49 8B CE E8}" + "condition:uint16(0) == 0x5a4d and any of them}" +#else + "rule vDbgPrintExWithPrefixInternal" + "{strings:$function = {68 90 00 00 00 68 [4] E8 [4] 89 95 [4] 89 8D [4] 8B 45 ?? 89 85 [4] 8B 45 ?? 89 85 [4] 64 A1 18 00 00 00 89 45 ?? 83 FA FF 0F 84}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule FindFixAndRun" + "{strings:$function = {8B FF 55 8B EC 6A FE 68 [4] 68 [4] 64 A1 00 00 00 00 50 81 EC [90-96] 00 0F 84 [4] B8 E7 7F 00 00 50 8D 8D [4] E8 [4] 85 C0 0F 88 [4] 8B 4? 38 66 83 7? 02 3A 0F 84}" + "condition:uint16(0) == 0x5a4d and any of them}" +#endif + "rule RtlInsertInvertedFunctionTable" + "{strings:$10_0_26100_3476 = {48 8D 0D [4] 49 F7 D8 48 8B F8 1B DB 23 5C 24 ?? E8 [4] 33 C9 E8}" + "$10_0_19041_662 = {48 8D 0D [4] E8 [4] [7] 8B 44 24 ?? 44 8B CB 4C 8B 44 24 ?? 48 8B D7 89 44 24 ?? E8}" + "$10_0_18362_1350 = {48 8D 0D [4] 33 D2 85 C0 48 0F 48 DA E8 [4] 33 C9 E8 [4] 8B 44 24 ?? 44 8B CF 4C 8B C3 89 44 24 ?? 48 8B D6 E8}" + "$10_0_10240_16384 = {48 8D 0D [4] 48 8B E8 E8 [4] 33 C9 E8 [4] 8B 15 [4] 3B 15 [4] 0F 84}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule LdrpCallInitRoutine" + "{strings:$10_0_26100_3476 = {40 53 56 57 41 54 41 55 [65-85] 84 03 FE 7F}" + "$function = {55 8B EC 56 57 53 8B F4 [0-2] FF 75 14 FF 75 10 FF 75 0C FF 55 08 8B E6 5B 5F 5E 5D C2 10 00}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_ExecQuery" + "{strings:$function = {4C 8B DC 56 57 41 54 41 56 41 57 48 83 EC 60 49 C7 43 B8 FE FF FF FF 49 89 5B 10 49 89 6B 18 45 8B E1 4D 8B F0 4C 8B F9 48 8B 41 08 48 83 78 20 00 0F 84}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_ExecMethod" + "{strings:$function = {48 8B C4 56 57 41 54 41 56 41 57 48 83 EC 70 48 C7 40 B8 FE FF FF FF 48 89 58 10 48 89 68 18 45 8B E1 4D 8B F0 48 8B EA 4C 8B F9 48 8B 41 08 48 83 78 20 00 75 0A B8 08 01 01 80 E9}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_ExecQueryAsync" + "{strings:$function = {4C 8B DC 56 57 41 54 41 56 41 57 48 83 EC 60 49 C7 43 B8 FE FF FF FF 49 89 5B 10 49 89 6B 18 45 8B E1 4D 8B F0 48 8B E9 48 8B 41 08 48 83 78 20 00 0F 84 [4] 49 83 63 08 00 4D 8D 43 08 E8}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_ExecMethodAsync" + "{strings:$function = {48 8B C4 57 41 54 41 55 41 56 41 57 48 83 EC 60 48 C7 40 B8 FE FF FF FF 48 89 58 10 48 89 68 18 48 89 70 20 45 8B E9 4D 8B F8 4C 8B F2 48 8B E9 48 8B 41 08 48 83 78 20 00 75 0A B8 08 01 01 80 E9}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_GetObject" + "{strings:$function = {4C 8B DC 56 57 41 54 41 56 41 57 48 83 EC 50 49 C7 43 ?? FE FF FF FF 49 89 5B 10 49 89 6B 18 4D 8B F9 45 8B E0 48 8B EA 4C 8B F1 48 8B 41 08 48 83 78 20 00 0F 84 12 AB 02 00 49 83 63 08 00 4D 8D 43 08 E8}" + "condition:uint16(0) == 0x5a4d and any of them}" + "rule WMI_GetObjectAsync" + "{strings:$function = {48 8B C4 56 57 41 54 41 56 41 57 48 83 EC 40 48 C7 40 C8 FE FF FF FF 48 89 58 10 48 89 68 18 4D 8B F9 45 8B E0 48 8B EA 48 8B F1 48 8B 41 08 48 83 78 20 00 75 0A B8 08 01 01 80 E9}" + "condition:uint16(0) == 0x5a4d and any of them}"; + +static void ScannerError(enum YRX_RESULT Result, const char* Where) +{ + const char* detail; + + switch (Result) + { + case YRX_SUCCESS: + return; + case YRX_SCAN_TIMEOUT: + DebugOutput("YaraScan (%s): scan timed out\n", Where); + return; + case YRX_SCAN_ERROR: + detail = yrx_last_error(); + DebugOutput("YaraScan (%s): scan error%s%s\n", Where, detail ? ": " : "", detail ? detail : ""); + return; + case YRX_SERIALIZATION_ERROR: + DebugOutput("YaraScan (%s): compiled-rule (de)serialisation failed - rules will be recompiled\n", Where); + return; + case YRX_SYNTAX_ERROR: + detail = yrx_last_error(); + DebugOutput("YaraScan (%s): rule syntax error%s%s\n", Where, detail ? ": " : "", detail ? detail : ""); + return; + default: + detail = yrx_last_error(); + DebugOutput("YaraScan (%s): error %d%s%s\n", Where, (int)Result, detail ? ": " : "", detail ? detail : ""); + return; + } +} + +// Copies a possibly non-NUL-terminated (ptr,len) identifier into Dst, ensuring +// a leading '$' so it compares the same way libyara's identifiers did. +// (YARA-X already returns "$name", so the prefix is normally a no-op.) +static void CopyPatternIdentifier(char* Dst, size_t DstSize, const uint8_t* Src, size_t SrcLen) +{ + size_t di = 0, si = 0; + + if (DstSize == 0) + return; + + if (!Src || SrcLen == 0) + { + Dst[0] = 0; + return; + } + + if (Src[0] != '$' && di < DstSize - 1) + Dst[di++] = '$'; + + while (si < SrcLen && di < DstSize - 1) + Dst[di++] = (char)Src[si++]; + + Dst[di] = 0; +} + +void ParseOptionLine(char* Line, char* Identifier, size_t MatchOffset, size_t MatchLength, void* user_data) +{ + char *Value, *Key, *p, *q, *r, c = 0; + ULONG_PTR delta = 0; + SIZE_T ValueLength = 0; + + if (!Line || !Identifier) + return; + + p = strchr(Line, '$'); + if (!p) + return; + + p = strchr(Line, '='); + if (!p) + return; + + r = strchr(p, ':'); + if (r && *(r + 1) == '$') + Value = r + 1; + else + Value = p + 1; + q = strchr(Value, '+'); + if (q) + { + delta = strtoul(q + 1, NULL, 0); + if (*(q - 1) == '*') + delta += MatchLength - 1; + } + else + { + q = strchr(Value, '-'); + if (q) + { + delta = -(int)strtoul(q + 1, NULL, 0); + if (*(q - 1) == '*') + delta += MatchLength - 1; + } + } + if (q) + { + ValueLength = (SIZE_T)(DWORD_PTR)(q - (DWORD_PTR)Value); + if (*(q - 1) == '*') + ValueLength--; + } + else + ValueLength = (SIZE_T)strlen(Value); + + if (*(Value + ValueLength - 1) == '*') + { + ValueLength--; + delta += MatchLength - 1; + } + + SIZE_T IdentifierLength = strlen(Identifier); + if (strncmp(Value, Identifier, IdentifierLength < ValueLength ? IdentifierLength : ValueLength)) + return; + + Key = Line; + if (r && *(r + 1) == '$') + { + c = *r; + *r = 0; + } + else + { + c = *p; + *p = 0; + } + + if (_strnicmp(Line, "bp", 2) && strncmp(Line, "br", 2) && strncmp(Line, "sysbp", 5)) + delta += (ULONG_PTR)user_data; + + memset(NewLine, 0, sizeof(NewLine)); + if (r) + sprintf(NewLine, "%s%c0x%p%s", Key, c, (PUCHAR)MatchOffset + delta, r); + else + sprintf(NewLine, "%s%c0x%p", Key, c, (PUCHAR)MatchOffset + delta); + + if (r && *(r + 1) == '$') + *r = c; + else + *p = c; + +#ifdef DEBUG_COMMENTS + DebugOutput("ParseOptionLine: %s", NewLine); +#endif + + if (!strchr(NewLine, '$')) + parse_config_line(NewLine); + + return; +} + +// --- YARA-X iteration callbacks -------------------------------------------- + +static void MatchCollectCallback(const struct YRX_MATCH* Match, void* user_data) +{ + MatchCollector* mc = (MatchCollector*)user_data; + + if (!Match || mc->Count >= MAX_RULE_MATCHES) + return; + + strncpy(mc->Items[mc->Count].Identifier, mc->CurIdentifier, MATCH_IDENT_MAX - 1); + mc->Items[mc->Count].Identifier[MATCH_IDENT_MAX - 1] = 0; + mc->Items[mc->Count].Offset = Match->offset; + mc->Items[mc->Count].Length = Match->length; + mc->Count++; +} + +static void PatternCollectCallback(const struct YRX_PATTERN* Pattern, void* user_data) +{ + MatchCollector* mc = (MatchCollector*)user_data; + const uint8_t* id = NULL; + size_t idlen = 0; + + if (yrx_pattern_identifier(Pattern, &id, &idlen) == YRX_SUCCESS) + CopyPatternIdentifier(mc->CurIdentifier, MATCH_IDENT_MAX, id, idlen); + else + mc->CurIdentifier[0] = 0; + + yrx_pattern_iter_matches(Pattern, MatchCollectCallback, mc); +} + +typedef struct { + char* CapeOptions; // heap copy owned by ConfigRuleCallback (see below) +} MetaScan; + +static void MetaScanCallback(const struct YRX_METADATA* Meta, void* user_data) +{ + MetaScan* ms = (MetaScan*)user_data; + + // The YRX_METADATA (and every pointer inside it) is freed as soon as this + // callback returns, so the value must be copied out here, not aliased. + if (ms->CapeOptions) + return; + if (Meta && Meta->value_type == YRX_STRING && Meta->identifier && + !strcmp(Meta->identifier, "cape_options") && Meta->value.string) + ms->CapeOptions = _strdup(Meta->value.string); +} + +// --- matching-rule callbacks --------------------------------------------------- + +static void ConfigRuleCallback(const struct YRX_RULE* Rule, void* user_data) +{ + char RuleId[256]; + const uint8_t* rid = NULL; + size_t ridlen = 0; + BOOL SetBreakpoints = FALSE; + MetaScan ms = { NULL }; + MatchCollector* mc; + SIZE_T length; + char* OptionLine; + int i; + + if (yrx_rule_identifier(Rule, &rid, &ridlen) != YRX_SUCCESS || !rid) + return; + if (ridlen >= sizeof(RuleId)) + ridlen = sizeof(RuleId) - 1; + memcpy(RuleId, rid, ridlen); + RuleId[ridlen] = 0; + + if (!strcmp(RuleId, "capemon")) + return; + + DebugOutput("YaraScan hit: %s\n", RuleId); + if (TraceRunning) + DebuggerOutput("YaraScan hit: %s ", RuleId); + + yrx_rule_iter_metadata(Rule, MetaScanCallback, &ms); + if (!ms.CapeOptions) + return; + + mc = (MatchCollector*)calloc(1, sizeof(MatchCollector)); + if (!mc) + { + free(ms.CapeOptions); + return; + } + + yrx_rule_iter_patterns(Rule, PatternCollectCallback, mc); + + for (i = 0; i < mc->Count; i++) + { + DebugOutput("YaraScan match: %s (0x%x)", mc->Items[i].Identifier, (unsigned)mc->Items[i].Offset); + if (TraceRunning) + DebuggerOutput("YaraScan match: %s (0x%x) ", mc->Items[i].Identifier, (unsigned)mc->Items[i].Offset); + } + + length = (SIZE_T)strlen(ms.CapeOptions); + OptionLine = ms.CapeOptions; + while (OptionLine && OptionLine < ms.CapeOptions + length) + { + char* p = strchr(OptionLine, ','); + if (p) + *p = 0; + + if (!strchr(OptionLine, '$')) + parse_config_line(OptionLine); + else + { + for (i = 0; i < mc->Count; i++) + ParseOptionLine(OptionLine, mc->Items[i].Identifier, mc->Items[i].Offset, mc->Items[i].Length, user_data); + } + + if (!_strnicmp(OptionLine, "bp", 2) || !strncmp(OptionLine, "br", 2) || !strncmp(OptionLine, "sysbp", 5)) + SetBreakpoints = TRUE; + + if (!_stricmp("dump", OptionLine)) + { + DebugOutput("YaraScan: Dump of region at 0x%p triggered by Yara.", user_data); + if (TraceRunning) + DebuggerOutput("YaraScan: Dump of region at 0x%p triggered by Yara ", user_data); + DumpRegion(user_data); + } + if (!_stricmp("coverage", OptionLine)) + { + if (remove_dll_range((ULONG_PTR)user_data)) + DebugOutput("YaraScan: Region at 0x%p removed from dll range for coverage.", user_data); + else + DebugOutput("YaraScan: Failed to remove region at 0x%p from dll range for coverage.", user_data); + } + if (!_stricmp("clear", OptionLine)) + { + BreakpointsHit = FALSE; + g_config.bp0 = NULL; + g_config.bp1 = NULL; + g_config.bp2 = NULL; + g_config.bp3 = NULL; + g_config.br0 = NULL; + g_config.br1 = NULL; + g_config.br2 = NULL; + g_config.br3 = NULL; + g_config.hc0 = 0; + g_config.hc1 = 0; + g_config.hc2 = 0; + g_config.hc3 = 0; + memset(Action0, 0, MAX_PATH); + memset(Action1, 0, MAX_PATH); + memset(Action2, 0, MAX_PATH); + memset(Action3, 0, MAX_PATH); + } + + if (p) + { + *p = ','; + OptionLine = p + 1; + } + else + OptionLine = NULL; + } + + if (DebuggerInitialised && SetBreakpoints) + SetInitialBreakpoints(user_data); + + free(mc); + free(ms.CapeOptions); +} + +static void AddressRuleCallback(const struct YRX_RULE* Rule, void* user_data) +{ + NameByAddress* AddressInfos = (NameByAddress*)user_data; + char RuleId[256]; + const uint8_t* rid = NULL; + size_t ridlen = 0; + MatchCollector* mc; + int i; + SIZE_T j; + + if (yrx_rule_identifier(Rule, &rid, &ridlen) != YRX_SUCCESS || !rid) + return; + if (ridlen >= sizeof(RuleId)) + ridlen = sizeof(RuleId) - 1; + memcpy(RuleId, rid, ridlen); + RuleId[ridlen] = 0; + + mc = (MatchCollector*)calloc(1, sizeof(MatchCollector)); + if (!mc) + return; + + yrx_rule_iter_patterns(Rule, PatternCollectCallback, mc); + + // Match libyara backend: for the AddressInfos entry whose name equals the + // rule identifier, record the offset of the last match seen for the rule. + for (j = 0; AddressInfos[j].FunctionName != NULL; j++) + { + if (strcmp(RuleId, AddressInfos[j].FunctionName)) + continue; + for (i = 0; i < mc->Count; i++) + { +#ifdef DEBUG_COMMENTS + DebugOutput("AddressRuleCallback: %s at RVA 0x%x", AddressInfos[j].FunctionName, (unsigned)mc->Items[i].Offset); +#endif + AddressInfos[j].Address = (PVOID)mc->Items[i].Offset; + } + } + + free(mc); +} + +// --- scanner lifecycle ------------------------------------------------------- + +static YRX_SCANNER* GetThreadScanner(void) +{ + YRX_SCANNER* s; + ScannerNode* node; + + if (t_Scanner) + return t_Scanner; + if (!Rules) + return NULL; + + s = NULL; + if (yrx_scanner_create(Rules, &s) != YRX_SUCCESS || !s) + { + if (YaraLogging) + DebugOutput("YaraScan: yrx_scanner_create failed\n"); + return NULL; + } + + if (g_config.yara_timeout > 0) + yrx_scanner_set_timeout(s, (uint64_t)g_config.yara_timeout); + + node = (ScannerNode*)calloc(1, sizeof(ScannerNode)); + if (node && ScannerLockInit) + { + node->Scanner = s; + EnterCriticalSection(&ScannerLock); + node->Next = ScannerList; + ScannerList = node; + LeaveCriticalSection(&ScannerLock); + } + else if (node) + { + free(node); + } + + t_Scanner = s; + return s; +} + +// --- public API ------------------------------------------------------------ + +static void YaraScanInternal(PVOID Address, SIZE_T Size, YRX_RULE_CALLBACK Callback, void* CallbackData, const char* Where) +{ + YRX_SCANNER* Scanner; + enum YRX_RESULT Result = YRX_SUCCESS; + + if (!YaraActivated || !Size) + return; + + if (t_Scanning) + { +#ifdef DEBUG_COMMENTS + DebugOutput("YaraScan (%s): re-entrant scan on same thread skipped\n", Where); +#endif + return; + } + + // Shared for the whole scan: blocks YaraShutdown() from destroying this + // thread's scanner (or the shared Rules) out from under yrx_scanner_scan(). + AcquireSRWLockShared(&ScanShutdownLock); + + if (!YaraActivated) + { + ReleaseSRWLockShared(&ScanShutdownLock); + return; + } + + Scanner = GetThreadScanner(); + if (!Scanner) + { + ReleaseSRWLockShared(&ScanShutdownLock); + return; + } + + if (yrx_scanner_on_matching_rule(Scanner, Callback, CallbackData) != YRX_SUCCESS) + { + ReleaseSRWLockShared(&ScanShutdownLock); + return; + } + + t_Scanning = 1; + __try + { + Result = yrx_scanner_scan(Scanner, (const uint8_t*)Address, (size_t)Size); + } + __except (EXCEPTION_EXECUTE_HANDLER) + { + t_Scanning = 0; + ReleaseSRWLockShared(&ScanShutdownLock); + if (YaraLogging) + DebugOutput("YaraScan (%s): exception scanning 0x%p\n", Where, Address); + return; + } + t_Scanning = 0; + ReleaseSRWLockShared(&ScanShutdownLock); + + if (Result != YRX_SUCCESS) + ScannerError(Result, Where); +} + +void YaraScan(PVOID Address, SIZE_T Size) +{ + SIZE_T AccessibleSize; + + if (!YaraActivated || !Size) + return; + + AccessibleSize = GetAccessibleSize(Address); + if (!AccessibleSize) + { +#ifdef DEBUG_COMMENTS + DebugOutput("YaraScan: Memory at 0x%p is inaccessible.\n", Address); +#endif + return; + } + + if (AccessibleSize < Size) + Size = AccessibleSize; + + Size = (SIZE_T)ReverseScanForNonZero(Address, Size); + if (!Size) + { + if (YaraLogging) + DebugOutput("YaraScan: Nothing to scan at 0x%p!\n", Address); + return; + } + +#ifndef DEBUG_COMMENTS + if (YaraLogging) +#endif + DebugOutput("YaraScan: Scanning 0x%p, size 0x%x\n", Address, Size); + + YaraScanInternal(Address, Size, ConfigRuleCallback, Address, "YaraScan"); +} + +void SilentYaraScan(PVOID Address, SIZE_T Size) +{ +#ifndef DEBUG_COMMENTS + BOOL PreviousYaraLogging = YaraLogging; + YaraLogging = FALSE; +#endif + YaraScan(Address, Size); +#ifndef DEBUG_COMMENTS + YaraLogging = PreviousYaraLogging; +#endif +} + +NameByAddress* GetAddressesByYara(HMODULE ModuleBase, PCHAR FunctionNames[], SIZE_T FunctionCount, SIZE_T* OutFoundCount) +{ + NameByAddress* AddressInfos; + SIZE_T Size, FoundCount, i; + + if (!YaraActivated || !FunctionNames || FunctionCount == 0) + return NULL; + + Size = GetAccessibleSize(ModuleBase); + if (!Size) + return NULL; + + Size = (SIZE_T)ReverseScanForNonZero(ModuleBase, Size); + if (!Size) + { + if (YaraLogging) + DebugOutput("GetAddressesByYara: Nothing to scan at 0x%p!\n", ModuleBase); + return NULL; + } + + AddressInfos = (NameByAddress*)calloc(FunctionCount + 1, sizeof(NameByAddress)); + if (!AddressInfos) + return NULL; + + for (i = 0; i < FunctionCount; i++) + { + AddressInfos[i].FunctionName = FunctionNames[i]; + AddressInfos[i].Address = NULL; + } + + YaraScanInternal((PVOID)ModuleBase, Size, AddressRuleCallback, AddressInfos, "GetAddressesByYara"); + + FoundCount = 0; + for (i = 0; i < FunctionCount; i++) + { + if (AddressInfos[i].Address) + { + AddressInfos[i].Address = (PVOID)((ULONG_PTR)ModuleBase + (ULONG_PTR)AddressInfos[i].Address); + FoundCount++; + } + } + + if (OutFoundCount) + *OutFoundCount = FoundCount; + + return AddressInfos; +} + +PVOID GetAddressByYara(HMODULE ModuleBase, PCHAR FunctionName) +{ + PCHAR FunctionNames[] = { FunctionName, NULL }; + SIZE_T FoundCount = 0; + NameByAddress* Results; + PVOID FoundAddress = NULL; + + if (!YaraActivated) + return NULL; + + Results = GetAddressesByYara(ModuleBase, FunctionNames, 1, &FoundCount); + if (Results && FoundCount > 0) + { + FoundAddress = Results[0].Address; + free(Results); + } + else if (Results) + { + free(Results); + } + + return FoundAddress; +} + +BOOL ScanForRulesCanary(PVOID Address, SIZE_T Size) +{ + BOOL PreviousYaraLogging = YaraLogging; + BOOL CapemonRulesDetected = FALSE; + + YaraLogging = FALSE; + if (GetAddressByYara(Address, "capemon")) + { + CapemonRulesDetected = TRUE; + DebugOutput("ScanForRulesCanary: capemon rules detected"); + } + YaraLogging = PreviousYaraLogging; + return CapemonRulesDetected; +} + +void YaraShutdown() +{ + ScannerNode* node; + + YaraActivated = FALSE; + + // Wait for every in-flight YaraScanInternal() (on any thread) to finish + // and release its shared hold before destroying the scanners/Rules those + // calls (or a not-yet-started one whose thread already cached t_Scanner) + // may still be using. See the comment on ScanShutdownLock's declaration. + AcquireSRWLockExclusive(&ScanShutdownLock); + + if (ScannerLockInit) + { + EnterCriticalSection(&ScannerLock); + node = ScannerList; + ScannerList = NULL; + LeaveCriticalSection(&ScannerLock); + + while (node) + { + ScannerNode* next = node->Next; + if (node->Scanner) + yrx_scanner_destroy(node->Scanner); + free(node); + node = next; + } + } + + t_Scanner = NULL; + + if (Rules) + { + yrx_rules_destroy(Rules); + Rules = NULL; + } + + ReleaseSRWLockExclusive(&ScanShutdownLock); + + // Deliberately NOT calling yrx_finalize(): it tears down process-wide + // wasmtime exception-handler state and is unsafe while capemon's own VEH + // and debugger are active. capemon lives for the process lifetime anyway. +} + +// Reads an entire file into a NUL-terminated heap buffer (caller frees). +static char* ReadWholeFile(const char* Path, size_t* OutLen) +{ + FILE* f = fopen(Path, "rb"); + long len; + char* buf; + size_t got; + + if (!f) + return NULL; + + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len < 0) + { + fclose(f); + return NULL; + } + + buf = (char*)malloc((size_t)len + 1); + if (!buf) + { + fclose(f); + return NULL; + } + + got = fread(buf, 1, (size_t)len, f); + fclose(f); + buf[got] = 0; + if (OutLen) + *OutLen = got; + return buf; +} + +BOOL YaraInit() +{ + YRX_COMPILER* Compiler = NULL; + char analyzer_path[MAX_PATH], yara_dir[MAX_PATH], file_name[MAX_PATH], compiled_rules[MAX_PATH]; + uint32_t compiler_flags = YRX_RELAXED_RE_SYNTAX | YRX_ENABLE_CONDITION_OPTIMIZATION; + enum YRX_RESULT rc; + + if (!ScannerLockInit) + { + InitializeCriticalSection(&ScannerLock); + ScannerLockInit = TRUE; + } + + strncpy(analyzer_path, our_dll_path, strlen(our_dll_path) + 1); + if (!g_config.standalone) + PathRemoveFileSpec(analyzer_path); + PathRemoveFileSpec(analyzer_path); + sprintf(yara_dir, "%s\\data\\yara", analyzer_path); + sprintf(compiled_rules, "%s\\capemon.yrx", yara_dir); + + // 1) Try the compiled-rule cache. + { + size_t blob_len = 0; + char* blob = ReadWholeFile(compiled_rules, &blob_len); + if (blob) + { + rc = yrx_rules_deserialize((const uint8_t*)blob, blob_len, &Rules); + free(blob); + if (rc == YRX_SUCCESS && Rules) + DebugOutput("YaraInit: Compiled rules loaded from %s\n", compiled_rules); + else + { + DebugOutput("YaraInit: Ignoring stale/incompatible %s\n", compiled_rules); + ScannerError(rc, "deserialize"); + Rules = NULL; + } + } + } + + // 2) Otherwise compile from source. + if (!Rules) + { + if (yrx_compiler_create(compiler_flags, &Compiler) != YRX_SUCCESS || !Compiler) + { + DebugOutput("YaraInit: yrx_compiler_create failure\n"); + goto fail; + } + + if (yrx_compiler_add_source(Compiler, InternalYara) != YRX_SUCCESS) + { + const char* detail = yrx_last_error(); + DebugOutput("YaraInit: failed to add internal rules%s%s\n", detail ? ": " : "", detail ? detail : ""); + } + + if (g_config.yarascan) + { + char FindString[MAX_PATH]; + WIN32_FIND_DATA FindFileData; + HANDLE hFind; + unsigned int count = 0; + + sprintf(FindString, "%s\\*.yar", yara_dir); +#ifdef DEBUG_COMMENTS + DebugOutput("YaraInit: Yara search string: %s", FindString); +#endif + hFind = FindFirstFile(FindString, &FindFileData); + if (hFind != INVALID_HANDLE_VALUE) + { + do + { + size_t src_len = 0; + char* src; + + if (FindFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + continue; + + snprintf(file_name, sizeof(file_name), "%s\\%s", yara_dir, FindFileData.cFileName); + + src = ReadWholeFile(file_name, &src_len); + if (!src) + { + DebugOutput("YaraInit: Unable to open file %s\n", file_name); + continue; + } + + if (!strstr(src, "cape_options")) + { + DebugOutput("YaraInit: File %s lacks cape_options metadata - skipping\n", file_name); + free(src); + continue; + } + + rc = yrx_compiler_add_source_with_origin(Compiler, src, file_name); + free(src); + + if (rc != YRX_SUCCESS) + { + const char* detail = yrx_last_error(); + DebugOutput("YaraInit: Unable to compile rule file %s%s%s\n", + file_name, detail ? ": " : "", detail ? detail : ""); + } + else + { + count++; +#ifdef DEBUG_COMMENTS + DebugOutput("YaraInit: Compiled rule file %s\n", file_name); +#endif + } + } + while (FindNextFile(hFind, &FindFileData)); + + FindClose(hFind); + DebugOutput("YaraInit: Compiled %d rule files\n", count); + } + else + DebugOutput("YaraInit: Found no Yara rules in %s\n", yara_dir); + } + + Rules = yrx_compiler_build(Compiler); + yrx_compiler_destroy(Compiler); + Compiler = NULL; + + if (!Rules) + { + const char* detail = yrx_last_error(); + DebugOutput("YaraInit: yrx_compiler_build failed%s%s\n", detail ? ": " : "", detail ? detail : ""); + goto fail; + } + + if (g_config.yarascan) + { + struct YRX_BUFFER* blob = NULL; + if (yrx_rules_serialize(Rules, &blob) == YRX_SUCCESS && blob) + { + FILE* f = fopen(compiled_rules, "wb"); + if (f) + { + fwrite(blob->data, 1, blob->length, f); + fclose(f); + DebugOutput("YaraInit: Compiled rules saved to %s\n", compiled_rules); + } + yrx_buffer_destroy(blob); + } + else + DebugOutput("YaraInit: yrx_rules_serialize failed - cache not written\n"); + } + } + + YaraActivated = TRUE; + YaraLogging = TRUE; + + { + OSVERSIONINFO OSVersion; + OSVersion.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); +#pragma warning(suppress : 4996) + if (!GetVersionEx(&OSVersion)) + { + ErrorOutput("YaraInit: Failed to get OS version"); + return TRUE; + } + +#ifdef _WIN64 + if ((OSVersion.dwMajorVersion == 6 && OSVersion.dwMinorVersion > 1) || OSVersion.dwMajorVersion > 6) + { + PVOID RtlInsertInvertedFunctionTable = GetAddressByYara(GetModuleHandleA("ntdll"), "RtlInsertInvertedFunctionTable"); + if (RtlInsertInvertedFunctionTable) + { + LdrpInvertedFunctionTableSRWLock = (PVOID)((PBYTE)RtlInsertInvertedFunctionTable + *(DWORD*)((PBYTE)RtlInsertInvertedFunctionTable + 3) + 7); + DebugOutput("RtlInsertInvertedFunctionTable 0x%p, LdrpInvertedFunctionTableSRWLock 0x%p", RtlInsertInvertedFunctionTable, LdrpInvertedFunctionTableSRWLock); + } + } +#endif + } + + return TRUE; + +fail: + if (Compiler) + yrx_compiler_destroy(Compiler); + if (Rules) + { + yrx_rules_destroy(Rules); + Rules = NULL; + } + return FALSE; +} diff --git a/capemon.vcxproj b/capemon.vcxproj index da4318a7..bf877cad 100644 --- a/capemon.vcxproj +++ b/capemon.vcxproj @@ -150,7 +150,9 @@ MultiThreaded Level3 ProgramDatabase - .\libyara\include;.\bson;.\distorm\include;C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\WTL81\Include;%(AdditionalIncludeDirectories) + + CAPE_USE_YARA_X;WIN32;_CRT_SECURE_NO_WARNINGS;_CRT_SECURE_NO_WARNINGSNDEBUG;_WINDOWS;_USRDLL;MONGO_HAVE_STDINT;MONGO_STATIC_BUILD;%(PreprocessorDefinitions) + .\yara-x\include;.\bson;.\distorm\include;C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\WTL81\Include;%(AdditionalIncludeDirectories) false false false @@ -163,8 +165,10 @@ Windows true true - libyara32.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies) - $(ProjectDir)\Release;$(ProjectDir)\libyara\lib + + yara_x_capi.dll.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies) + $(ProjectDir)\Release;$(ProjectDir)docs\yara-x\bin\x32 0x30000000 @@ -176,17 +180,22 @@ + + + copy /y "$(ProjectDir)docs\yara-x\bin\x32\yara_x_capi.dll" "$(TargetDir)" + true - WIN32;_WIN64;_CRT_SECURE_NO_WARNINGS;_WINDOWS;_USRDLL;MONGO_HAVE_STDINT;MONGO_STATIC_BUILD;%(PreprocessorDefinitions) + + CAPE_USE_YARA_X;WIN32;_WIN64;_CRT_SECURE_NO_WARNINGS;_WINDOWS;_USRDLL;MONGO_HAVE_STDINT;MONGO_STATIC_BUILD;%(PreprocessorDefinitions) MultiThreaded Level3 ProgramDatabase - .\libyara\include;.\bson;.\distorm\include;%(AdditionalIncludeDirectories) + .\yara-x\include;.\bson;.\distorm\include;%(AdditionalIncludeDirectories) false false false @@ -196,8 +205,10 @@ Windows true true - libyara64.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies) - $(ProjectDir)x64\Release;$(ProjectDir)\libyara\lib + + yara_x_capi.dll.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies) + $(ProjectDir)x64\Release;$(ProjectDir)docs\yara-x\bin\x64 @@ -210,6 +221,10 @@ + + + copy /y "$(ProjectDir)docs\yara-x\bin\x64\yara_x_capi.dll" "$(TargetDir)" + @@ -234,7 +249,17 @@ - + + + true + true + + + + true + false + false + diff --git a/capemon.vcxproj.filters b/capemon.vcxproj.filters index 9f317a23..96f8954e 100644 --- a/capemon.vcxproj.filters +++ b/capemon.vcxproj.filters @@ -282,6 +282,9 @@ Source Files\CAPE + + Source Files\CAPE + Source Files\CAPE diff --git a/docs/yara-x-migration.md b/docs/yara-x-migration.md new file mode 100644 index 00000000..f81dce69 --- /dev/null +++ b/docs/yara-x-migration.md @@ -0,0 +1,198 @@ +# YARA-X backend for the in-monitor scanner + +Status: **work in progress / opt-in**. libyara remains the default. + +capemon's in-monitor YARA scanning lives entirely behind `CAPE/YaraHarness.h` +(7 functions + the `NameByAddress` struct). This change adds a second +implementation of that same interface, `CAPE/YaraHarnessX.c`, built against +[YARA-X](https://github.com/VirusTotal/yara-x) instead of libyara. Exactly one +backend is compiled in, selected by the `CAPE_USE_YARA_X` preprocessor define. + +Nothing outside the harness changes: every caller (`hook_clr.c`, `Trace.c`, +`CAPE.c`, `hooks.c`, `capemon.c`, ...) keeps calling `YaraScan`, +`GetAddressesByYara`, etc. unchanged. + +--- + +## What is in the tree now + +| Path | Purpose | +|---|---| +| `CAPE/YaraHarness.c` | libyara backend (unchanged, still the default) | +| `CAPE/YaraHarnessX.c` | **new** — YARA-X backend, identical public API | +| `CAPE/YaraHarness.h` | `#include "yara.h"` is now skipped when `CAPE_USE_YARA_X` is set | +| `yara-x/include/yara_x.h` | vendored YARA-X C API header (v1.20.0) | +| `docs/yara-x/bin/{x32,x64}/` | prebuilt `yara_x_capi.dll` + import lib + PDB — this is what `capemon.vcxproj` actually links/loads today (see `docs/yara-x/readme.md`); **Step 1 below (a static lib in `yara-x/lib/`) is an alternative build path that is not currently wired into the project** | + +The `yara-x/` layout deliberately mirrors `libyara/` (`include/` + `lib/`). + +--- + +## Behavioural parity notes + +* `cape_options` string metadata drives dynamic config exactly as before; + `$pattern` references inside option lines are resolved to matched offsets via + `ParseOptionLine`. +* Pattern identifiers are normalised to a leading `$` (YARA-X already returns + `$name`, so this is normally a no-op) so option-line matching is unchanged. +* **Compiled-rule cache uses a different filename: `capemon.yrx`** (not + `capemon.yac`). The serialized blob format differs and is locked to the exact + YARA-X version; a stale/incompatible cache is detected on load and the rules + are recompiled from `data/yara/*.yar`. +* YARA-X scanners are single-threaded, stateful objects. The backend keeps **one + `YRX_SCANNER` per thread** (the `YRX_RULES` object is shared, which YARA-X + explicitly supports) plus a per-thread re-entrancy guard so a scan triggered + from inside a match callback is skipped rather than corrupting scanner state. +* `yrx_finalize()` is intentionally **not** called at shutdown — it tears down + process-wide wasmtime exception-handler state and is unsafe while capemon's own + VEH and debugger are installed. capemon lives for the process lifetime anyway. +* Compiler flags: `YRX_RELAXED_RE_SYNTAX | YRX_ENABLE_CONDITION_OPTIMIZATION`. + Relaxed regex syntax maximises compatibility with the libyara-era rule corpus. + +--- + +## Step 1 (alternative) — produce the YARA-X static libraries + +> The project as committed links the **dynamic** `yara_x_capi.dll` prebuilt +> under `docs/yara-x/bin/{x32,x64}/` (see `docs/yara-x/readme.md` for how +> those were built). The static-lib path below is kept for reference/rollback +> to a statically-linked build; it requires redoing Step 2 to point back at +> `yara-x/lib/yara_xNN.lib` and the transitive system libs it needs. + +Requires a Rust toolchain + [`cargo-c`](https://github.com/lu-zero/cargo-c) on a +machine with the MSVC build tools. + +```bat +rustup target add x86_64-pc-windows-msvc i686-pc-windows-msvc +cargo install cargo-c + +set RUSTFLAGS=-C target-feature=+crt-static + +:: 64-bit +cargo cinstall -p yara-x-capi --release ^ + --target x86_64-pc-windows-msvc ^ + --library-type staticlib ^ + --no-default-features --features pe,dotnet,math,hash,string,console,elf,macho ^ + --destdir out64 --prefix / + +:: 32-bit +cargo cinstall -p yara-x-capi --release ^ + --target i686-pc-windows-msvc ^ + --library-type staticlib ^ + --no-default-features --features pe,dotnet,math,hash,string,console,elf,macho ^ + --destdir out32 --prefix / +``` + +Then copy the artefacts into the repo, renamed per-arch like the libyara libs: + +``` +out64\lib\yara_x_capi.lib -> yara-x\lib\yara_x64.lib +out32\lib\yara_x_capi.lib -> yara-x\lib\yara_x32.lib +``` + +Keep `yara-x/include/yara_x.h` in sync with the version the libs were built from +(currently v1.20.0). The serialized `capemon.yrx` cache is only valid for the +exact version it was produced with. + +Notes: +* `+crt-static` makes the Rust lib link the **static** CRT (`libcmt`), matching + capemon's `/MT` Release configs. This conflicts with `/MTd` in **Debug** + configs — see Step 3. +* Dropping `--features` you don't need (`magic`, `lief`, `time`) keeps the lib + smaller. `pe` + `dotnet` are the important ones for CAPE rules. +* `cargo-c` prints the transitive system libraries the static lib needs (into a + `.pc` file). Expect roughly: `ntdll userenv bcrypt advapi32 ws2_32 kernel32`. + +--- + +## Step 2 — project changes (`capemon.vcxproj`) + +> This describes the static-lib wiring matching Step 1 above. The +> **currently-committed** `Release|x64` block instead links +> `docs\yara-x\bin\x64\yara_x_capi.dll.lib` (dynamic import lib, no +> `ntdll`/`userenv`/`bcrypt`/`advapi32`) and has a `PostBuildEvent` that copies +> `yara_x_capi.dll` next to the built `capemon_x64.dll` so it can be loaded at +> runtime. Only `Release|x64` is enabled today; `Win32` is not yet wired up. + +For each of the four `ItemDefinitionGroup` blocks (Debug/Release × Win32/x64): + +1. **PreprocessorDefinitions** — add `CAPE_USE_YARA_X`. +2. **AdditionalIncludeDirectories** — `.\libyara\include` → `.\yara-x\include`. +3. **AdditionalLibraryDirectories** — `$(ProjectDir)\libyara\lib` → `$(ProjectDir)\yara-x\lib`. +4. **AdditionalDependencies**: + * Win32: `libyara32.lib` → `yara_x32.lib;ntdll.lib;userenv.lib;bcrypt.lib;advapi32.lib` + * x64: `libyara64.lib` → `yara_x64.lib;ntdll.lib;userenv.lib;bcrypt.lib;advapi32.lib` + * (`crypt32.lib` was only needed by libyara's hash module — safe to drop once + libyara is gone; harmless to leave.) + +Add the source file to the project (once, applies to all configs): + +```xml + +``` + +and exclude the libyara backend from the build (keep the file for now): + +```xml + + true + +``` + +(Or, cleaner: put both `` entries under `Condition`s keyed on a +`$(UseYaraX)` MSBuild property so a single switch flips the whole thing.) + +--- + +## Step 3 — Debug configuration + +Rust's `+crt-static` gives a release static CRT. In capemon **Debug** +(`/MTd` → `libcmtd`) the linker will report `LNK4098: defaultlib 'libcmt' +conflicts`. Options, easiest first: + +1. **Only enable `CAPE_USE_YARA_X` in Release** (Debug keeps libyara). capemon + ships Release; this is the recommended starting point. +2. Build a second Rust lib without `+crt-static` and link Debug capemon against + the dynamic CRT (`/MDd`) — changes capemon's CRT model, invasive. +3. `/NODEFAULTLIB:libcmt` + accept mixing — fragile, not recommended. + +--- + +## Step 4 — rules + +* Delete any stale `data/yara/capemon.yac` and `capemon.yrx`. +* Run the full CAPE rule corpus through the `yara-x` CLI: + `yr fmt --check` / `yr compile data/yara/*.yar` and fix rejects. YARA-X is + stricter (more warnings-as-errors, a few removed constructs); most rules pass + unchanged. Track fixes in the CAPE rules repo, not here. +* `pe`, `dotnet`, `math`, `hash`, `string`, `console`, `elf`, `macho` modules are + built in (per the feature list in Step 1). `magic` is **not** — grep the corpus + for `import "magic"` before shipping. + +--- + +## Step 5 — regression test + +Build Release x86 **and** x64. Then, against known samples: + +* [ ] `yarascan=1` — a rule with `bpN=$str+off` style `cape_options` sets the + same breakpoints as the libyara build (diff the debug log). +* [ ] `dump`, `coverage`, `clear` option keywords still act. +* [ ] `GetAddressByYara`/`GetAddressesByYara` still resolve + `RtlInsertInvertedFunctionTable`, the `WMI_*` and `vDbgPrint*` internal + rules (check the `YaraInit` / hook-resolution log lines). +* [ ] `ScanForRulesCanary` still trips on the `capemon` canary rule. +* [ ] Multi-threaded target with JIT + unpacking active: no crash/hang, scans on + different threads all produce hits. +* [ ] First run compiles + writes `capemon.yrx`; second run loads it; corrupting + the file forces a clean recompile. + +--- + +## Rollback + +Everything is behind `CAPE_USE_YARA_X` + one extra source file. Remove the define, +re-include `YaraHarness.c`, restore the four vcxproj blocks. `libyara/` is left +untouched until the switch is considered permanent, at which point delete +`libyara/`, `CAPE/YaraHarness.c`, and the `#ifndef CAPE_USE_YARA_X` guard in the +header. diff --git a/docs/yara-x-windows-session.md b/docs/yara-x-windows-session.md new file mode 100644 index 00000000..68556898 --- /dev/null +++ b/docs/yara-x-windows-session.md @@ -0,0 +1,248 @@ +# YARA-X migration — Windows session handoff + +Paste this whole file into the new session so it has full context. + +## Where we are + +capemon's in-monitor YARA scanning is fully isolated behind `CAPE/YaraHarness.h` +(7 functions + `NameByAddress`). On the previous (macOS) session we added a +second backend implementing that same interface against **YARA-X** instead of +libyara, selected by the `CAPE_USE_YARA_X` preprocessor define. libyara is +untouched and remains the default. + +Already committed to the working tree (all additive): + +| Path | State | +|---|---| +| `CAPE/YaraHarnessX.c` | **new** — complete YARA-X backend (~640 LoC). Written against the real v1.20.0 header, **never compiled**. | +| `CAPE/YaraHarness.h` | `#include "yara.h"` skipped when `CAPE_USE_YARA_X` is defined | +| `yara-x/include/yara_x.h` | vendored YARA-X C API header, **v1.20.0** | +| `yara-x/lib/` | **empty** — Step 1 fills it | +| `docs/yara-x-migration.md` | design notes / rationale | +| `docs/yara-x-windows-session.md` | this file | +| `capemon.vcxproj` / `.filters` | **`Release\|x64` block already wired** for the YARA-X backend (Step 2 below is pre-done for that one config); the other 3 configs still use libyara | + +Goal of the Windows session: do Step 1 (build the libs) and Step 3 (build + test) +to get **x64 Release** capemon building and passing the regression checklist with +the YARA-X backend. Step 2 is only "verify / adjust the linker deps". Then repeat +Step 2 + 3 for Win32 Release. + +--- + +## Decisions already made (don't re-litigate, just apply) + +* **`pulley` Cargo feature ON.** YARA-X compiles rule conditions to WASM and runs + them in wasmtime. Default backend is Cranelift JIT, which allocates + **RWX/executable memory** at scan time — bad inside an injected monitoring DLL + (trips RWX detection, blocked by ACG/CFG). `pulley` switches wasmtime to a + portable bytecode interpreter: no JIT, no RWX. Slightly slower condition eval, + irrelevant for CAPE's simple `any of them` rules. +* **Static CRT (`-C target-feature=+crt-static`)** to match capemon's `/MT`. + This produces `libcmt`, which conflicts with capemon **Debug** (`/MTd`, + `libcmtd`). So: **Release configs only** for now. Debug keeps libyara. +* Compiled-rule cache filename is `capemon.yrx` (not `.yac`) — different, + version-locked format. Handled in code. +* One `YRX_SCANNER` per thread (YARA-X scanners aren't thread-safe; the + `YRX_RULES` object is shared). Handled in code. +* `magic` module deliberately excluded (would need libmagic). `pe`, `dotnet`, + `hash`, `math`, `string`, `console`, `elf`, `macho`, `lnk` are in the default + feature set and are enough. + +--- + +## Step 1 — build the YARA-X static libraries + +`yara-x-capi` has `crate-type = ["staticlib", ...]`, so **plain `cargo build` +produces the `.lib`** — no `cargo-c` needed (the C header is already vendored). + +Prereqs: `rustup` with MSVC toolchain, and the "Desktop development with C++" +VS workload. From a **x64 Native Tools Command Prompt**: + +```bat +rustup target add x86_64-pc-windows-msvc i686-pc-windows-msvc + +git clone --depth 1 --branch v1.20.0 https://github.com/VirusTotal/yara-x C:\src\yara-x +cd C:\src\yara-x + +set RUSTFLAGS=-C target-feature=+crt-static + +:: ---- x64 ---- +cargo build -p yara-x-capi --release --features pulley --target x86_64-pc-windows-msvc + +:: ---- x86 ---- +cargo build -p yara-x-capi --release --features pulley --target i686-pc-windows-msvc +``` + +Artefacts: + +``` +C:\src\yara-x\target\x86_64-pc-windows-msvc\release\yara_x_capi.lib +C:\src\yara-x\target\i686-pc-windows-msvc\release\yara_x_capi.lib +``` + +Get the exact list of **system libs** the static lib needs (Rust std + wasmtime +pull in a handful): + +```bat +cargo rustc -p yara-x-capi --release --features pulley ^ + --target x86_64-pc-windows-msvc --crate-type staticlib -- ^ + --print native-static-libs +``` + +It prints a line like +`native-static-libs: kernel32.lib advapi32.lib ntdll.lib userenv.lib ws2_32.lib bcrypt.lib ...` +— **copy that list**, you need it in Step 2. + +Vendor into the repo (rename per-arch like the libyara libs): + +```bat +copy C:\src\yara-x\target\x86_64-pc-windows-msvc\release\yara_x_capi.lib \yara-x\lib\yara_x64.lib +copy C:\src\yara-x\target\i686-pc-windows-msvc\release\yara_x_capi.lib \yara-x\lib\yara_x32.lib +``` + +Sanity-check the header matches: `yara-x\include\yara_x.h` should already say +`v1.20.0`-era content (it's `capi/include/yara_x.h` from the same tag). If you +built a different tag, replace it with that tag's `capi/include/yara_x.h`. + +> Note: the capemon DLL will grow by roughly **4–8 MB** (Rust std + wasmtime). +> Measure `capemon_x64.dll` before/after. + +--- + +## Step 2 — wire `capemon.vcxproj` + +**Already done for `Release|x64`** (commit in the tree). Just verify: +`git diff capemon.vcxproj` should show, in the `Release|x64` block only, +`CAPE_USE_YARA_X` added, `libyara\include`→`yara-x\include`, +`libyara\lib`→`yara-x\lib`, `libyara64.lib`→`yara_x64.lib;ntdll.lib;userenv.lib;bcrypt.lib;advapi32.lib`, +and `YaraHarness.c` / `YaraHarnessX.c` swapped via `ExcludedFromBuild` conditions. +After Step 1's `native-static-libs` output, adjust the extra `*.lib` names in +`` if they differ. + +For **Win32 Release** (do this after x64 is green), repeat the same edits in the +`Release|Win32` block (around line 145). + +In that block: + +1. `` — prepend `CAPE_USE_YARA_X;` + (currently starts `WIN32;_WIN64;_CRT_SECURE_NO_WARNINGS;...`). + +2. `` — change `.\libyara\include` → `.\yara-x\include`. + +3. `` — change `$(ProjectDir)\libyara\lib` → + `$(ProjectDir)\yara-x\lib`. + +4. `` — replace `libyara64.lib;` with + `yara_x64.lib;` **plus the system libs from Step 1** that aren't already + there. Existing line is: + `libyara64.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies)` + → e.g.: + `yara_x64.lib;ntdll.lib;userenv.lib;bcrypt.lib;advapi32.lib;bson.lib;crypt32.lib;ws2_32.lib;%(AdditionalDependencies)` + (`crypt32.lib` was libyara's — harmless to leave.) + +Then swap the source file for this config. Find: +```xml + +``` +Replace with: +```xml + + true + + + true + false + +``` +(`YaraHarnessX.c` excluded everywhere except x64 Release; `YaraHarness.c` +excluded in x64 Release.) + +--- + +## Step 3 — build + fix + test + +### Build + +```bat +msbuild capemon.sln /p:Configuration=Release /p:Platform=x64 /t:capemon +``` + +Expected first-time friction: +* **Missing `_x86.lib` symbol / arch mismatch** — you're linking `yara_x64.lib` + into x64, fine; make sure you didn't cross the 32/64 libs. +* **`LNK2019` unresolved `bcrypt`/`ntdll`/etc.** — add the missing lib from the + `native-static-libs` list to ``. +* **`LNK4098 libcmt conflicts with use of other libs`** — you're building + Release (`/MT` → `libcmt`) and the Rust lib is also `libcmt` (`+crt-static`). + These should agree. If it complains about `libcmtd`, you accidentally touched + a Debug block — revert it. +* **C compile errors in `YaraHarnessX.c`** — it's never been compiled. Likely + suspects: a missing cast MSVC wants, `_strdup` needing `` (already + included), C89-vs-C99 declaration placement. Fix in place; the logic is a + faithful port of `YaraHarness.c`. + +### Rules + +Delete stale caches so rules recompile fresh: +```bat +del \data\yara\capemon.yac +del \data\yara\capemon.yrx +``` +Then confirm the corpus compiles under YARA-X: +```bat +yr compile \data\yara\*.yar :: `yr` = yara-x CLI, `cargo install yara-x` +``` +Fix rejects in the **CAPE rules repo**, not capemon. `grep -r 'import "magic"'` +the corpus — that module isn't built in. + +### Regression checklist (run a couple of known samples through CAPE) + +- [ ] First analysis run: log shows `YaraInit: Compiled N rule files` then + `Compiled rules saved to ...capemon.yrx`. Second run: `Compiled rules + loaded from ...capemon.yrx`. Delete/corrupt the file → clean recompile. +- [ ] A rule with `cape_options` containing `bpN=$str+off` sets the **same** + breakpoints as a libyara build (diff `YaraScan hit:` / `YaraScan match:` + lines and the resulting `bpN` config). +- [ ] `dump`, `coverage`, `clear` option keywords still act. +- [ ] `GetAddressByYara` internal rules still resolve — check the + `RtlInsertInvertedFunctionTable 0x... LdrpInvertedFunctionTableSRWLock` + line appears in the log, and WMI/`vDbgPrint` hook resolution still works. +- [ ] `ScanForRulesCanary` still trips on the `capemon` canary. +- [ ] Multi-threaded sample with JIT + unpacking active: **no crash / no hang**; + scans on multiple threads all produce hits (this exercises the per-thread + scanner + re-entrancy guard). +- [ ] `capemon_x64.dll` size delta is acceptable. + +### Then repeat for Win32 Release + +Same four edits in the `Release|Win32` block (line ~145): `CAPE_USE_YARA_X`, +`.\yara-x\include`, `$(ProjectDir)\yara-x\lib`, `libyara32.lib` → `yara_x32.lib` ++ system libs. Extend both `ExcludedFromBuild` conditions to also cover +`'Release|Win32'`. Build `/p:Platform=Win32`, re-run the checklist (32-bit target). + +--- + +## Risks to watch during testing + +* **Injection model.** wasmtime and Rust std both use TLS and one-time init. + LoadLibrary injection: fine. If capemon's loader does manual mapping for some + scenarios, watch for crashes on the first `YaraScan` (uninitialised Rust std / + wasmtime TLS). `pulley` reduces but may not eliminate this. +* **Loader lock.** `YaraScan` can be called from DLL-notification / hook context. + YARA-X allocates during scan; if that happens under the loader lock it can + deadlock. libyara had the same exposure — compare behaviour, don't assume new. +* **`yrx_finalize` is intentionally never called** (see code comment) — it tears + down process-wide wasmtime trap-handler state and would fight capemon's VEH. + +--- + +## What to report back to continue + +1. Did x64 Release build? Any `YaraHarnessX.c` compile fixes made (paste diffs). +2. Final `` line (the resolved system-lib list). +3. `capemon_x64.dll` size before/after. +4. Regression checklist results — especially the multi-threaded no-crash item and + the breakpoint-parity item. +5. Any rule-corpus rejects (list them; they’re fixed in the rules repo). +6. Whether to proceed to Win32 Release and/or tackle Debug (needs a second, + non-`+crt-static` Rust build against `/MDd`). diff --git a/docs/yara-x/bin/x32/yara_x_capi.dll b/docs/yara-x/bin/x32/yara_x_capi.dll new file mode 100644 index 00000000..815161f1 Binary files /dev/null and b/docs/yara-x/bin/x32/yara_x_capi.dll differ diff --git a/docs/yara-x/bin/x32/yara_x_capi.dll.lib b/docs/yara-x/bin/x32/yara_x_capi.dll.lib new file mode 100644 index 00000000..7419ba2e Binary files /dev/null and b/docs/yara-x/bin/x32/yara_x_capi.dll.lib differ diff --git a/docs/yara-x/bin/x32/yara_x_capi.pdb b/docs/yara-x/bin/x32/yara_x_capi.pdb new file mode 100644 index 00000000..165e30da Binary files /dev/null and b/docs/yara-x/bin/x32/yara_x_capi.pdb differ diff --git a/docs/yara-x/bin/x64/yara_x_capi.dll b/docs/yara-x/bin/x64/yara_x_capi.dll new file mode 100644 index 00000000..ba9a3c2e Binary files /dev/null and b/docs/yara-x/bin/x64/yara_x_capi.dll differ diff --git a/docs/yara-x/bin/x64/yara_x_capi.dll.lib b/docs/yara-x/bin/x64/yara_x_capi.dll.lib new file mode 100644 index 00000000..f6a8acc8 Binary files /dev/null and b/docs/yara-x/bin/x64/yara_x_capi.dll.lib differ diff --git a/docs/yara-x/bin/x64/yara_x_capi.pdb b/docs/yara-x/bin/x64/yara_x_capi.pdb new file mode 100644 index 00000000..ff9c6e69 Binary files /dev/null and b/docs/yara-x/bin/x64/yara_x_capi.pdb differ diff --git a/docs/yara-x/readme.md b/docs/yara-x/readme.md new file mode 100644 index 00000000..7f60268b --- /dev/null +++ b/docs/yara-x/readme.md @@ -0,0 +1,103 @@ +# YARA-X Compilation & Wasmtime Pulley Integration Guide + +This guide details the internal compilation architecture of YARA-X condition evaluation, explains the relationship between the JIT compiler and the Pulley interpreter within `wasmtime`, details how to avoid JIT execution memory constraints (executable memory pages), lists instructions for multi-architecture builds, and details how to generate and use debug symbols. + +--- + +## Technical Background: YARA-X Wasm Runtime + +To achieve maximum performance during scanning, YARA-X compiles YARA rule conditions into WebAssembly (Wasm) bytecode on the fly. + +To execute this bytecode, YARA-X integrates **Wasmtime** as its execution engine. However, executing unmanaged WebAssembly bytecode on a host machine requires translating it into a format the engine can execute, which introduces specific architectural constraints. + +--- + +## The JIT Compiler vs. Pulley Interpreter + +Wasmtime offers two primary methods of executing WebAssembly bytecode: + +### 1. Cranelift JIT Compilation (Default) +By default, Wasmtime uses the **Cranelift** compiler backend to compile WebAssembly bytecode directly into native host machine CPU instructions (e.g., x86_64, AArch64) at runtime. +* **Performance:** Maximum execution speed. +* **Security Constraint:** Requires the allocation of executable memory pages (e.g., `VirtualAlloc` with `PAGE_EXECUTE_READWRITE` or `PAGE_EXECUTE_READ`), which may be restricted or prohibited in secured sandbox environments, containers, or protected processes. + +### 2. Pulley Bytecode Interpreter +Wasmtime includes the **Pulley** interpreter—a highly portable, virtual-machine-based software interpreter. +* **Security Advantage:** Because Pulley executes bytecode via a software interpreter loop, **no native machine instructions are generated or executed from dynamically allocated memory pages**. All code runs out of standard, non-executable read/write memory pages, eliminating JIT execution constraints and complying with strict security policies. +* **Portability:** Runs on any hardware architecture supported by Rust, even those without native Cranelift JIT backends. + +--- + +## The Compilation Trap: Why Cranelift is Required for Pulley + +When attempting to build YARA-X with Pulley to reduce binary size or strip JIT capabilities, it is a common mistake to completely disable the `"cranelift"` feature of the `wasmtime` dependency in `lib/Cargo.toml`. + +**This results in immediate compiler errors (e.g., undeclared associated functions like `Module::from_binary`).** + +### Architectural Root Cause +1. **Pulley is not a direct WASM interpreter.** Wasmtime's Pulley VM does not interpret raw, standard WebAssembly bytecode on the fly. Instead, WebAssembly bytecode must be compiled into **Pulley interpreter bytecode** (an optimized virtual machine instruction format). +2. **Cranelift is still the compiler.** The **Cranelift** backend is the compiler responsible for translating standard WebAssembly bytecode into Pulley interpreter bytecode. +3. **No Compiler = No Execution:** If `"cranelift"` is completely disabled, Wasmtime compiles with **zero compiler backends**. As a result, Wasmtime strips all dynamic rule-compilation APIs (such as `Module::from_binary`, `Module::new`, `Config::cranelift_opt_level`, and `Engine::unload_process_handlers`). +4. **The Solution:** You **must retain** `"cranelift"` and `"runtime"` features in your `lib/Cargo.toml` dependencies. The compiler backend is required to translate the generated rules into Pulley format, but the execution remains 100% compliant with non-executable page sandboxing. + +--- + +## Pre-Compiled & Integrated Binaries + +Prerelease-ready, size-optimized binaries built with Pulley and Link-Time Optimization (LTO) have been generated and integrated into this project's workspace for immediate linking or loading: + +* **x64 (64-bit Target):** Located under `docs/yara-x/bin/x64/` + * `yara_x_capi.dll` (Approx. 20.2 MB) — Standard Windows x64 DLL. + * `yara_x_capi.dll.lib` — Import library for compilation linkage. + * `yara_x_capi.pdb` — Full debug symbols (PDB format). +* **x32 (32-bit Target):** Located under `docs/yara-x/bin/x32/` + * `yara_x_capi.dll` (Approx. 16.8 MB) — Standard Windows x32 DLL. + * `yara_x_capi.dll.lib` — Import library for compilation linkage. + * `yara_x_capi.pdb` — Full debug symbols (PDB format). + +--- + +## Compilation Guide: Multi-Architecture Build Commands + +Use the following commands inside the `yara-x` directory to build the size-optimized release binaries manually. If `cargo` is not in your global system `PATH`, locate it at `C:\Users\Doome\.cargo\bin\cargo.exe`. + +### 1. Compile for x64 (64-bit Release with Pulley) +```powershell +& 'C:\Users\Doome\.cargo\bin\cargo.exe' build -p yara-x-capi --profile release-lto --target x86_64-pc-windows-msvc --features pulley +``` + +### 2. Compile for x32 (32-bit Release with Pulley) +```powershell +& 'C:\Users\Doome\.cargo\bin\cargo.exe' build -p yara-x-capi --profile release-lto --target i686-pc-windows-msvc --features pulley +``` + +--- + +## Developer Debugging Guide: How to Compile with Debug Symbols + +During development or troubleshooting, developers may need to inspect the C-API, check call stacks, or run a debugger (like WinDbg, VS Debugger, or `cdb.exe`). + +### Option A: Standard Debug Build (Recommended for active debugging) +To produce a fully unoptimized build containing full line information, function scopes, and variables with zero compiler inlining: +```powershell +# For 64-bit Debug: +& 'C:\Users\Doome\.cargo\bin\cargo.exe' build -p yara-x-capi --target x86_64-pc-windows-msvc --features pulley + +# For 32-bit Debug: +& 'C:\Users\Doome\.cargo\bin\cargo.exe' build -p yara-x-capi --target i686-pc-windows-msvc --features pulley +``` +* **Debug Symbols:** This creates `yara_x_capi.pdb` and `yara_x_capi.dll` inside `target/x86_64-pc-windows-msvc/debug/`. Because optimization is turned off (`opt-level = 0`), you can set breakpoints and inspect all local variables natively. + +### Option B: Optimized Release Build with Debug Symbols +If you need to analyze performance bottlenecks or debug issues that only manifest under optimized builds, you can force the release profile to retain full debug symbols. + +Add the following to the workspace `Cargo.toml` in `yara-x` (or pass it via environment variables): +```toml +[profile.release] +debug = true # Force full debug symbols generation even in release builds +``` +Then compile using the standard release command: +```powershell +& 'C:\Users\Doome\.cargo\bin\cargo.exe' build -p yara-x-capi --release --target x86_64-pc-windows-msvc --features pulley +``` +This will output an optimized DLL with a matching `.pdb` file inside `target/x86_64-pc-windows-msvc/release/`. diff --git a/yara-x/include/yara_x.h b/yara-x/include/yara_x.h new file mode 100644 index 00000000..ace19ece --- /dev/null +++ b/yara-x/include/yara_x.h @@ -0,0 +1,939 @@ +#ifndef YARA_X +#define YARA_X + +#pragma once + +/* Generated with cbindgen:0.29.4 */ + +// This file is autogenerated by cbindgen. Don't modify it manually. + +#include +#include +#include +#include +#include + + +// Flag passed to [`yrx_compiler_create`] for producing colorful error +// messages. +#define YRX_COLORIZE_ERRORS 1 + +// Flag passed to [`yrx_compiler_create`] that enables a more relaxed +// syntax check for regular expressions. +// +// YARA-X enforces stricter regular expression syntax compared to YARA. +// For instance, YARA accepts invalid escape sequences and treats them +// as literal characters (e.g., \R is interpreted as a literal 'R'). It +// also allows some special characters to appear unescaped, inferring +// their meaning from the context (e.g., `{` and `}` in `/foo{}bar/` are +// literal, but in `/foo{0,1}bar/` they form the repetition operator +// `{0,1}`). +// +// When this flag is set, YARA-X mimics YARA's behavior, allowing +// constructs that YARA-X doesn't accept by default. +#define YRX_RELAXED_RE_SYNTAX 2 + +// Flag passed to [`yrx_compiler_create`] for treating slow patterns as +// errors instead of warnings. +#define YRX_ERROR_ON_SLOW_PATTERN 4 + +// Flag passed to [`yrx_compiler_create`] for treating slow loops as +// errors instead of warnings. +#define YRX_ERROR_ON_SLOW_LOOP 8 + +// Flag passed to [`yrx_compiler_create`] for enabling optimizations. +// With this flag the compiler tries to optimize rule conditions by applying +// techniques like common subexpression elimination (CSE) and loop-invariant +// code motion (LICM). +#define YRX_ENABLE_CONDITION_OPTIMIZATION 16 + +// Flag passed to [`yrx_compiler_create`] for disabling `include` statements. +// With this flag, the compiler produces an error when `include` statements are +// encountered. +#define YRX_DISABLE_INCLUDES 32 + +// Error codes returned by functions in this API. +typedef enum YRX_RESULT { + // Everything was OK. + YRX_SUCCESS, + // A syntax error occurred while compiling YARA rules. + YRX_SYNTAX_ERROR, + // An error occurred while defining or setting a global variable. This may + // happen when a variable is defined twice and when you try to set a value + // that doesn't correspond to the variable's type. + YRX_VARIABLE_ERROR, + // An error occurred during a scan operation. + YRX_SCAN_ERROR, + // A scan operation was aborted due to a timeout. + YRX_SCAN_TIMEOUT, + // An error indicating that some of the arguments passed to a function is + // invalid. Usually indicates a nil pointer to a scanner or compiler. + YRX_INVALID_ARGUMENT, + // An error indicating that some of the strings passed to a function is + // not valid UTF-8. + YRX_INVALID_UTF8, + // An error indicating that a scanner that was already in multi-block + // mode has been used as a standard scanner. + YRX_INVALID_STATE, + // An error occurred while serializing/deserializing YARA rules. + YRX_SERIALIZATION_ERROR, + // An error returned when a rule doesn't have any metadata. + YRX_NO_METADATA, + // An error returned in cases where some API is not supported because the + // library was not built with the required features. + YRX_NOT_SUPPORTED, +} YRX_RESULT; + +// Types of metadata values. +typedef enum YRX_METADATA_TYPE { + YRX_I64, + YRX_F64, + YRX_BOOLEAN, + YRX_STRING, + YRX_BYTES, +} YRX_METADATA_TYPE; + +// A compiler that takes YARA source code and produces compiled rules. +typedef struct YRX_COMPILER YRX_COMPILER; + +// A pattern defined in a rule. +typedef struct YRX_PATTERN YRX_PATTERN; + +// A single YARA rule. +typedef struct YRX_RULE YRX_RULE; + +// A set of compiled YARA rules. +typedef struct YRX_RULES YRX_RULES; + +// A scanner that scans data with a set of compiled YARA rules. +typedef struct YRX_SCANNER YRX_SCANNER; + +// Represents a buffer with arbitrary data. +typedef struct YRX_BUFFER { + // Pointer to the data contained in the buffer. + uint8_t *data; + // Length of data in bytes. + size_t length; +} YRX_BUFFER; + +// Contains information about a pattern match. +typedef struct YRX_MATCH { + // Offset within the data where the match occurred. + size_t offset; + // Length of the match. + size_t length; +} YRX_MATCH; + +// Callback function passed to [`yrx_pattern_iter_matches`]. +// +// The callback is called by all matches found for a pattern, and it receives +// a pointer to a [`YRX_MATCH`] structure. This pointer is guaranteed to be +// valid while the callback function is being executed, but it will be freed +// after the callback function returns, so you cannot use the pointer, or any +// other pointer contained in the structure, outside the callback. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +typedef void (*YRX_MATCH_CALLBACK)(const struct YRX_MATCH *match_, + void *user_data); + +// Represents a metadata value that contains raw bytes. +typedef struct YRX_METADATA_BYTES { + // Number of bytes. + size_t length; + // Pointer to the bytes. + const uint8_t *data; +} YRX_METADATA_BYTES; + +// A metadata value. +typedef union YRX_METADATA_VALUE { + // Value if the metadata is I64. + int64_t i64; + // Value if the metadata is F64. + double f64; + // Value if the metadata is BOOLEAN. + bool boolean; + // Value if the metadata is STRING. + const char *string; + // Value if the metadata is BYTES. + struct YRX_METADATA_BYTES bytes; +} YRX_METADATA_VALUE; + +// A metadata entry. +typedef struct YRX_METADATA { + // Metadata identifier. + const char *identifier; + // Metadata type. + enum YRX_METADATA_TYPE value_type; + // Metadata value. + // + // This a union type, the variant that should be used is determined by the + // type indicated in `value_type`. + union YRX_METADATA_VALUE value; +} YRX_METADATA; + +// Callback function passed to [`yrx_rule_iter_metadata`]. +// +// The callback is called for each metadata in the rule, and receives a pointer +// to a [`YRX_METADATA`] structure. This pointer is guaranteed to be valid +// while the callback function is being executed, but it will be freed after +// the callback function returns, so you cannot use the pointer, or any other +// pointer contained in this structure, outside the callback. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +typedef void (*YRX_METADATA_CALLBACK)(const struct YRX_METADATA *metadata, + void *user_data); + +// Callback function passed to [`yrx_rule_iter_patterns`]. +// +// The callback is called for each pattern defined in the rule, and it receives +// a pointer to a [`YRX_PATTERN`] structure. This pointer is guaranteed to be +// valid while the callback function is being executed, but it will be freed +// after the callback function returns, so you cannot use this pointer, or +// any other pointer contained in the structure, outside the callback. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +typedef void (*YRX_PATTERN_CALLBACK)(const struct YRX_PATTERN *pattern, + void *user_data); + +// Callback function passed to [`yrx_rule_iter_tags`]. +// +// The callback is called for each tag defined in the rule, and it receives +// a pointer to a string with the tag name. This pointer is guaranteed to be +// valid while the callback function is being executed, but it will be freed +// after the callback function returns, so you cannot use this pointer, or +// any other pointer contained in the structure, outside the callback. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +typedef void (*YRX_TAG_CALLBACK)(const char *tag, + void *user_data); + +// Callback function passed to `yrx_scanner_on_matching_rule` or +// [`yrx_rules_iter`]. +// +// The callback receives a pointer to a rule, represented by a [`YRX_RULE`] +// structure. This pointer is guaranteed to be valid while the callback +// function is being executed, but it may be freed after the callback function +// returns, so you cannot use the pointer outside the callback. +// +// It also receives the `user_data` pointer that can point to arbitrary data +// owned by the user. +typedef void (*YRX_RULE_CALLBACK)(const struct YRX_RULE *rule, + void *user_data); + +// Callback function passed to [`yrx_rules_iter_imports`]. +// +// The callback is called for every module imported by the rules, and it +// receives a pointer to the module's name. This pointer is guaranteed to be +// valid while the callback function is being executed, but it will be freed +// after the callback function returns, so you cannot use the pointer outside +// the callback. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +typedef void (*YRX_IMPORT_CALLBACK)(const char *module_name, + void *user_data); + +// Callback function used when a YARA rule calls the console module. +// +// The callback function is invoked with a string representing the message +// being logged. The function can print the message to stdout, append it to a +// file, etc. If no callback is set these messages are ignored. +typedef void (*YRX_CONSOLE_CALLBACK)(const char *message); + +// Callback function passed to [`yrx_scanner_iter_slowest_rules`]. +// +// The callback function receives pointers to the namespace and rule name, +// and two float numbers with the time spent by the rule matching patterns +// and executing its condition. The pointers are valid as long as the callback +// function is being executed, but will be freed after the callback returns. +// +// The callback also receives a `user_data` pointer that can point to arbitrary +// data owned by the user. +// +// Requires the `rules-profiling` feature. +typedef void (*YRX_SLOWEST_RULES_CALLBACK)(const char *namespace_, + const char *rule, + double pattern_matching_time, + double condition_exec_time, + void *user_data); + +// Returns the error message for the most recent function in this API +// invoked by the current thread. +// +// The returned pointer is only valid until this thread calls some other +// function, as it can modify the last error and render the pointer to +// a previous error message invalid. Also, the pointer will be null if +// the most recent function was successfully. +const char *yrx_last_error(void); + +// Destroys a [`YRX_BUFFER`] object. +void yrx_buffer_destroy(struct YRX_BUFFER *buf); + +// Compiles YARA source code and creates a [`YRX_RULES`] object that contains +// the compiled rules. +// +// The rules must be destroyed with [`yrx_rules_destroy`]. +enum YRX_RESULT yrx_compile(const char *src, + struct YRX_RULES **rules); + +// Finalizes YARA-X. +// +// This function only needs to be called in a very specific scenario: +// when YARA-X is used as a dynamically loaded library (`.so`, `.dll`, +// `.dylib`) **and** that library must be unloaded at runtime. +// +// Its primary purpose is to remove the process-wide signal handlers +// installed by the [wasmtime] engine. +// +// # Safety +// +// This function is **unsafe** to call under normal circumstances. It has +// strict preconditions that must be met: +// +// - There must be no other active `wasmtime` engines in the process. This +// applies not only to clones of the engine used by YARA-X (which should not +// exist because YARA-X uses a single copy of its engine), but to *any* +// `wasmtime` engine, since global state shared by all engines is torn +// down. +// +// - On Unix platforms, no other signal handlers may have been installed +// for signals intercepted by `wasmtime`. If other handlers have been set, +// `wasmtime` cannot reliably restore the original state, which may lead +// to undefined behavior. +// +// [wasmtime]: https://wasmtime.dev/ +void yrx_finalize(void); + +// Creates a [`YRX_COMPILER`] object. +enum YRX_RESULT yrx_compiler_create(uint32_t flags, + struct YRX_COMPILER **compiler); + +// Destroys a [`YRX_COMPILER`] object. +void yrx_compiler_destroy(struct YRX_COMPILER *compiler); + +// Adds a YARA source code to be compiled. +// +// This function can be called multiple times. +enum YRX_RESULT yrx_compiler_add_source(struct YRX_COMPILER *compiler, + const char *src); + +// Adds a YARA source code to be compiled, specifying an origin for the +// source code. +// +// This function is similar to [`yrx_compiler_add_source`], but in addition +// to the source code itself it provides a string that identifies the origin +// of the code, usually the file path from where the source was obtained. +// +// This origin is shown in error reports. +enum YRX_RESULT yrx_compiler_add_source_with_origin(struct YRX_COMPILER *compiler, + const char *src, + const char *origin); + +// Adds a directory to the list of directories where the compiler should +// look for included files. +// +// When an `include` statement is found, the compiler looks for the included +// file in the directories added with this function, in the order they were +// added. +// +// If this function is not called, the compiler will only look for included +// files in the current directory. +enum YRX_RESULT yrx_compiler_add_include_dir(struct YRX_COMPILER *compiler, + const char *dir); + +// Tell the compiler that a YARA module is not supported. +// +// Import statements for ignored modules will be ignored without errors but a +// warning will be issued. Any rule that make use of an ignored module will be +// ignored, while the rest of rules that don't rely on that module will be +// correctly compiled. +enum YRX_RESULT yrx_compiler_ignore_module(struct YRX_COMPILER *compiler, + const char *module); + +// Sets the maximum number of warnings. +// +// The compiler will report only the first `n` warnings. +enum YRX_RESULT yrx_compiler_max_warnings(struct YRX_COMPILER *compiler, + size_t n); + +// Enables a feature on this compiler. +// +// When defining the structure of a module in a `.proto` file, you can +// specify that certain fields are accessible only when one or more +// features are enabled. For example, the snippet below shows the +// definition of a field named `requires_foo_and_bar`, which can be +// accessed only when both features "foo" and "bar" are enabled. +// +// ```protobuf +// optional uint64 requires_foo_and_bar = 500 [ +// (yara.field_options) = { +// acl: [ +// { +// allow_if: "foo", +// error_title: "foo is required", +// error_label: "this field was used without foo" +// }, +// { +// allow_if: "bar", +// error_title: "bar is required", +// error_label: "this field was used without bar" +// } +// ] +// } +// ]; +// ``` +// +// If some of the required features are not enabled, using this field in +// a YARA rule will cause an error while compiling the rules. The error +// looks like: +// +// ```text +// error[E034]: foo is required +// --> line:5:29 +// | +// 5 | test_proto2.requires_foo_and_bar == 0 +// | ^^^^^^^^^^^^^^^^^^^^ this field was used without foo +// | +// ``` +// +// Notice that both the title and label in the error message are defined +// in the .proto file. +// +// # Important +// +// This API is hidden from the public documentation because it is unstable +// and subject to change. +enum YRX_RESULT yrx_compiler_enable_feature(struct YRX_COMPILER *compiler, + const char *feature); + +// Tell the compiler that a YARA module can't be used. +// +// Import statements for the banned module will cause an error. The error +// message can be customized by using the given error title and message. +// +// If this function is called multiple times with the same module name, +// the error title and message will be updated. +enum YRX_RESULT yrx_compiler_ban_module(struct YRX_COMPILER *compiler, + const char *module, + const char *error_title, + const char *error_msg); + +// Creates a new namespace. +// +// Further calls to `yrx_compiler_add_source` will put the rules under the +// newly created namespace. +// +// The `namespace` argument must be pointer to null-terminated UTF-8 string. +// If the string is not valid UTF-8 the result is an `INVALID_ARGUMENT` error. +enum YRX_RESULT yrx_compiler_new_namespace(struct YRX_COMPILER *compiler, + const char *namespace_); + +// Defines a global variable of string type and sets its initial value. +enum YRX_RESULT yrx_compiler_define_global_str(struct YRX_COMPILER *compiler, + const char *ident, + const char *value); + +// Defines a global variable of bool type and sets its initial value. +enum YRX_RESULT yrx_compiler_define_global_bool(struct YRX_COMPILER *compiler, + const char *ident, + bool value); + +// Defines a global variable of integer type and sets its initial value. +enum YRX_RESULT yrx_compiler_define_global_int(struct YRX_COMPILER *compiler, + const char *ident, + int64_t value); + +// Defines a global variable of float type and sets its initial value. +enum YRX_RESULT yrx_compiler_define_global_float(struct YRX_COMPILER *compiler, + const char *ident, + double value); + +// Defines a global variable from a JSON-encoded string. +// +// This is best for complex types like maps and arrays. For simple types +// (e.g., booleans, integers, strings), prefer dedicated functions to avoid +// the overhead of JSON deserialization. +// +// When defining a map, keys must be of string type, and values can be +// any of the types supported by YARA, including other maps. Arrays must be +// homogeneous (all elements must be the same type). +enum YRX_RESULT yrx_compiler_define_global_json(struct YRX_COMPILER *compiler, + const char *ident, + const char *value); + +// Returns the errors encountered during the compilation in JSON format. +// +// In the address indicated by the `buf` pointer, the function will copy a +// `YRX_BUFFER*` pointer. The `YRX_BUFFER` structure represents a buffer +// that contains the JSON representation of the compilation errors. +// +// The JSON consists on an array of objects, each object representing a +// compilation error. The object has the following fields: +// +// * type: A string that describes the type of error. +// * code: Error code (e.g: "E009"). +// * title: Error title (e.g: "unknown identifier `foo`"). +// * labels: Array of labels. +// * text: The full text of the error report, as shown by the command-line tool. +// +// Here is an example: +// +// ```json +// [ +// { +// "type": "UnknownIdentifier", +// "code": "E009", +// "title": "unknown identifier `foo`", +// "labels": [ +// { +// "level": "error", +// "code_origin": null, +// "span": {"start":25,"end":28}, +// "text": "this identifier has not been declared" +// } +// ], +// "text": "... ..." +// } +// ] +// ``` +// +// The [`YRX_BUFFER`] must be destroyed with [`yrx_buffer_destroy`]. +enum YRX_RESULT yrx_compiler_errors_json(struct YRX_COMPILER *compiler, + struct YRX_BUFFER **buf); + +// Returns the warnings encountered during the compilation in JSON format. +// +// In the address indicated by the `buf` pointer, the function will copy a +// `YRX_BUFFER*` pointer. The `YRX_BUFFER` structure represents a buffer +// that contains the JSON representation of the warnings. +// +// The JSON consists on an array of objects, each object representing a +// warning. The object has the following fields: +// +// * type: A string that describes the type of warning. +// * code: Warning code (e.g: "slow_pattern"). +// * title: Error title (e.g: "slow pattern"). +// * labels: Array of labels. +// * text: The full text of the warning report, as shown by the command-line tool. +// +// Here is an example: +// +// ```json +// [ +// { +// "type": "SlowPattern", +// "code": "slow_pattern", +// "title": "slow pattern", +// "labels": [ +// { +// "level": "warning", +// "code_origin": null, +// "span": {"start":25,"end":28}, +// "text": "this pattern may slow down the scan" +// } +// ], +// "text": "... ..." +// } +// ] +// ``` +// +// The [`YRX_BUFFER`] must be destroyed with [`yrx_buffer_destroy`]. +enum YRX_RESULT yrx_compiler_warnings_json(struct YRX_COMPILER *compiler, + struct YRX_BUFFER **buf); + +// Builds the source code previously added to the compiler. +// +// After calling this function the compiler is reset to its initial state, +// (i.e: the state it had after returning from yrx_compiler_create) you can +// keep using it by adding more sources and calling this function again. +struct YRX_RULES *yrx_compiler_build(struct YRX_COMPILER *compiler); + +// Returns the name of the pattern represented by [`YRX_PATTERN`]. +// +// Arguments `ident` and `len` are output parameters that receive pointers +// to a `const uint8_t*` and `size_t`, where this function will leave a pointer +// to the rule's name and its length, respectively. The rule's name is *NOT* +// null-terminated, and the pointer will be valid as long as the `YRX_RULES` +// object that contains the pattern is not freed. The name is guaranteed to be +// a valid UTF-8 string. +enum YRX_RESULT yrx_pattern_identifier(const struct YRX_PATTERN *pattern, + const uint8_t **ident, + size_t *len); + +// Iterates over the matches of a pattern, calling the callback with a pointer +// to a [`YRX_MATCH`] structure for each pattern. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_MATCH_CALLBACK`] for more details. +enum YRX_RESULT yrx_pattern_iter_matches(const struct YRX_PATTERN *pattern, + YRX_MATCH_CALLBACK callback, + void *user_data); + +// Returns the name of the rule represented by [`YRX_RULE`]. +// +// Arguments `ident` and `len` are output parameters that receive pointers +// to a `const uint8_t*` and `size_t`, where this function will leave a pointer +// to the rule's name and its length, respectively. The rule's name is *NOT* +// null-terminated, and the pointer will be valid as long as the `YRX_RULES` +// object that contains the rule is not freed. The name is guaranteed to be a +// valid UTF-8 string. +enum YRX_RESULT yrx_rule_identifier(const struct YRX_RULE *rule, + const uint8_t **ident, + size_t *len); + +// Returns the namespace of the rule represented by [`YRX_RULE`]. +// +// Arguments `ns` and `len` are output parameters that receive pointers to a +// `const uint8_t*` and `size_t`, where this function will leave a pointer +// to the rule's namespace and its length, respectively. The namespace is *NOT* +// null-terminated, and the pointer will be valid as long as the `YRX_RULES` +// object that contains the rule is not freed. The namespace is guaranteed to +// be a valid UTF-8 string. +enum YRX_RESULT yrx_rule_namespace(const struct YRX_RULE *rule, + const uint8_t **ns, + size_t *len); + +// Iterates over the metadata of a rule, calling the callback with a pointer +// to a [`YRX_METADATA`] structure for each metadata in the rule. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_METADATA_CALLBACK`] for more details. +enum YRX_RESULT yrx_rule_iter_metadata(const struct YRX_RULE *rule, + YRX_METADATA_CALLBACK callback, + void *user_data); + +// Iterates over the patterns in a rule, calling the callback with a pointer +// to a [`YRX_PATTERN`] structure for each pattern. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_PATTERN_CALLBACK`] for more details. +enum YRX_RESULT yrx_rule_iter_patterns(const struct YRX_RULE *rule, + YRX_PATTERN_CALLBACK callback, + void *user_data); + +// Iterates over the tags in a rule, calling the callback with a pointer +// to each tag. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_TAG_CALLBACK`] for more details. +enum YRX_RESULT yrx_rule_iter_tags(const struct YRX_RULE *rule, + YRX_TAG_CALLBACK callback, + void *user_data); + +// Iterates over the compiled rules, calling the callback function for each +// rule. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_RULE_CALLBACK`] for more details. +enum YRX_RESULT yrx_rules_iter(const struct YRX_RULES *rules, + YRX_RULE_CALLBACK callback, + void *user_data); + +// Returns the total number of rules. +// +// Returns -1 in case of error. +int yrx_rules_count(struct YRX_RULES *rules); + +// Serializes the rules as a sequence of bytes. +// +// In the address indicated by the `buf` pointer, the function will copy a +// `YRX_BUFFER*` pointer. The `YRX_BUFFER` structure represents a buffer +// that contains the serialized rules. This structure has a pointer to the +// data itself, and its length. +// +// The [`YRX_BUFFER`] must be destroyed with `yrx_buffer_destroy`. +enum YRX_RESULT yrx_rules_serialize(const struct YRX_RULES *rules, + struct YRX_BUFFER **buf); + +// Deserializes the rules from a sequence of bytes produced by +// [`yrx_rules_serialize`]. +enum YRX_RESULT yrx_rules_deserialize(const uint8_t *data, + size_t len, + struct YRX_RULES **rules); + +// Iterates over the modules imported by the rules, calling the callback with +// the name of each imported module. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. +// +// See [`YRX_IMPORT_CALLBACK`] for more details. +enum YRX_RESULT yrx_rules_iter_imports(const struct YRX_RULES *rules, + YRX_IMPORT_CALLBACK callback, + void *user_data); + +// Destroys a [`YRX_RULES`] object. +void yrx_rules_destroy(struct YRX_RULES *rules); + +// Creates a [`YRX_SCANNER`] object that can be used for scanning data with +// the provided [`YRX_RULES`]. +// +// It's ok to pass the same [`YRX_RULES`] to multiple scanners, and use each +// scanner from a different thread. The scanner can be used as many times as +// you want, and it must be destroyed with [`yrx_scanner_destroy`]. Also, the +// scanner is valid as long as the rules are not destroyed, so, always destroy +// the [`YRX_SCANNER`] object before the [`YRX_RULES`] object. +enum YRX_RESULT yrx_scanner_create(const struct YRX_RULES *rules, + struct YRX_SCANNER **scanner); + +// Destroys a [`YRX_SCANNER`] object. +void yrx_scanner_destroy(struct YRX_SCANNER *scanner); + +// Sets a timeout (in seconds) for scan operations. +// +// The scan functions will return a timeout error once the provided timeout +// duration has elapsed. The scanner will make every effort to stop promptly +// after the designated timeout duration. However, in some cases, particularly +// with rules containing only a few patterns, the scanner could potentially +// continue running for a longer period than the specified timeout. +enum YRX_RESULT yrx_scanner_set_timeout(struct YRX_SCANNER *scanner, + uint64_t timeout); + +// Enables or disables fast scan mode for the scanner. +// +// In fast scan mode, the scanner avoids tracking matches for patterns when it +// is not necessary (e.g. when a rule condition only performs a simple boolean +// check `$a`). +// +// Note that using fast scan mode implies that not all matches will be +// reported. For instance, when iterating matches using [`yara_x::ScanResults`], +// you won't get all occurrences of the pattern in the file, only the first +// one. +enum YRX_RESULT yrx_scanner_fast_scan(struct YRX_SCANNER *scanner, + bool yes); + +// Sets the maximum number of matches per pattern. +// +// When a pattern reaches the maximum number of matches it won't produce more +// matches. +enum YRX_RESULT yrx_scanner_max_matches_per_pattern(struct YRX_SCANNER *scanner, + size_t n); + +// Scans a data buffer. +// +// `data` can be null as long as `len` is 0. In such cases its handled as +// empty data. Some YARA rules (i.e: `rule dummy { condition: true }`) can +// match even with empty data. +enum YRX_RESULT yrx_scanner_scan(struct YRX_SCANNER *scanner, + const uint8_t *data, + size_t len); + +// Scans a file. +// +// This function is similar to `yrx_scanner_scan`, but it receives a file +// path instead of data to be scanned. +enum YRX_RESULT yrx_scanner_scan_file(struct YRX_SCANNER *scanner, + const char *path); + +// Scans a block of data. +// +// This function is designed for scenarios where the data to be scanned is not +// available as a single contiguous block of memory, but rather arrives in +// smaller, discrete blocks, allowing for incremental scanning. +// +// Each call to this function scans a block of data. The `base` argument +// specifies the offset of the current block within the overall data being +// scanned. In most cases you will want to call this function multiple times, +// providing a different block on each call. +// +// Once this function is called for a scanner, it enters block scanning mode +// and any subsequent call to [`yrx_scanner_scan`] will fail with +// [`YRX_RESULT::YRX_INVALID_STATE`]. Once the scanner is in block scanning +// mode it can be used in that mode only. +// +// When all blocks have been scanned, you must call [`yrx_scanner_finish`]. +// +// # Limitations of Block Scanning +// +// Block scanning works by analyzing data in chunks rather than as a whole +// file. This makes it useful for streaming or memory-constrained scenarios, +// but it comes with important limitations compared to standard scanning: +// +// 1) Modules won't work. Parsers for structured formats (e.g., PE, ELF) +// require access to the entire file and cannot be applied in block +// scanning mode. +// 2) Other modules like `hash` won't work either, as they require access to +// all the scanned data during the evaluation of the rule's condition, +// something that can't be guaranteed in block scanning mode. The hash +// functions will return `undefined` when used in a multi-block context. +// 3) Built-in functions like `uint8`, `uint16`, `uint32`, etc., have the +// same limitation. They also return `undefined` in block scanning mode. +// 4) The `filesize` keyword returns `undefined` in block scanning mode. +// 5) Patterns won't match across block boundaries. Every match will be +// completely contained within one of the blocks. +// +// All these limitations imply that in block scanning mode you should only +// use rules that rely on text, hex or regex patterns. +// +// # Data Consistency in Overlapping Blocks +// +// When [`yrx_scanner_scan_block`] is invoked multiple times with different +// blocks that may overlap, the user is responsible for ensuring data +// consistency. This means that if the same region of the original data is +// present in two or more overlapping blocks, the content of that region must +// be identical across all calls to `scan`. +// +// Generally speaking, the scanner does not verify this consistency and +// assumes the user provides accurate and consistent data. In debug releases +// the scanner may try to verify this consistency, but only when some pattern +// matches in the overlapping region. +enum YRX_RESULT yrx_scanner_scan_block(struct YRX_SCANNER *scanner, + size_t base, + const uint8_t *data, + size_t len); + +// Finalizes the scan of a set of memory blocks. +// +// This function must be used in conjunction with [`yrx_scanner_scan_block`] +// when scanning data in blocks. After all data blocks have been scanned, this +// functions evaluates the conditions of the YARA rules and produces the final +// scan results. +// +// After this function returns, the scanner is ready to be used again for +// scanning a new set of memory blocks. However, the scanner remains in block +// scanning mode and can't be used for normal scanning. +enum YRX_RESULT yrx_scanner_finish(struct YRX_SCANNER *scanner); + +// Sets a callback function that is called by the scanner for each rule that +// matched during a scan. +// +// The `user_data` pointer can be used to provide additional context to your +// callback function. If the callback is not set, the scanner doesn't notify +// about matching rules. +// +// See [`YRX_RULE_CALLBACK`] for more details. +enum YRX_RESULT yrx_scanner_on_matching_rule(struct YRX_SCANNER *scanner, + YRX_RULE_CALLBACK callback, + void *user_data); + +// Specifies the output data structure for a module. +// +// Each YARA module generates an output consisting of a data structure that +// contains information about the scanned file. This data structure is represented +// by a Protocol Buffer. Typically, you won't need to provide this output data +// yourself, as the YARA module automatically generates different outputs for +// each file it scans. +// +// However, there are two scenarios in which you may want to provide the output +// for a module yourself: +// +// 1) When the module does not produce any output on its own. +// 2) When you already know the output of the module for the upcoming file to +// be scanned, and you prefer to reuse this data instead of generating it +// again. +// +// Case 1) applies to certain modules lacking a main function, thus incapable of +// producing any output on their own. For such modules, you must set the output +// before scanning the associated data. Since the module's output typically varies +// with each scanned file, you need to call [yrx_scanner_set_module_output] prior +// to each invocation of [yrx_scanner_scan]. Once [yrx_scanner_scan] is executed, +// the module's output is consumed and will be empty unless set again before the +// subsequent call. +// +// Case 2) applies when you have previously stored the module's output for certain +// scanned data. In such cases, when rescanning the data, you can utilize this +// function to supply the module's output, thereby preventing redundant computation +// by the module. This optimization enhances performance by eliminating the need +// for the module to reparse the scanned data. +// +// The `name` argument is either a YARA module name (i.e: "pe", "elf", "dotnet", +// etc.) or the fully-qualified name of the protobuf message associated to +// the module. It must be a valid UTF-8 string. +// +// If the scanner is in block scanning mode this function returns `YRX_INVALID_STATE`. +enum YRX_RESULT yrx_scanner_set_module_output(struct YRX_SCANNER *scanner, + const char *name, + const uint8_t *data, + size_t len); + +// Specifies metadata for a module. +// +// Since the module's output typically varies with each scanned file, you need to +// call [yrx_scanner_set_module_data] prior to each invocation of +// [yrx_scanner_scan]. Once [yrx_scanner_scan] is executed, the module's metadata +// is consumed and will be empty unless set again before the subsequent call. +// +// The `name` argument is the name of a YARA module. It must be a valid UTF-8 string. +// +// The `name` as well as `data` must be valid from the time they are used as arguments +// of this function until the scan is executed. +// +// If the scanner is in block scanning mode this function returns `YRX_INVALID_STATE`. +enum YRX_RESULT yrx_scanner_set_module_data(struct YRX_SCANNER *scanner, + const char *name, + const uint8_t *data, + size_t len); + +// Sets the value of a global variable of type string. +enum YRX_RESULT yrx_scanner_set_global_str(struct YRX_SCANNER *scanner, + const char *ident, + const char *value); + +// Sets the value of a global variable of type bool. +enum YRX_RESULT yrx_scanner_set_global_bool(struct YRX_SCANNER *scanner, + const char *ident, + bool value); + +// Sets the value of a global variable of type int. +enum YRX_RESULT yrx_scanner_set_global_int(struct YRX_SCANNER *scanner, + const char *ident, + int64_t value); + +// Sets the value of a global variable of type float. +enum YRX_RESULT yrx_scanner_set_global_float(struct YRX_SCANNER *scanner, + const char *ident, + double value); + +// Sets the value of a global variable from a JSON-encoded string. +// +// This is best for complex types like maps and arrays. For simple types +// (e.g., booleans, integers, strings), prefer dedicated functions to avoid +// the overhead of JSON deserialization. +// +// The type of the JSON-encoded value must match the type of the variable +// as it was defined. +enum YRX_RESULT yrx_scanner_set_global_json(struct YRX_SCANNER *scanner, + const char *ident, + const char *value); + +// Sets the callback for console module. +enum YRX_RESULT yrx_scanner_on_console_log(struct YRX_SCANNER *scanner, + YRX_CONSOLE_CALLBACK callback); + +// Iterates over the slowest N rules, calling the callback for each rule. +// +// Requires the `rules-profiling` feature, otherwise returns +// `YRX_RESULT::NOT_SUPPORTED`. +// +// See [`YRX_SLOWEST_RULES_CALLBACK`] for more details. +enum YRX_RESULT yrx_scanner_iter_slowest_rules(struct YRX_SCANNER *scanner, + size_t n, + YRX_SLOWEST_RULES_CALLBACK callback, + void *user_data); + +// Clears all accumulated profiling data. +// +// This resets the profiling data collected during rule execution across +// scanned files. Use this to start a new profiling session, ensuring the +// results reflect only the data gathered after this method is called. +// +// Requires the `rules-profiling` feature, otherwise returns +// `YRX_RESULT::NOT_SUPPORTED`. +// +enum YRX_RESULT yrx_scanner_clear_profiling_data(struct YRX_SCANNER *scanner); + +#endif /* YARA_X */