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

241 lines
9.0 KiB
C#

// DefenderRuleParser
// Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser
// This project is licensed under the Apache 2.0 License.
/*
* Summary: Generic weighted-pattern parser (HSTR-like families, ARHSTR/PEHSTR/CMDHSTR/MACROHSTR).
* Origin: dump-driven; header (threshold, count) and subrule layout derived from hex.
* Role: Tokenizes wildcard-encoded bytes (0x90 scheme), falls back to string decoding, and preserves weights/thresholds.
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using DefenderRuleParser2.Models;
using DefenderRuleParser2.Parsers.Wildcards;
namespace DefenderRuleParser2.Parsers
{
public sealed class GenericWeightedPatternParser : ISignatureParser
{
private readonly string _signatureType;
public GenericWeightedPatternParser(string signatureType)
{
_signatureType = signatureType;
}
public void Parse(BinaryReader reader, int size, uint threatId)
{
long offset = reader.BaseStream.Position;
var patterns = new List<string>();
var logic = new List<SubRuleLogic>();
try
{
byte[] buffer = reader.ReadBytes(size);
if (buffer == null || buffer.Length != size)
{
Logger.Warn($"[{_signatureType}] Short read ({(buffer == null ? 0 : buffer.Length)}/{size}) @0x{offset:X}");
reader.BaseStream.Seek(offset + size, SeekOrigin.Begin);
return;
}
using (var ms = new MemoryStream(buffer))
using (var br = new BinaryReader(ms))
{
if (br.BaseStream.Length < 7)
{
Logger.Warn($"[{_signatureType}] Size too small ({size}) @0x{offset:X}");
reader.BaseStream.Seek(offset + size, SeekOrigin.Begin);
return;
}
ushort unknown = br.ReadUInt16();
ushort threshold = br.ReadUInt16();
ushort subRuleCount = br.ReadUInt16();
byte ctrl = br.ReadByte();
Logger.Info($"[{_signatureType}] Threat ID: {threatId}, Threshold: {threshold}, SubRules: {subRuleCount}");
for (int i = 0; i < subRuleCount && br.BaseStream.Position < br.BaseStream.Length; i++)
{
long subStart = br.BaseStream.Position;
if (subStart + 3 > br.BaseStream.Length)
{
Logger.Warn($" ! SubRule header #{i + 1} truncated.");
break;
}
ushort weight = br.ReadUInt16();
if (br.BaseStream.Position >= br.BaseStream.Length)
{
Logger.Warn($" ! SubRule header #{i + 1} missing type/len.");
break;
}
byte b1 = br.ReadByte();
byte typeByte = 0xFF;
byte ruleLen;
if (b1 <= 0x02)
{
typeByte = b1;
if (br.BaseStream.Position >= br.BaseStream.Length)
{
Logger.Warn($" ! SubRule header #{i + 1} missing length after type.");
break;
}
ruleLen = br.ReadByte();
}
else
{
ruleLen = b1;
}
bool hadAsciiTag = false;
if (br.BaseStream.Position + 2 <= br.BaseStream.Length)
{
byte t0 = br.ReadByte();
byte t1 = br.ReadByte();
if (t0 == 0x80 && t1 == 0x01)
{
hadAsciiTag = true;
}
else
{
br.BaseStream.Position -= 2;
}
}
if (br.BaseStream.Position + ruleLen > br.BaseStream.Length)
{
Logger.Warn($" ! SubRule #{i + 1} truncated: size={ruleLen} beyond buffer.");
break;
}
byte[] patternBytes = br.ReadBytes(ruleLen);
string renderedHuman = null;
string renderedYara = null;
bool usedTokenizer = false;
if (!hadAsciiTag && LooksLikeWildcardPattern(patternBytes))
{
int consumed; bool hadTerm;
var tokens = WildcardPattern.Tokenize(patternBytes, 0, patternBytes.Length, out consumed, out hadTerm);
if (tokens != null && tokens.Count > 0)
{
renderedHuman = WildcardPattern.RenderHuman(tokens);
renderedYara = WildcardPattern.RenderYaraHex(tokens);
usedTokenizer = true;
Logger.Info($" > SubRule #{i + 1}: Weight={weight}, WildTokenized=true, Len={ruleLen}, Term={(hadTerm ? "yes" : "no")}");
Logger.Info($" · Human : {renderedHuman}");
Logger.Info($" · YARA hex: {renderedYara}");
}
}
if (!usedTokenizer)
{
string decoded = IsLikelyUtf16(patternBytes)
? Encoding.Unicode.GetString(patternBytes).TrimEnd('\0')
: Encoding.UTF8.GetString(patternBytes).TrimEnd('\0');
renderedHuman = decoded;
string suffix =
hadAsciiTag ? " (ascii-tag)" :
(typeByte <= 0x02 ? $" (type=0x{typeByte:X2})" : "");
Logger.Info($" > SubRule #{i + 1}: Weight={weight}, Pattern=\"{decoded}\"{suffix}");
}
patterns.Add(renderedHuman ?? string.Empty);
logic.Add(new SubRuleLogic
{
Pattern = renderedHuman ?? string.Empty,
Weight = weight,
Control = (typeByte <= 0x02) ? typeByte : (byte)0
});
}
if (ThreatDatabase.TryGetThreat(threatId, out var threat))
{
threat.Signatures.Add(new SignatureEntry
{
Type = _signatureType,
Offset = offset,
Pattern = patterns,
Parsed = true,
Logic = new SignatureLogic
{
Threshold = threshold,
SubRules = logic
}
});
}
}
}
catch (Exception ex)
{
Logger.Error($"[!] {_signatureType} Error parsing at offset 0x{offset:X}: {ex.Message}");
}
finally
{
reader.BaseStream.Seek(offset + size, SeekOrigin.Begin);
}
}
private bool LooksLikeWildcardPattern(byte[] data)
{
if (data == null || data.Length == 0) return false;
for (int i = 0; i < data.Length; i++)
{
if (data[i] != 0x90) continue;
if (i + 1 < data.Length)
{
byte next = data[i + 1];
if (next == 0x00 || next == 0x90) return true;
if (next < 0x32) return true;
}
}
return false;
}
private bool IsLikelyUtf16(byte[] data)
{
if (data == null || data.Length < 2) return false;
int nulls = 0;
for (int i = 1; i < data.Length; i += 2)
{
if (data[i] == 0x00) nulls++;
}
return (nulls >= data.Length / 4);
}
}
}