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

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