mirror of
https://github.com/andreacristaldi/DefenderRuleParser
synced 2026-06-16 13:55:00 +00:00
349 lines
12 KiB
C#
349 lines
12 KiB
C#
// DefenderRuleParser
|
|
// Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser
|
|
// This project is licensed under the Apache 2.0 License.
|
|
/*
|
|
* Summary: SIGTREE parser — base tree/atom structure reader.
|
|
* Origin: dump-driven; atom headers (flags, attr, len) reconstructed from repeated patterns in hex.
|
|
* Role: Provides a readable summary of node sequences and payload hints.
|
|
*/
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using DefenderRuleParser2.Models;
|
|
/*
|
|
* Summary: SIGTREE parser — base tree/atom structure reader.
|
|
* Origin: dump-driven; atom headers (flags, attr, len) reconstructed from repeated patterns in hex.
|
|
* Role: Provides a readable summary of node sequences and payload hints.
|
|
*/
|
|
namespace DefenderRuleParser2.Parsers
|
|
{
|
|
|
|
public sealed class SigTreeParser : ISignatureParser
|
|
{
|
|
|
|
private static readonly Dictionary<ushort, string> _attrNames = new Dictionary<ushort, string>()
|
|
{
|
|
{ 0x0501, "ATTR_0501" },
|
|
{ 0x0001, "ATTR_0001" },
|
|
{ 0x002B, "ATTR_002B" },
|
|
{ 0x0100, "ATTR_0100" },
|
|
{ 0x0012, "ATTR_0012" },
|
|
{ 0x6028, "ATTR_6028" },
|
|
{ 0x1A04, "ATTR_1A04" },
|
|
{ 0x100E, "ATTR_100E" }
|
|
};
|
|
|
|
|
|
|
|
private readonly HashSet<ushort> _containerAttrs;
|
|
|
|
|
|
private const int HexFullLimit = 1024;
|
|
|
|
|
|
private const int HexPreviewEdge = 32;
|
|
|
|
public SigTreeParser()
|
|
: this(new HashSet<ushort> { 0x0001, 0x1A04, 0x100E })
|
|
{ }
|
|
|
|
public SigTreeParser(HashSet<ushort> containerAttrs)
|
|
{
|
|
_containerAttrs = containerAttrs ?? new HashSet<ushort>();
|
|
}
|
|
|
|
public void Parse(BinaryReader reader, int size, uint threatId)
|
|
{
|
|
long baseOffset = reader.BaseStream.Position;
|
|
long endOffset = baseOffset + size;
|
|
|
|
if (size < 5)
|
|
{
|
|
Logger.Warn($"[!] SIGTREE: size too small ({size}) @0x{baseOffset:X}");
|
|
reader.BaseStream.Seek(endOffset, SeekOrigin.Begin);
|
|
return;
|
|
}
|
|
|
|
byte[] buf = reader.ReadBytes(size);
|
|
if (buf == null || buf.Length != size)
|
|
{
|
|
Logger.Warn($"[!] SIGTREE: short read ({(buf == null ? 0 : buf.Length)}/{size}) @0x{baseOffset:X}");
|
|
reader.BaseStream.Seek(endOffset, SeekOrigin.Begin);
|
|
return;
|
|
}
|
|
|
|
Logger.Info($"[SIGTREE] Threat ID: {threatId} Type: SIGNATURE_TYPE_SIGTREE Offset: 0x{baseOffset:X} Size: {size}");
|
|
|
|
var atoms = new List<string>(16);
|
|
int p = 0;
|
|
int atomIdx = 0;
|
|
|
|
|
|
bool atomTruncated = false;
|
|
int missingBytes = 0;
|
|
long truncatedAtAbs = 0;
|
|
|
|
while (p + 5 <= buf.Length)
|
|
{
|
|
int atomStart = p;
|
|
|
|
ushort flags = ReadU16(buf, ref p);
|
|
ushort attr = ReadU16(buf, ref p);
|
|
byte len = buf[p++];
|
|
|
|
int payloadStart = p;
|
|
int remaining = buf.Length - payloadStart;
|
|
|
|
|
|
if (len > remaining)
|
|
{
|
|
missingBytes = len - remaining;
|
|
truncatedAtAbs = baseOffset + atomStart + 4;
|
|
|
|
Logger.Warn($"[SIGTREE] atom {atomIdx}: TRUNCATED (Len={len}, Remaining={remaining}) " +
|
|
$"@+0x{atomStart + 4:X} (abs=0x{truncatedAtAbs:X}); end-of-chunk.");
|
|
|
|
atoms.Add($"Atom#{atomIdx}: Flags=0x{flags:X4} Attr={AttrLabel(attr)} Len={len} Kind=TRUNCATED Missing={missingBytes}");
|
|
|
|
atomTruncated = true;
|
|
break;
|
|
}
|
|
|
|
|
|
byte[] payload = len > 0 ? Sub(buf, payloadStart, len) : Array.Empty<byte>();
|
|
string kind = len == 0 ? "empty" : (IsLikelyAscii(payload) ? "ascii" : "binary");
|
|
|
|
Logger.Info($" - [#{atomIdx}] Flags=0x{flags:X4} Attr={AttrLabel(attr)} Len={len} @+0x{atomStart:X} Kind={kind}");
|
|
|
|
|
|
if (len > 0)
|
|
{
|
|
if (len <= HexFullLimit)
|
|
{
|
|
Logger.Info($" HexFull({len}): {Hex(payload, len)}");
|
|
}
|
|
else
|
|
{
|
|
Logger.Info($" HexPreview({len}): {HexHeadTail(payload, HexPreviewEdge)} ...");
|
|
}
|
|
}
|
|
|
|
|
|
if (len > 0 && _containerAttrs.Contains(attr))
|
|
{
|
|
var items = HeuristicSplitInnerPairs(payload);
|
|
if (items.Count > 0)
|
|
{
|
|
Logger.Info($" Inner: {items.Count} sub-item(s) detected via 'key 60 len' pattern");
|
|
|
|
var counts = new Dictionary<byte, int>();
|
|
foreach (var it in items)
|
|
counts[it.Key] = counts.TryGetValue(it.Key, out var c) ? c + 1 : 1;
|
|
|
|
foreach (var it in items)
|
|
{
|
|
int toShow = Math.Min(it.Value.Length, 24);
|
|
string headHex = Hex(it.Value, toShow);
|
|
string marker = it.Value.Length == 0 ? " (marker)" : "";
|
|
Logger.Info($" - K_{it.Key:X2} @+0x{it.Offset:X}: len={it.Value.Length}{marker} val={headHex}{(it.Value.Length > 24 ? " ..." : "")}");
|
|
}
|
|
|
|
|
|
var innerList = new StringBuilder("Inner=[");
|
|
for (int i = 0; i < items.Count; i++)
|
|
{
|
|
if (i > 0) innerList.Append("; ");
|
|
var it = items[i];
|
|
innerList.AppendFormat("K_{0:X2}@+0x{1:X}/len={2}", it.Key, it.Offset, it.Value.Length);
|
|
}
|
|
innerList.Append(']');
|
|
|
|
var stats = new StringBuilder(" Stats=[");
|
|
bool first = true;
|
|
foreach (var kv in counts)
|
|
{
|
|
if (!first) stats.Append("; ");
|
|
first = false;
|
|
stats.AppendFormat("K_{0:X2}={1}", kv.Key, kv.Value);
|
|
}
|
|
stats.Append(']');
|
|
|
|
atoms.Add($"Atom#{atomIdx}: Flags=0x{flags:X4} Attr={AttrLabel(attr)} Len={len} Kind={kind} {innerList}{stats}");
|
|
}
|
|
else
|
|
{
|
|
atoms.Add($"Atom#{atomIdx}: Flags=0x{flags:X4} Attr={AttrLabel(attr)} Len={len} Kind={kind}");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
atoms.Add($"Atom#{atomIdx}: Flags=0x{flags:X4} Attr={AttrLabel(attr)} Len={len} Kind={kind}");
|
|
}
|
|
|
|
p = payloadStart + len;
|
|
atomIdx++;
|
|
|
|
|
|
if (buf.Length - p < 5) break;
|
|
}
|
|
|
|
|
|
try
|
|
{
|
|
var entry = new SignatureEntry
|
|
{
|
|
Type = "SIGNATURE_TYPE_SIGTREE",
|
|
Offset = baseOffset,
|
|
Parsed = true,
|
|
Pattern = atoms,
|
|
ConditionType = "ATOM_LIST",
|
|
ConditionValue = atomIdx,
|
|
Logic = new SignatureLogic { Threshold = atomIdx },
|
|
|
|
|
|
};
|
|
|
|
if (ThreatDatabase.TryGetThreat(threatId, out var threat) && threat != null)
|
|
threat.Signatures.Add(entry);
|
|
}
|
|
catch
|
|
{
|
|
|
|
}
|
|
|
|
reader.BaseStream.Seek(endOffset, SeekOrigin.Begin);
|
|
}
|
|
|
|
|
|
|
|
private static ushort ReadU16(byte[] b, ref int p)
|
|
{
|
|
ushort v = (ushort)(b[p] | (b[p + 1] << 8));
|
|
p += 2;
|
|
return v;
|
|
}
|
|
|
|
private static string AttrLabel(ushort attr)
|
|
=> _attrNames.TryGetValue(attr, out var name)
|
|
? $"{name}(0x{attr:X4})"
|
|
: $"ATTR_0x{attr:X4}";
|
|
|
|
private static string Hex(byte[] data, int count)
|
|
{
|
|
if (data == null || data.Length == 0 || count <= 0) return string.Empty;
|
|
count = Math.Min(count, data.Length);
|
|
var sb = new StringBuilder(count * 3);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
if (i > 0) sb.Append(' ');
|
|
sb.Append(data[i].ToString("X2"));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string HexHeadTail(byte[] data, int edge)
|
|
{
|
|
if (data == null || data.Length == 0) return string.Empty;
|
|
if (data.Length <= 2 * edge) return Hex(data, data.Length);
|
|
|
|
var sb = new StringBuilder((edge * 2 + 8) * 3);
|
|
// head
|
|
for (int i = 0; i < edge; i++)
|
|
{
|
|
if (i > 0) sb.Append(' ');
|
|
sb.Append(data[i].ToString("X2"));
|
|
}
|
|
sb.Append(" ... ");
|
|
// tail
|
|
for (int i = data.Length - edge; i < data.Length; i++)
|
|
{
|
|
if (i > data.Length - edge) sb.Append(' ');
|
|
sb.Append(data[i].ToString("X2"));
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
|
|
private static bool IsLikelyAscii(byte[] b)
|
|
{
|
|
if (b == null || b.Length == 0) return false;
|
|
int bad = 0, maxBad = Math.Max(1, b.Length / 10);
|
|
for (int i = 0; i < b.Length; i++)
|
|
{
|
|
byte c = b[i];
|
|
if (c == 0x09 || c == 0x0A || c == 0x0D) continue;
|
|
if (c < 0x20 || c > 0x7E)
|
|
{
|
|
if (++bad > maxBad) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static byte[] Sub(byte[] b, int off, int len)
|
|
{
|
|
var r = new byte[len];
|
|
Buffer.BlockCopy(b, off, r, 0, len);
|
|
return r;
|
|
}
|
|
|
|
|
|
|
|
private sealed class InnerItem
|
|
{
|
|
public byte Key;
|
|
public int Offset;
|
|
public byte[] Value;
|
|
}
|
|
|
|
private static List<InnerItem> HeuristicSplitInnerPairs(byte[] payload)
|
|
{
|
|
var result = new List<InnerItem>(8);
|
|
if (payload == null || payload.Length < 3) return result;
|
|
|
|
int i = 0;
|
|
while (i + 2 < payload.Length)
|
|
{
|
|
|
|
if (payload[i + 1] == 0x60)
|
|
{
|
|
byte key = payload[i];
|
|
int lenPos = i + 2;
|
|
int dataPos = lenPos + 1;
|
|
int len = payload[lenPos];
|
|
|
|
|
|
if (len == 0x00 && lenPos + 1 < payload.Length)
|
|
{
|
|
len = payload[lenPos + 1];
|
|
dataPos = lenPos + 2;
|
|
}
|
|
|
|
else if (len == 0xFF && lenPos + 2 < payload.Length)
|
|
{
|
|
len = payload[lenPos + 1] | (payload[lenPos + 2] << 8);
|
|
dataPos = lenPos + 3;
|
|
}
|
|
|
|
int remaining = payload.Length - dataPos;
|
|
if (len >= 0 && len <= remaining)
|
|
{
|
|
var val = Sub(payload, dataPos, len);
|
|
result.Add(new InnerItem { Key = key, Offset = i, Value = val });
|
|
|
|
|
|
i = dataPos + len;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
i++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
|