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

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