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

462 lines
15 KiB
C#

// DefenderRuleParser
// Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser
// This project is licensed under the Apache 2.0 License.
/*
* Summary: SIGTREE_EXT parser — extended tree structure variant.
* Origin: dump-driven; additional flags/attr space seen in dumps guided the implementation.
* Role: Mirrors the base SIGTREE approach with extra attributes preserved.
*/
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 SigTreeExtParser : ISignatureParser
{
private const int MIN_PREVIEW_ASCII_RUN = 6;
public void Parse(BinaryReader reader, int size, uint threatId)
{
long baseAbs = reader.BaseStream.Position;
long endAbs = baseAbs + size;
if (size < 4)
{
Logger.Warn(string.Format("[!] SIGTREE_EXT: size too small ({0}) @0x{1:X}", size, baseAbs));
reader.BaseStream.Seek(endAbs, SeekOrigin.Begin);
return;
}
var buf = reader.ReadBytes(size);
if (buf == null || buf.Length != size)
{
Logger.Warn(string.Format("[!] SIGTREE_EXT: short read ({0}/{1}) @0x{2:X}",
(buf == null ? 0 : buf.Length), size, baseAbs));
reader.BaseStream.Seek(endAbs, SeekOrigin.Begin);
return;
}
int p = 0;
if (p + 4 > buf.Length)
{
Logger.Warn(string.Format("[!] SIGTREE_EXT: header truncated @0x{0:X}", baseAbs));
reader.BaseStream.Seek(endAbs, SeekOrigin.Begin);
return;
}
ushort nodeCount = ReadU16(buf, ref p);
ushort flags = ReadU16(buf, ref p);
Logger.Info(string.Format("[SIGTREE_EXT] Threat={0} @0x{1:X}, Size={2}, Nodes={3}, Flags=0x{4:X}",
threatId, baseAbs, size, nodeCount, flags));
var nodes = new List<string>(Math.Max(8, (int)nodeCount));
int parsedNodes = 0;
int firstMarker = FindNextMarker(buf, p);
if (firstMarker > p)
{
int prologLen = firstMarker - p;
bool nonZero = HasAnyNonZero(buf, p, prologLen);
Logger.Info(string.Format(" - [PROLOG] @+0x{0:X} len={1}", p, prologLen));
LogPayloadPreviews(buf, p, prologLen, " ");
if (nonZero && parsedNodes < nodeCount)
{
nodes.Add(string.Format("Node#{0}: PROLOG len={1}", parsedNodes, prologLen));
parsedNodes++;
}
p = firstMarker;
}
while (parsedNodes < nodeCount && p + 2 <= buf.Length)
{
if (!IsMarkerAt(buf, p))
{
int next = FindNextMarker(buf, p + 1);
if (next < 0) break;
p = next;
}
int nodeStart = p;
long nodeStartAbs = baseAbs + nodeStart;
p += 2;
if (p + 9 > buf.Length)
{
Logger.Warn(string.Format("[SIGTREE_EXT] Node#{0} header truncated @+0x{1:X} (abs=0x{2:X})",
parsedNodes, nodeStart, nodeStartAbs));
break;
}
byte nType = buf[p++];
byte nFlags = buf[p++];
ushort child1 = ReadU16(buf, ref p);
ushort child2 = ReadU16(buf, ref p);
ushort weight = ReadU16(buf, ref p);
byte u8 = buf[p++];
int payloadStart = p;
int payloadEnd;
if (parsedNodes == nodeCount - 1)
{
payloadEnd = buf.Length;
}
else
{
int nextMarker = FindNextMarker(buf, p);
payloadEnd = (nextMarker >= 0) ? nextMarker : buf.Length;
}
if (payloadEnd < payloadStart) payloadEnd = payloadStart;
int payloadLen = payloadEnd - payloadStart;
Logger.Info(string.Format(" - [#{0}] @0x{1:X} len={2} Type=0x{3:X2} ({4}) Flags=0x{5:X2} Child1={6} Child2={7} Weight={8} U8=0x{9:X2}",
parsedNodes, nodeStartAbs, (payloadEnd - nodeStart),
nType, DescribeLogic(nType), nFlags, child1, child2, weight, u8));
Logger.Info(string.Format(" Payload: rel=+0x{0:X}, abs=0x{1:X}, len={2}",
payloadStart, baseAbs + payloadStart, payloadLen));
if (payloadLen > 0)
{
LogPayloadPreviews(buf, payloadStart, payloadLen, " ");
}
else
{
Logger.Info(" (empty payload)");
}
nodes.Add(string.Format("Node#{0}: type={1} w={2} c1={3} c2={4} payloadLen={5}",
parsedNodes, DescribeLogic(nType), weight, child1, child2, payloadLen));
p = payloadEnd;
parsedNodes++;
}
if (p < buf.Length)
{
int tailStart = p;
int tailLen = buf.Length - tailStart;
Logger.Info(string.Format(" - [TAIL] Unparsed bytes after nodes: rel=+0x{0:X}, len={1}", tailStart, tailLen));
LogPayloadPreviews(buf, tailStart, tailLen, " ");
}
if (parsedNodes != nodeCount)
Logger.Warn(string.Format("[SIGTREE_EXT] ParsedNodes={0}, Expected={1}", parsedNodes, nodeCount));
else
Logger.Info(string.Format("[SIGTREE_EXT] ParsedNodes={0}, Expected={1} (OK)", parsedNodes, nodeCount));
try
{
if (ThreatDatabase.TryGetThreat(threatId, out var threat))
{
threat.Signatures.Add(new SignatureEntry
{
Type = "SIGNATURE_TYPE_SIGTREE_EXT",
Offset = baseAbs,
Parsed = (parsedNodes == nodeCount),
Pattern = nodes,
ConditionType = "TREE_STRUCTURE",
ConditionValue = nodeCount,
Logic = new SignatureLogic { Threshold = nodeCount }
});
}
}
catch (Exception ex)
{
Logger.Warn(string.Format("[SIGTREE_EXT] ThreatDatabase update failed: {0}", ex.Message));
}
reader.BaseStream.Seek(endAbs, SeekOrigin.Begin);
}
private static ushort ReadU16(byte[] b, ref int p)
{
if (p + 2 > b.Length) return 0;
ushort v = (ushort)(b[p] | (b[p + 1] << 8));
p += 2;
return v;
}
private static bool IsMarkerAt(byte[] b, int ofs)
{
return (ofs + 1 < b.Length) && b[ofs] == 0x30 && b[ofs + 1] == 0x00;
}
private static int FindNextMarker(byte[] b, int start)
{
for (int i = Math.Max(0, start); i + 1 < b.Length; i++)
if (b[i] == 0x30 && b[i + 1] == 0x00)
return i;
return -1;
}
private static bool HasAnyNonZero(byte[] b, int start, int len)
{
int end = Math.Min(b.Length, start + len);
for (int i = start; i < end; i++)
if (b[i] != 0x00) return true;
return false;
}
private static string Hex(byte[] b, int start, int len)
{
int n = Math.Min(len, b.Length - start);
if (n < 0) n = 0;
var sb = new StringBuilder(n * 3);
for (int i = 0; i < n; i++)
{
sb.Append(b[start + i].ToString("X2"));
if (i + 1 < n) sb.Append(' ');
}
return sb.ToString();
}
private static bool IsPrintable(byte x) { return x >= 0x20 && x <= 0x7E; }
private static void FindLongestAsciiRun(byte[] b, int start, int len, out int relStart, out int runLen)
{
relStart = -1; runLen = 0;
int end = Math.Min(b.Length, start + len);
int i = start;
while (i < end)
{
while (i < end && !IsPrintable(b[i])) i++;
if (i >= end) break;
int j = i;
while (j < end && IsPrintable(b[j])) j++;
int curLen = j - i;
if (curLen > runLen)
{
runLen = curLen;
relStart = i - start;
}
i = j;
}
if (runLen < MIN_PREVIEW_ASCII_RUN)
{
relStart = -1;
runLen = 0;
}
}
private static int ConsumeWildcardBlock(byte[] b, int start, int len, int iAbs)
{
int endAbs = Math.Min(b.Length, start + len);
int i = iAbs;
if (i >= endAbs || b[i] != 0x90) return iAbs;
if (i + 1 >= endAbs) return iAbs;
byte op = b[i + 1];
if (op == 0x00) return iAbs;
if (op == 0x90) return (i + 2);
if (op >= 0x32)
{
return iAbs;
}
if ((op >= 0x01 && op <= 0x03) || op == 0x17)
{
if (i + 2 < endAbs) return (i + 3);
return iAbs;
}
if (op == 0x04 || op == 0x05 || op == 0x19 || op == 0x1A)
{
if (i + 3 < endAbs) return (i + 4);
return iAbs;
}
if (op == 0x09)
{
if (i + 3 < endAbs) return (i + 4);
return iAbs;
}
if (op == 0x1B)
{
if (i + 1 < endAbs) return (i + 2);
return iAbs;
}
if (i + 3 < endAbs)
{
int lo = b[i + 2], hi = b[i + 3];
bool looksWord = (hi == 0x00) || (lo <= 0x20 && hi <= 0x04);
return looksWord ? (i + 4) : (i + 3);
}
if (i + 2 < endAbs) return (i + 3);
return iAbs;
}
private static void SelectPreviewWindow(byte[] b, int start, int len, out int winStartAbs, out int winLen)
{
winStartAbs = start; winLen = 0;
int relAnchor, anchorLen;
FindLongestAsciiRun(b, start, len, out relAnchor, out anchorLen);
int absStart;
if (relAnchor >= 0)
{
absStart = start + relAnchor;
}
else
{
int end = Math.Min(b.Length, start + len);
absStart = end;
for (int i = start; i < end; i++)
{
if (IsPrintable(b[i])) { absStart = i; break; }
}
if (absStart >= end)
{
winStartAbs = start; winLen = 0;
return;
}
}
int endAbsBound = Math.Min(b.Length, start + len);
int i2 = absStart;
while (i2 < endAbsBound)
{
if (IsPrintable(b[i2]))
{
i2++;
continue;
}
if (b[i2] == 0x90)
{
if (i2 + 1 < endAbsBound && b[i2 + 1] == 0x00)
break;
int next = ConsumeWildcardBlock(b, start, len, i2);
if (next == i2) break;
i2 = next;
continue;
}
break;
}
winStartAbs = absStart;
winLen = Math.Max(0, i2 - absStart);
}
private static void LogPayloadPreviews(byte[] buf, int start, int len, string pad)
{
if (len <= 0)
{
Logger.Info(pad + "(no data)");
return;
}
Logger.Verbose(string.Format(pad + "HexFull({0}): {1}", len, Hex(buf, start, len)));
if (len > 64)
{
string head = Hex(buf, start, 64);
Logger.Info(string.Format(pad + "HexHead(64): {0} ...", head));
}
int winStartAbs, winLen;
SelectPreviewWindow(buf, start, len, out winStartAbs, out winLen);
if (winLen <= 0)
{
Logger.Info(string.Format(pad + "Ascii/Wild @+0x{0:X}: ", start));
Logger.Info(string.Format(pad + "ASCII-only @+0x{0:X}: \"\"", start));
Logger.Info(string.Format(pad + "YARA-hex @+0x{0:X}: ", start));
return;
}
var slice = new byte[winLen];
Buffer.BlockCopy(buf, winStartAbs, slice, 0, winLen);
int consumed;
bool hadTerm;
var tokens = WildcardPattern.Tokenize(slice, 0, slice.Length, out consumed, out hadTerm);
string human = WildcardPattern.RenderHuman(tokens, Encoding.ASCII) ?? string.Empty;
string yara = WildcardPattern.RenderYaraHex(tokens) ?? string.Empty;
var sbAscii = new StringBuilder();
for (int k = 0; k < tokens.Count; k++)
{
var lit = tokens[k] as TokLiteral;
if (lit != null && lit.Bytes != null && lit.Bytes.Length > 0)
sbAscii.Append(Encoding.ASCII.GetString(lit.Bytes));
}
Logger.Info(string.Format(pad + "Ascii/Wild @+0x{0:X}: {1}", winStartAbs, human));
Logger.Info(string.Format(pad + "ASCII-only @+0x{0:X}: \"{1}\"", winStartAbs, sbAscii.ToString()));
Logger.Info(string.Format(pad + "YARA-hex @+0x{0:X}: {1}", winStartAbs, yara));
}
private static string DescribeLogic(byte type)
{
switch (type)
{
case 0x01: return "AND";
case 0x02: return "LEAF";
case 0x03: return "OR";
case 0x04: return "NOT";
case 0x0E: return "MATCH";
case 0x1D: return "LOGIC_NODE";
default: return string.Format("TYPE_0x{0:X2}", type);
}
}
}
}