// DefenderRuleParser // Author: Andrea Cristaldi 2025 - https://github.com/andreacristaldi/DefenderRuleParser // This project is licensed under the Apache 2.0 License. /* * Summary: Simple text extractor for diagnostic/aux strings present in blobs. * Origin: dump-driven; UTF-16LE/ASCII detection based on byte distribution. * Role: Feeds readable strings to logs and exporters. */ using System; using System.Collections.Generic; using System.IO; using DefenderRuleParser2.Models; namespace DefenderRuleParser2.Parsers { public sealed class FixedHashOptions { public string SignatureType { get; set; } public int EntrySize { get; set; } public int HashOffset { get; set; } public int HashSize { get; set; } public int CountHeaderSize { get; set; } public int[] AutoStrides { get; set; } public int MaxTailPadding { get; set; } public int MinEntries { get; set; } public static FixedHashOptions ForLocalHash() { return new FixedHashOptions { SignatureType = "SIGNATURE_TYPE_LOCALHASH", EntrySize = 20, // SHA1 (20 bytes) HashOffset = 0, HashSize = 20, CountHeaderSize = 0, AutoStrides = null, MaxTailPadding = 0, MinEntries = 1 }; } public static FixedHashOptions ForKvir32() { return new FixedHashOptions { SignatureType = "SIGNATURE_TYPE_KVIR32", EntrySize = 0, HashOffset = 0, // CRC32 in the first 4 bytes (LE) HashSize = 4, CountHeaderSize = 0, // also try 2/4 dynamically AutoStrides = new[] { 16, 20, 22, 24, 28, 32 }, MaxTailPadding = 8, MinEntries = 2 }; } } public class GenericFixedHashListParser : ISignatureParser { private const int MaxRecords = 1_000_000; private readonly FixedHashOptions _opt; public GenericFixedHashListParser(FixedHashOptions opt) { if (opt == null) throw new ArgumentNullException("opt"); if (opt.HashSize <= 0) throw new ArgumentOutOfRangeException("HashSize"); _opt = opt; } public void Parse(BinaryReader reader, int size, uint threatId) { long start = reader.BaseStream.Position; byte[] buf = reader.ReadBytes(size); try { var tryCountHeaders = new List(); if (_opt.CountHeaderSize == 0) { tryCountHeaders.Add(0); tryCountHeaders.Add(2); tryCountHeaders.Add(4); } else { tryCountHeaders.Add(_opt.CountHeaderSize); } int chosenStride = _opt.EntrySize; int chosenHeader = 0; int chosenCount = -1; List hashes; foreach (int hdr in tryCountHeaders) { if (_opt.EntrySize > 0) { if (TryParseWithKnownStride(buf, hdr, _opt.EntrySize, _opt.HashOffset, _opt.HashSize, _opt.MinEntries, _opt.MaxTailPadding, out hashes, out chosenStride, out chosenHeader, out chosenCount)) { Emit(threatId, start, hashes, chosenStride, chosenHeader, chosenCount, "FIXED"); return; } } if (_opt.EntrySize == 0 && _opt.AutoStrides != null) { for (int i = 0; i < _opt.AutoStrides.Length; i++) { int s = _opt.AutoStrides[i]; if (TryParseWithKnownStride(buf, hdr, s, _opt.HashOffset, _opt.HashSize, _opt.MinEntries, _opt.MaxTailPadding, out hashes, out chosenStride, out chosenHeader, out chosenCount)) { Emit(threatId, start, hashes, chosenStride, chosenHeader, chosenCount, "AUTO"); return; } } } } Logger.HexDump(string.Format("[FALLBACK] raw blob ({0} bytes) @0x{1:X}", (buf == null ? 0 : buf.Length), start), buf, start); var entry = new SignatureEntry { Type = _opt.SignatureType, Offset = start - 4, Parsed = true, ConditionType = "BLOB", ConditionValue = (buf == null ? 0 : buf.Length), Pattern = new List { ToHex(buf, 0, (buf == null ? 0 : buf.Length)) }, }; AddToThreat(threatId, entry); } catch (Exception ex) { Logger.Error(string.Format("[!] {0} Error parsing at offset @0x{1:X}: {2}", _opt.SignatureType, start, ex.Message)); } finally { reader.BaseStream.Seek(start + size, SeekOrigin.Begin); } } private static bool TryParseWithKnownStride( byte[] b, int headerSize, int stride, int hashOffset, int hashSize, int minEntries, int maxTailPad, out List outHashes, out int outStride, out int outHeaderSize, out int outCount) { outHashes = null; outStride = stride; outHeaderSize = headerSize; outCount = -1; if (b == null || b.Length == 0) return false; if (stride <= 0) return false; if (hashOffset < 0 || hashOffset + hashSize > stride) return false; if (!(headerSize == 0 || headerSize == 2 || headerSize == 4)) return false; if (b.Length < headerSize) return false; int offset = headerSize; int len = b.Length - offset; if (headerSize == 2 && b.Length >= 2) { int cnt = b[0] | (b[1] << 8); if (cnt <= 0 || cnt > MaxRecords) return false; int used = cnt * stride; int pad = len - used; if (pad < 0 || pad > maxTailPad) return false; outCount = cnt; } else if (headerSize == 4 && b.Length >= 4) { int cnt = b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24); if (cnt <= 0 || cnt > MaxRecords) return false; int used = cnt * stride; int pad = len - used; if (pad < 0 || pad > maxTailPad) return false; outCount = cnt; } else { if (len < stride) return false; int usable = (len / stride) * stride; int pad = len - usable; if (pad > maxTailPad) return false; outCount = usable / stride; } if (outCount < minEntries) return false; var hashes = new List(outCount); int pos = offset; for (int i = 0; i < outCount; i++, pos += stride) { if (pos + stride > b.Length) return false; string hx = ToHex(b, pos + hashOffset, hashSize); hashes.Add(hx); } if (!LooksPlausibleHashList(hashes, hashSize)) return false; outHashes = hashes; return true; } private static bool LooksPlausibleHashList(List hashes, int hashSize) { if (hashes == null || hashes.Count == 0) return false; string zero = new string('0', hashSize * 2); string ffff = new string('F', hashSize * 2); var set = new HashSet(StringComparer.Ordinal); bool allZero = true, allFF = true; for (int i = 0; i < hashes.Count; i++) { string h = hashes[i]; set.Add(h); if (!string.Equals(h, zero, StringComparison.Ordinal)) allZero = false; if (!string.Equals(h, ffff, StringComparison.Ordinal)) allFF = false; } if (allZero || allFF) return false; if (set.Count <= 1) return false; int needUnique = Math.Min(3, hashes.Count); if (set.Count < needUnique) return false; return true; } private void Emit(uint threatId, long start, List hashes, int stride, int header, int count, string mode) { Logger.Info(string.Format("[{0}] {1}: stride={2}, header={3}, count={4}", _opt.SignatureType, mode, stride, header, count)); for (int i = 0; i < hashes.Count; i++) Logger.Info(string.Format(" [{0,3}] {1}", i + 1, hashes[i])); var entry = new SignatureEntry { Type = _opt.SignatureType, Offset = start - 4, Parsed = true, Pattern = hashes, ConditionType = "FIXED_HASH_LIST", ConditionValue = hashes.Count, }; AddToThreat(threatId, entry); } private static void AddToThreat(uint threatId, SignatureEntry entry) { Threat t; if (ThreatDatabase.TryGetThreat(threatId, out t)) t.Signatures.Add(entry); } private static string ToHex(byte[] b, int ofs, int len) { if (b == null || len <= 0 || ofs < 0 || ofs + len > b.Length) return string.Empty; char[] c = new char[len * 2]; int k = 0; for (int i = 0; i < len; i++) { byte v = b[ofs + i]; c[k++] = (char)((v >> 4) < 10 ? '0' + (v >> 4) : 'A' + ((v >> 4) - 10)); c[k++] = (char)((v & 0x0F) < 10 ? '0' + (v & 0x0F) : 'A' + ((v & 0x0F) - 10)); } return new string(c); } private static string FormatHexSpaced(byte[] b, int ofs, int len) { if (b == null || len <= 0 || ofs < 0 || ofs + len > b.Length) return string.Empty; char[] c = new char[len * 3 - 1]; int k = 0; for (int i = 0; i < len; i++) { byte v = b[ofs + i]; c[k++] = (char)((v >> 4) < 10 ? '0' + (v >> 4) : 'A' + ((v >> 4) - 10)); c[k++] = (char)((v & 0xF) < 10 ? '0' + (v & 0xF) : 'A' + ((v & 0xF) - 10)); if (i != len - 1) c[k++] = ' '; } return new string(c); } } }