Files
Andrea Cristaldi 492ffc428b First commit
2025-08-28 17:14:35 +02:00

159 lines
6.6 KiB
C#

// DefenderRuleParser
// Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser
// This project is licensed under the Apache 2.0 License.
using DefenderRuleParser2;
using DefenderRuleParser2.Models;
using DefenderRuleParser2.Parsers.Wildcards; // 0x90-based wildcard engine (WildcardPattern/TokLiteral/TokWild)
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace DefenderRuleParser2.Parsers
{
/// <summary>
/// Parser for SIGNATURE_TYPE_FOLDERNAME (hex-dump driven, .NET 4.7-safe).
///
/// Empirical layout from hex dumps:
/// u16 Threshold
/// u8[] Payload → folder name text (ASCII or UTF-16LE), sometimes with 0x90-based wildcard sequences.
///
/// Behavior:
/// - Read threshold, then the remaining payload as raw bytes.
/// - If the payload appears to use Defender-style 0x90 wildcard constructs, tokenize it and render:
/// · Human-friendly string (ASCII literals + [=WILD]/[=WILD:0xNN])
/// · Conservative YARA-like hex (ranges for wildcards)
/// Also log an ASCII-only view made from literal tokens for quick visual checks.
/// - Otherwise, decode payload as ASCII/UTF-16LE by heuristic and log it plainly.
/// - Persist ONE human-friendly string per signature in SignatureEntry.Pattern
/// (wildcarded human form when present, else decoded text) to keep JSON/HTML stable.
/// </summary>
public class FolderNameParser : ISignatureParser
{
public void Parse(BinaryReader reader, int size, uint threatId)
{
long offset = reader.BaseStream.Position;
try
{
byte[] buffer = reader.ReadBytes(size);
if (buffer == null || buffer.Length < 2)
{
Logger.Info("[FOLDERNAME] ❗ Signature too short.");
return;
}
using (var ms = new MemoryStream(buffer))
using (var br = new BinaryReader(ms))
{
ushort threshold = br.ReadUInt16();
byte[] payload = ReadRest(br);
bool isWild = LooksLikeWildcardPattern(payload);
string humanOut;
if (isWild)
{
int consumed;
bool hadTerm;
var tokens = WildcardPattern.Tokenize(payload, 0, payload.Length, out consumed, out hadTerm);
string human = WildcardPattern.RenderHuman(tokens, Encoding.ASCII);
string yara = WildcardPattern.RenderYaraHex(tokens);
// ASCII-only (concat literal tokens) for logging
var sbAscii = new StringBuilder();
for (int i = 0; i < tokens.Count; i++)
{
var lit = tokens[i] as TokLiteral;
if (lit != null && lit.Bytes != null && lit.Bytes.Length > 0)
sbAscii.Append(Encoding.ASCII.GetString(lit.Bytes));
}
string asciiOnly = sbAscii.ToString();
Logger.Info(string.Format("[FOLDERNAME] Threat ID: {0}, Threshold: {1} (wildcarded)", threatId, threshold));
if (!string.IsNullOrEmpty(asciiOnly)) Logger.Info(" · ASCII-only : " + asciiOnly);
if (!string.IsNullOrEmpty(human)) Logger.Info(" · WILD/Human : " + human);
if (!string.IsNullOrEmpty(yara)) Logger.Info(" · WILD/YARA : " + yara);
humanOut = !string.IsNullOrEmpty(human) ? human : asciiOnly;
}
else
{
string folderName = DecodeBestEffort(payload);
Logger.Info(string.Format("[FOLDERNAME] Threat ID: {0}, Threshold: {1}", threatId, threshold));
Logger.Info(" > Folder: " + folderName);
humanOut = folderName;
}
if (!string.IsNullOrWhiteSpace(humanOut) &&
ThreatDatabase.TryGetThreat(threatId, out var threat))
{
threat.Signatures.Add(new SignatureEntry
{
Type = "SIGNATURE_TYPE_FOLDERNAME",
Offset = offset,
Pattern = new List<string> { humanOut },
Parsed = true,
ConditionType = (threshold > 1) ? "MIN_MATCHES" : "PRESENT",
ConditionValue = Math.Max(1, (int)threshold)
});
}
}
}
catch (Exception ex)
{
Logger.Error(string.Format("[!] FOLDERNAME Error parsing at offset 0x{0:X}: {1}", offset, ex.Message));
}
finally
{
reader.BaseStream.Seek(offset + size, SeekOrigin.Begin);
}
}
// ---------- helpers ----------
private static byte[] ReadRest(BinaryReader br)
{
int left = (int)(br.BaseStream.Length - br.BaseStream.Position);
if (left <= 0) return new byte[0];
return br.ReadBytes(left);
}
/// <summary>
/// Detect Defender-style 0x90 wildcard usage:
/// - 0x90 0x00 (terminator), 0x90 0x90 (escape), or 0x90 followed by an opcode-like byte (< 0x32).
/// </summary>
private static bool LooksLikeWildcardPattern(byte[] data)
{
if (data == null || data.Length < 2) return false;
for (int i = 0; i < data.Length - 1; i++)
{
if (data[i] != 0x90) continue;
byte n = data[i + 1];
if (n == 0x00 || n == 0x90 || n < 0x32) return true;
}
return false;
}
/// <summary>
/// Decode bytes as ASCII vs UTF-16LE based on NUL distribution; trim trailing NULs/whitespace.
/// </summary>
private static string DecodeBestEffort(byte[] bytes)
{
if (bytes == null || bytes.Length == 0) return string.Empty;
int nullOdd = 0;
for (int i = 1; i < bytes.Length; i += 2)
if (bytes[i] == 0x00) nullOdd++;
bool utf16 = (bytes.Length >= 4) && (nullOdd >= bytes.Length / 4);
string s = utf16 ? Encoding.Unicode.GetString(bytes) : Encoding.UTF8.GetString(bytes);
return s.TrimEnd('\0').Trim();
}
}
}