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

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;
}
}
}