mirror of
https://github.com/andreacristaldi/DefenderRuleParser
synced 2026-06-16 13:55:00 +00:00
326 lines
11 KiB
C#
326 lines
11 KiB
C#
// 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<int>();
|
|
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<string> 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<string> { 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<string> 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<string>(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<string> 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<string>(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<string> 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);
|
|
}
|
|
}
|
|
}
|