mirror of
https://github.com/andreacristaldi/DefenderRuleParser
synced 2026-06-16 13:55:00 +00:00
516 lines
18 KiB
C#
516 lines
18 KiB
C#
// DefenderRuleParser
|
|
// Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser
|
|
// This project is licensed under the Apache 2.0 License.
|
|
/*
|
|
* Summary: SIGTREE_BM parser — simplified tree/atom structure based on hex observations.
|
|
* Origin: dump-driven; headers (u8/u16 variants) and length encodings inferred from dumps.
|
|
* Role: Logs outer/inner atoms, wildcard views, and chunk-wide UTF-16, without assuming vendor semantics.
|
|
*/
|
|
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 SigTreeBmParser : ISignatureParser
|
|
{
|
|
private static readonly byte[] s_empty = new byte[0];
|
|
|
|
public void Parse(BinaryReader reader, int size, uint threatId)
|
|
{
|
|
long baseOff = reader.BaseStream.Position;
|
|
long endOff = baseOff + size;
|
|
|
|
if (size < 4)
|
|
{
|
|
Logger.Warn(string.Format("[!] SIGTREE_BM: size too small ({0}) @0x{1:X}", size, baseOff));
|
|
reader.BaseStream.Seek(endOff, SeekOrigin.Begin);
|
|
return;
|
|
}
|
|
|
|
byte[] buf = reader.ReadBytes(size);
|
|
if (buf == null || buf.Length != size)
|
|
{
|
|
Logger.Warn(string.Format("[!] SIGTREE_BM: short read ({0}/{1}) @0x{2:X}", (buf == null ? 0 : buf.Length), size, baseOff));
|
|
reader.BaseStream.Seek(endOff, SeekOrigin.Begin);
|
|
return;
|
|
}
|
|
|
|
// ---- header ----
|
|
int p = 0;
|
|
int nodeCount; ushort flags; string hdr;
|
|
if (!TryReadHeader(buf, ref p, out nodeCount, out flags, out hdr))
|
|
{
|
|
Logger.Warn(string.Format("[SIGTREE_BM] Unknown header @0x{0:X}; giving up.", baseOff));
|
|
reader.BaseStream.Seek(endOff, SeekOrigin.Begin);
|
|
return;
|
|
}
|
|
|
|
Logger.Info(string.Format(
|
|
"[SIGTREE_BM] Threat={0} @0x{1:X}, Size={2}, Hdr={3}, nodeCount(u8/u16)={4}, flags=0x{5:X}",
|
|
threatId, baseOff, size, hdr, nodeCount, flags));
|
|
|
|
var atomsSummary = new List<string>(Math.Max(8, nodeCount));
|
|
bool truncated = false;
|
|
int atomIdx = 0;
|
|
|
|
|
|
while (atomIdx < nodeCount && p + 5 <= buf.Length)
|
|
{
|
|
int atomStart = p;
|
|
|
|
ushort aflags, attr;
|
|
if (!ReadU16(buf, ref p, out aflags) || !ReadU16(buf, ref p, out attr))
|
|
{
|
|
Logger.Warn(string.Format("[SIGTREE_BM] atom {0}: header truncated @+0x{1:X}", atomIdx, atomStart));
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
int len, lenBytes; string lenFlavor; uint tag;
|
|
if (!TryReadLen(buf, ref p, buf.Length, out len, out lenBytes, out lenFlavor, out tag))
|
|
{
|
|
|
|
len = buf.Length - p;
|
|
lenBytes = 0;
|
|
lenFlavor = "LAST_REST";
|
|
}
|
|
|
|
int payloadStart = p;
|
|
int remain = buf.Length - payloadStart;
|
|
if (len > remain)
|
|
{
|
|
Logger.Warn(string.Format("[SIGTREE_BM] atom {0}: TRUNCATED (Len={1}, Remaining={2}) @+0x{3:X} (abs=0x{4:X})",
|
|
atomIdx, len, remain, payloadStart, baseOff + payloadStart));
|
|
atomsSummary.Add(string.Format("Atom#{0}: Flags=0x{1:X4} Attr={2} Len={3}({4}) Kind=TRUNCATED Missing={5}",
|
|
atomIdx, aflags, AttrLabel(attr), len, lenFlavor, len - remain));
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
byte[] payload = (len > 0) ? Sub(buf, payloadStart, len) : s_empty;
|
|
string kind = (len == 0) ? "empty" : "binary";
|
|
|
|
Logger.Info(string.Format(" - [#{0}] Flags=0x{1:X4} Attr={2} Len={3}({4}) @+0x{5:X} Kind={6}",
|
|
atomIdx, aflags, AttrLabel(attr), len, lenFlavor, atomStart, kind));
|
|
|
|
if (len > 0)
|
|
{
|
|
Logger.Verbose(string.Format(" HexFull({0}): {1}", len, Hex(payload, payload.Length)));
|
|
|
|
|
|
DumpTextViews(payload);
|
|
DumpWildcardViews(payload);
|
|
|
|
|
|
var inner = ScanInnerAtoms(payload);
|
|
if (inner != null && inner.Count > 0)
|
|
{
|
|
Logger.Info(string.Format(" InnerAtoms(noise-reduced): {0}", inner.Count));
|
|
for (int i = 0; i < inner.Count; i++)
|
|
{
|
|
var ia = inner[i];
|
|
Logger.Info(string.Format(" * [i#{0}] Flags=0x{1:X4} Attr=0x{2:X4} Len={3}({4}) @+{5}",
|
|
i, ia.Flags, ia.Attr, ia.Len, ia.LenFlavor, ia.Offset));
|
|
if (ia.Data != null && ia.Data.Length > 0)
|
|
{
|
|
DumpTextViews(ia.Data, " ");
|
|
DumpWildcardViews(ia.Data, " ");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
atomsSummary.Add(string.Format("Atom#{0}: Flags=0x{1:X4} Attr={2} Len={3}({4}) Kind={5}",
|
|
atomIdx, aflags, AttrLabel(attr), len, lenFlavor, kind));
|
|
|
|
p = payloadStart + len;
|
|
atomIdx++;
|
|
|
|
if (lenFlavor == "LAST_REST") break;
|
|
}
|
|
|
|
|
|
var wide = HarvestUtf16(buf, minChars: 6);
|
|
if (wide != null && wide.Count > 0)
|
|
{
|
|
Logger.Info(string.Format("[SIGTREE_BM] UTF16LE(chunk): {0} string(s) harvested", wide.Count));
|
|
for (int i = 0; i < wide.Count; i++)
|
|
{
|
|
Logger.Info(string.Format(" · @+0x{0:X} (abs=0x{1:X}): \"{2}\"",
|
|
wide[i].Offset, baseOff + wide[i].Offset, wide[i].Text));
|
|
}
|
|
}
|
|
|
|
|
|
Threat t;
|
|
if (ThreatDatabase.TryGetThreat(threatId, out t))
|
|
{
|
|
t.Signatures.Add(new SignatureEntry
|
|
{
|
|
Type = "SIGNATURE_TYPE_SIGTREE_BM",
|
|
Offset = baseOff,
|
|
Parsed = (!truncated && (atomIdx == nodeCount)) || (p == buf.Length && atomIdx > 0),
|
|
Pattern = atomsSummary,
|
|
ConditionType = "TREE_STRUCTURE",
|
|
ConditionValue = nodeCount
|
|
});
|
|
}
|
|
|
|
reader.BaseStream.Seek(endOff, SeekOrigin.Begin);
|
|
}
|
|
|
|
|
|
|
|
private static bool TryReadHeader(byte[] b, ref int p, out int nodes, out ushort flags, out string desc)
|
|
{
|
|
nodes = 0; flags = 0; desc = "unknown";
|
|
if (p + 4 > b.Length) return false;
|
|
|
|
|
|
int p0 = p;
|
|
int n = b[p++]; int u0 = b[p++]; flags = (ushort)(b[p] | (b[p + 1] << 8)); p += 2;
|
|
if (n >= 1 && n <= 64)
|
|
{
|
|
nodes = n; desc = "H2(u8,u8,u16)";
|
|
return true;
|
|
}
|
|
|
|
|
|
p = p0;
|
|
ushort n16, fl16;
|
|
if (!ReadU16(b, ref p, out n16)) return false;
|
|
if (!ReadU16(b, ref p, out fl16)) return false;
|
|
nodes = n16; flags = fl16; desc = "H1(u16,u16)";
|
|
return true;
|
|
}
|
|
|
|
private static bool TryReadLen(byte[] b, ref int p, int total, out int len, out int lenBytes, out string flavor, out uint tag)
|
|
{
|
|
len = 0; lenBytes = 0; flavor = "u8"; tag = 0;
|
|
if (p >= total) return false;
|
|
|
|
byte first = b[p++];
|
|
if (first != 0x00)
|
|
{
|
|
len = first; lenBytes = 1; flavor = "u8";
|
|
return true;
|
|
}
|
|
|
|
int rem = total - p;
|
|
int save = p;
|
|
|
|
|
|
if (rem >= 1)
|
|
{
|
|
int l = b[p];
|
|
if (p + 1 + l <= total) { len = l; flavor = "00+u8"; lenBytes = 2; p += 1; return true; }
|
|
}
|
|
|
|
|
|
p = save; rem = total - p;
|
|
if (rem >= 2)
|
|
{
|
|
int l = b[p] | (b[p + 1] << 8);
|
|
if (l > 0 && p + 2 + l <= total) { len = l; flavor = "00+u16"; lenBytes = 3; p += 2; return true; }
|
|
}
|
|
|
|
|
|
p = save; rem = total - p;
|
|
if (rem >= 4)
|
|
{
|
|
uint l32 = (uint)(b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24));
|
|
if (l32 > 0 && (long)p + 4 + l32 <= total) { len = (int)l32; flavor = "00+u32"; lenBytes = 5; p += 4; return true; }
|
|
}
|
|
|
|
|
|
p = save; rem = total - p;
|
|
if (rem >= 5)
|
|
{
|
|
tag = (uint)(b[p] | (b[p + 1] << 8) | (b[p + 2] << 16) | (b[p + 3] << 24));
|
|
int l = b[p + 4];
|
|
if (p + 5 + l <= total) { len = l; flavor = "00+tag32+u8"; lenBytes = 6; p += 5; return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
|
|
private sealed class InnerAtom
|
|
{
|
|
public int Offset;
|
|
public ushort Flags;
|
|
public ushort Attr;
|
|
public int Len;
|
|
public string LenFlavor;
|
|
public byte[] Data;
|
|
}
|
|
|
|
private static List<InnerAtom> ScanInnerAtoms(byte[] payload)
|
|
{
|
|
if (payload == null || payload.Length < 12) return null;
|
|
|
|
var list = new List<InnerAtom>(4);
|
|
int i = 0, limit = payload.Length, added = 0;
|
|
|
|
while (i + 5 <= limit)
|
|
{
|
|
int start = i;
|
|
|
|
ushort f, a;
|
|
if (!ReadU16(payload, ref i, out f) || !ReadU16(payload, ref i, out a))
|
|
{ i = start + 1; continue; }
|
|
|
|
int p = i, l, lb; string flav; uint _;
|
|
if (!TryReadLen(payload, ref p, limit, out l, out lb, out flav, out _))
|
|
{ i = start + 1; continue; }
|
|
|
|
if (p + l > limit) { i = start + 1; continue; }
|
|
|
|
|
|
if (l < 4) { i = p + l; continue; }
|
|
|
|
var data = (l > 0) ? Sub(payload, p, l) : s_empty;
|
|
list.Add(new InnerAtom { Offset = start, Flags = f, Attr = a, Len = l, LenFlavor = flav, Data = data });
|
|
added++;
|
|
|
|
i = p + l;
|
|
|
|
const int INNER_CAP = 16;
|
|
if (added >= INNER_CAP) break;
|
|
}
|
|
|
|
return (added > 0) ? list : new List<InnerAtom>();
|
|
}
|
|
|
|
|
|
|
|
private static void DumpTextViews(byte[] data, string pad = " ")
|
|
{
|
|
if (data == null || data.Length == 0) return;
|
|
|
|
string s16;
|
|
if (LooksLikeUtf16(data, out s16))
|
|
{
|
|
Logger.Info(pad + "UTF16LE: \"" + s16 + "\"");
|
|
return;
|
|
}
|
|
|
|
string s8;
|
|
if (LooksLikeAscii(data, out s8))
|
|
{
|
|
Logger.Info(pad + "ASCII: \"" + s8 + "\"");
|
|
}
|
|
}
|
|
|
|
private static void DumpWildcardViews(byte[] data, string pad = " ")
|
|
{
|
|
if (data == null || data.Length == 0) return;
|
|
|
|
|
|
bool maybeWild = false;
|
|
for (int i = 0; i + 1 < data.Length; i++)
|
|
if (data[i] == 0x90) { maybeWild = true; break; }
|
|
if (!maybeWild) return;
|
|
|
|
int consumed; bool hadTerm;
|
|
var toks = WildcardPattern.Tokenize(data, 0, data.Length, out consumed, out hadTerm);
|
|
if (toks == null || toks.Count == 0) return;
|
|
|
|
bool hasWild = false;
|
|
for (int i = 0; i < toks.Count; i++)
|
|
{
|
|
if (!(toks[i] is TokLiteral)) { hasWild = true; break; }
|
|
}
|
|
if (!hasWild) return;
|
|
|
|
string human = WildcardPattern.RenderHuman(toks, Encoding.ASCII);
|
|
if (!string.IsNullOrEmpty(human))
|
|
Logger.Info(pad + "0x90-pattern/Human: " + human);
|
|
|
|
string yara = WildcardPattern.RenderYaraHex(toks);
|
|
if (!string.IsNullOrEmpty(yara))
|
|
Logger.Info(pad + "0x90-pattern/YARA : " + yara);
|
|
|
|
|
|
byte[] literalConcat = ConcatLiteralBytes(toks);
|
|
if (literalConcat != null && literalConcat.Length > 0)
|
|
{
|
|
string s16;
|
|
if (LooksLikeUtf16(literalConcat, out s16))
|
|
{
|
|
Logger.Info(pad + "ASCII-only(UTF16LE): \"" + s16 + "\"");
|
|
}
|
|
else
|
|
{
|
|
string s8;
|
|
if (LooksLikeAscii(literalConcat, out s8))
|
|
Logger.Info(pad + "ASCII-only: \"" + s8 + "\"");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static byte[] ConcatLiteralBytes(IReadOnlyList<object> toks)
|
|
{
|
|
if (toks == null || toks.Count == 0) return Array.Empty<byte>();
|
|
int total = 0;
|
|
for (int i = 0; i < toks.Count; i++)
|
|
{
|
|
if (toks[i] is TokLiteral lit && lit.Bytes != null) total += lit.Bytes.Length;
|
|
}
|
|
if (total == 0) return Array.Empty<byte>();
|
|
var outBuf = new byte[total];
|
|
int p = 0;
|
|
for (int i = 0; i < toks.Count; i++)
|
|
{
|
|
if (toks[i] is TokLiteral lit && lit.Bytes != null && lit.Bytes.Length > 0)
|
|
{
|
|
Buffer.BlockCopy(lit.Bytes, 0, outBuf, p, lit.Bytes.Length);
|
|
p += lit.Bytes.Length;
|
|
}
|
|
}
|
|
return outBuf;
|
|
}
|
|
|
|
|
|
|
|
private sealed class WideHit
|
|
{
|
|
public int Offset;
|
|
public string Text;
|
|
}
|
|
|
|
|
|
private static List<WideHit> HarvestUtf16(byte[] b, int minChars)
|
|
{
|
|
var outList = new List<WideHit>();
|
|
if (b == null || b.Length < 2) return outList;
|
|
|
|
int i = 0, lim = b.Length - 1;
|
|
while (i < lim)
|
|
{
|
|
|
|
while (i + 1 < lim && !(b[i] >= 0x20 && b[i] <= 0x7E && b[i + 1] == 0x00)) i++;
|
|
if (i + 1 >= lim) break;
|
|
|
|
int start = i;
|
|
i += 2;
|
|
|
|
|
|
while (i + 1 < b.Length)
|
|
{
|
|
if (b[i] == 0x00 && b[i + 1] == 0x00) break;
|
|
if (!(b[i] >= 0x20 && b[i] <= 0x7E && (i + 1 < b.Length && b[i + 1] == 0x00)))
|
|
break;
|
|
i += 2;
|
|
}
|
|
|
|
int chars = (i - start) / 2;
|
|
if (chars >= minChars)
|
|
{
|
|
try
|
|
{
|
|
string s = Encoding.Unicode.GetString(b, start, i - start);
|
|
s = s.Replace("\r", "").Replace("\n", "");
|
|
outList.Add(new WideHit { Offset = start, Text = s });
|
|
}
|
|
catch { /* ignore */ }
|
|
}
|
|
|
|
|
|
if (i + 1 < b.Length && b[i] == 0x00 && b[i + 1] == 0x00) i += 2;
|
|
}
|
|
|
|
return outList;
|
|
}
|
|
|
|
|
|
|
|
private static bool ReadU16(byte[] b, ref int p, out ushort v)
|
|
{
|
|
v = 0;
|
|
if (p + 1 >= b.Length) return false;
|
|
v = (ushort)(b[p] | (b[p + 1] << 8));
|
|
p += 2;
|
|
return true;
|
|
}
|
|
|
|
private static byte[] Sub(byte[] b, int ofs, int len)
|
|
{
|
|
var dst = new byte[len];
|
|
Buffer.BlockCopy(b, ofs, dst, 0, len);
|
|
return dst;
|
|
}
|
|
|
|
private static string Hex(byte[] b, int len)
|
|
{
|
|
int n = Math.Min(len, (b == null ? 0 : b.Length));
|
|
var sb = new StringBuilder(n * 3);
|
|
for (int i = 0; i < n; i++)
|
|
{
|
|
sb.Append(b[i].ToString("X2"));
|
|
if (i + 1 < n) sb.Append(' ');
|
|
}
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static string AttrLabel(ushort attr)
|
|
{
|
|
|
|
return string.Format("ATTR_0x{0:X4}", attr);
|
|
}
|
|
|
|
private static bool LooksLikeAscii(byte[] b, out string s)
|
|
{
|
|
s = null;
|
|
if (b == null || b.Length == 0) return false;
|
|
int ascii = 0;
|
|
for (int i = 0; i < b.Length; i++) { byte x = b[i]; if (x >= 0x20 && x <= 0x7E) ascii++; }
|
|
if (ascii > 0 && (ascii * 100 / Math.Max(1, b.Length)) >= 70)
|
|
{
|
|
var sb = new StringBuilder(b.Length);
|
|
for (int i = 0; i < b.Length; i++)
|
|
{
|
|
byte x = b[i];
|
|
sb.Append((x >= 0x20 && x <= 0x7E) ? (char)x : '.');
|
|
}
|
|
s = sb.ToString();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool LooksLikeUtf16(byte[] b, out string s)
|
|
{
|
|
s = null;
|
|
if (b == null || b.Length < 4) return false;
|
|
|
|
int pairs = 0, zeros = 0;
|
|
int lim = b.Length - (b.Length % 2);
|
|
for (int i = 1; i < lim; i += 2) { if (b[i] == 0x00) zeros++; pairs++; }
|
|
if (pairs < 4) return false;
|
|
|
|
if (zeros * 100 / Math.Max(1, pairs) >= 70)
|
|
{
|
|
try
|
|
{
|
|
int end = lim;
|
|
while (end >= 2 && b[end - 1] == 0x00 && b[end - 2] == 0x00) end -= 2;
|
|
string s16 = Encoding.Unicode.GetString(b, 0, Math.Max(0, end));
|
|
s = s16.Replace("\r", "").Replace("\n", "");
|
|
return s.Length > 0;
|
|
}
|
|
catch { }
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
|