feat(eidas): Implement eIDAS Crypto Plugin with dependency injection and signing capabilities
- Added ServiceCollectionExtensions for eIDAS crypto providers. - Implemented EidasCryptoProvider for handling eIDAS-compliant signatures. - Created LocalEidasProvider for local signing using PKCS#12 keystores. - Defined SignatureLevel and SignatureFormat enums for eIDAS compliance. - Developed TrustServiceProviderClient for remote signing via TSP. - Added configuration support for eIDAS options in the project file. - Implemented unit tests for SM2 compliance and crypto operations. - Introduced dependency injection extensions for SM software and remote plugins.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
namespace StellaOps.Feedser.BinaryAnalysis;
|
||||
|
||||
using StellaOps.Feedser.BinaryAnalysis.Fingerprinters;
|
||||
using StellaOps.Feedser.BinaryAnalysis.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating and managing binary fingerprinters.
|
||||
/// Provides access to all available fingerprinting methods (Tier 4).
|
||||
/// </summary>
|
||||
public sealed class BinaryFingerprintFactory
|
||||
{
|
||||
private readonly Dictionary<FingerprintMethod, IBinaryFingerprinter> _fingerprinters;
|
||||
|
||||
public BinaryFingerprintFactory()
|
||||
{
|
||||
_fingerprinters = new Dictionary<FingerprintMethod, IBinaryFingerprinter>
|
||||
{
|
||||
[FingerprintMethod.TLSH] = new SimplifiedTlshFingerprinter(),
|
||||
[FingerprintMethod.InstructionHash] = new InstructionHashFingerprinter()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get fingerprinter for specified method.
|
||||
/// </summary>
|
||||
public IBinaryFingerprinter GetFingerprinter(FingerprintMethod method)
|
||||
{
|
||||
if (!_fingerprinters.TryGetValue(method, out var fingerprinter))
|
||||
{
|
||||
throw new NotSupportedException($"Fingerprint method {method} is not supported");
|
||||
}
|
||||
|
||||
return fingerprinter;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract fingerprints using all available methods.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BinaryFingerprint>> ExtractAllAsync(
|
||||
string binaryPath,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _fingerprinters.Values.Select(fp =>
|
||||
fp.ExtractAsync(binaryPath, cveId, targetFunction, cancellationToken));
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract fingerprints using all available methods from binary data.
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BinaryFingerprint>> ExtractAllAsync(
|
||||
ReadOnlyMemory<byte> binaryData,
|
||||
string binaryName,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = _fingerprinters.Values.Select(fp =>
|
||||
fp.ExtractAsync(binaryData, binaryName, cveId, targetFunction, cancellationToken));
|
||||
|
||||
var results = await Task.WhenAll(tasks);
|
||||
return results.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match candidate binary against known fingerprints using all methods.
|
||||
/// Returns best match result.
|
||||
/// </summary>
|
||||
public async Task<FingerprintMatchResult?> MatchBestAsync(
|
||||
string candidatePath,
|
||||
IEnumerable<BinaryFingerprint> knownFingerprints,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var matchTasks = new List<Task<FingerprintMatchResult>>();
|
||||
|
||||
foreach (var known in knownFingerprints)
|
||||
{
|
||||
if (_fingerprinters.TryGetValue(known.Method, out var fingerprinter))
|
||||
{
|
||||
matchTasks.Add(fingerprinter.MatchAsync(candidatePath, known, cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(matchTasks);
|
||||
|
||||
// Return best match (highest confidence)
|
||||
return results
|
||||
.Where(r => r.IsMatch)
|
||||
.OrderByDescending(r => r.Confidence)
|
||||
.ThenByDescending(r => r.Similarity)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match candidate binary data against known fingerprints using all methods.
|
||||
/// Returns best match result.
|
||||
/// </summary>
|
||||
public async Task<FingerprintMatchResult?> MatchBestAsync(
|
||||
ReadOnlyMemory<byte> candidateData,
|
||||
IEnumerable<BinaryFingerprint> knownFingerprints,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var matchTasks = new List<Task<FingerprintMatchResult>>();
|
||||
|
||||
foreach (var known in knownFingerprints)
|
||||
{
|
||||
if (_fingerprinters.TryGetValue(known.Method, out var fingerprinter))
|
||||
{
|
||||
matchTasks.Add(fingerprinter.MatchAsync(candidateData, known, cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(matchTasks);
|
||||
|
||||
return results
|
||||
.Where(r => r.IsMatch)
|
||||
.OrderByDescending(r => r.Confidence)
|
||||
.ThenByDescending(r => r.Similarity)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all available fingerprinting methods.
|
||||
/// </summary>
|
||||
public IReadOnlyList<FingerprintMethod> GetAvailableMethods()
|
||||
{
|
||||
return _fingerprinters.Keys.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
namespace StellaOps.Feedser.BinaryAnalysis.Fingerprinters;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Feedser.BinaryAnalysis.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprinter based on normalized instruction sequences.
|
||||
/// Extracts and hashes instruction opcodes while normalizing out operands.
|
||||
///
|
||||
/// This approach is resistant to:
|
||||
/// - Address randomization (ASLR)
|
||||
/// - Register allocation differences
|
||||
/// - Minor compiler optimizations
|
||||
///
|
||||
/// NOTE: This is a simplified implementation. Production use should integrate
|
||||
/// with disassemblers like Capstone for proper instruction decoding.
|
||||
/// </summary>
|
||||
public sealed class InstructionHashFingerprinter : IBinaryFingerprinter
|
||||
{
|
||||
private const string Version = "1.0.0";
|
||||
private const int MinInstructionSequence = 16; // Minimum instructions to fingerprint
|
||||
|
||||
public FingerprintMethod Method => FingerprintMethod.InstructionHash;
|
||||
|
||||
public async Task<BinaryFingerprint> ExtractAsync(
|
||||
string binaryPath,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var binaryData = await File.ReadAllBytesAsync(binaryPath, cancellationToken);
|
||||
var binaryName = Path.GetFileName(binaryPath);
|
||||
return await ExtractAsync(binaryData, binaryName, cveId, targetFunction, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<BinaryFingerprint> ExtractAsync(
|
||||
ReadOnlyMemory<byte> binaryData,
|
||||
string binaryName,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var metadata = ExtractMetadata(binaryData.Span, binaryName);
|
||||
var instructionHash = ComputeInstructionHash(binaryData.Span, metadata.Architecture);
|
||||
|
||||
var fingerprint = new BinaryFingerprint
|
||||
{
|
||||
FingerprintId = $"fingerprint:instruction:{instructionHash}",
|
||||
CveId = cveId,
|
||||
Method = FingerprintMethod.InstructionHash,
|
||||
FingerprintValue = instructionHash,
|
||||
TargetBinary = binaryName,
|
||||
TargetFunction = targetFunction,
|
||||
Metadata = metadata,
|
||||
ExtractedAt = DateTimeOffset.UtcNow,
|
||||
ExtractorVersion = Version
|
||||
};
|
||||
|
||||
return Task.FromResult(fingerprint);
|
||||
}
|
||||
|
||||
public async Task<FingerprintMatchResult> MatchAsync(
|
||||
string candidatePath,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var candidateData = await File.ReadAllBytesAsync(candidatePath, cancellationToken);
|
||||
return await MatchAsync(candidateData, knownFingerprint, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<FingerprintMatchResult> MatchAsync(
|
||||
ReadOnlyMemory<byte> candidateData,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var metadata = ExtractMetadata(candidateData.Span, "candidate");
|
||||
var candidateHash = ComputeInstructionHash(candidateData.Span, metadata.Architecture);
|
||||
|
||||
// Exact match only (instruction sequences must be identical after normalization)
|
||||
var isMatch = candidateHash.Equals(knownFingerprint.FingerprintValue, StringComparison.Ordinal);
|
||||
var similarity = isMatch ? 1.0 : 0.0;
|
||||
var confidence = isMatch ? 0.80 : 0.0; // High confidence for exact matches
|
||||
|
||||
var result = new FingerprintMatchResult
|
||||
{
|
||||
IsMatch = isMatch,
|
||||
Similarity = similarity,
|
||||
Confidence = confidence,
|
||||
MatchedFingerprintId = isMatch ? knownFingerprint.FingerprintId : null,
|
||||
Method = FingerprintMethod.InstructionHash,
|
||||
MatchDetails = new Dictionary<string, object>
|
||||
{
|
||||
["candidate_hash"] = candidateHash,
|
||||
["known_hash"] = knownFingerprint.FingerprintValue,
|
||||
["match_type"] = isMatch ? "exact" : "none"
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static string ComputeInstructionHash(ReadOnlySpan<byte> data, string architecture)
|
||||
{
|
||||
// Extract opcode patterns (simplified - production would use proper disassembly)
|
||||
var opcodes = ExtractOpcodePatterns(data, architecture);
|
||||
|
||||
// Normalize by removing operand-specific bytes
|
||||
var normalized = NormalizeOpcodes(opcodes);
|
||||
|
||||
// Hash the normalized sequence
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ExtractOpcodePatterns(ReadOnlySpan<byte> data, string architecture)
|
||||
{
|
||||
// Simplified opcode extraction
|
||||
// Production implementation would use Capstone or similar for proper disassembly
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var step = architecture switch
|
||||
{
|
||||
"x86_64" or "x86" => 1, // Variable length instructions
|
||||
"aarch64" => 4, // Fixed 4-byte instructions
|
||||
"armv7" => 2, // Thumb: 2-byte, ARM: 4-byte (simplified to 2)
|
||||
_ => 1
|
||||
};
|
||||
|
||||
// Sample instructions at regular intervals
|
||||
for (int i = 0; i < data.Length && i < 1024; i += step)
|
||||
{
|
||||
if (i + step <= data.Length)
|
||||
{
|
||||
// Extract opcode prefix (first byte for x86, full instruction for RISC)
|
||||
var opcode = data[i];
|
||||
|
||||
// Filter out likely data sections (high entropy, unusual patterns)
|
||||
if (IsLikelyInstruction(opcode))
|
||||
{
|
||||
sb.Append(opcode.ToString("x2"));
|
||||
sb.Append('-');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool IsLikelyInstruction(byte opcode)
|
||||
{
|
||||
// Simple heuristic: filter out common data patterns
|
||||
// Real implementation would use proper code/data discrimination
|
||||
return opcode != 0x00 && opcode != 0xFF && opcode != 0xCC; // Not null, not padding, not int3
|
||||
}
|
||||
|
||||
private static string NormalizeOpcodes(string opcodes)
|
||||
{
|
||||
// Remove position-dependent patterns
|
||||
// This is a simplified normalization
|
||||
var sb = new StringBuilder();
|
||||
var parts = opcodes.Split('-', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// Group similar opcodes to reduce position sensitivity
|
||||
var groups = parts.GroupBy(p => p).OrderBy(g => g.Key);
|
||||
|
||||
foreach (var group in groups)
|
||||
{
|
||||
sb.Append(group.Key);
|
||||
sb.Append(':');
|
||||
sb.Append(group.Count());
|
||||
sb.Append(';');
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static FingerprintMetadata ExtractMetadata(ReadOnlySpan<byte> data, string binaryName)
|
||||
{
|
||||
var format = DetectFormat(data);
|
||||
var architecture = DetectArchitecture(data, format);
|
||||
|
||||
return new FingerprintMetadata
|
||||
{
|
||||
Architecture = architecture,
|
||||
Format = format,
|
||||
Compiler = null,
|
||||
OptimizationLevel = null,
|
||||
HasDebugSymbols = false,
|
||||
FileOffset = null,
|
||||
RegionSize = data.Length
|
||||
};
|
||||
}
|
||||
|
||||
private static string DetectFormat(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < 4) return "unknown";
|
||||
|
||||
if (data[0] == 0x7F && data[1] == 'E' && data[2] == 'L' && data[3] == 'F')
|
||||
return "ELF";
|
||||
|
||||
if (data[0] == 'M' && data[1] == 'Z')
|
||||
return "PE";
|
||||
|
||||
if (data.Length >= 4)
|
||||
{
|
||||
var magic = BitConverter.ToUInt32(data[..4]);
|
||||
if (magic == 0xFEEDFACE || magic == 0xFEEDFACF ||
|
||||
magic == 0xCEFAEDFE || magic == 0xCFFAEDFE)
|
||||
return "Mach-O";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private static string DetectArchitecture(ReadOnlySpan<byte> data, string format)
|
||||
{
|
||||
if (format == "ELF" && data.Length >= 18)
|
||||
{
|
||||
var machine = BitConverter.ToUInt16(data.Slice(18, 2));
|
||||
return machine switch
|
||||
{
|
||||
0x3E => "x86_64",
|
||||
0x03 => "x86",
|
||||
0xB7 => "aarch64",
|
||||
0x28 => "armv7",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
if (format == "PE" && data.Length >= 0x3C + 4)
|
||||
{
|
||||
var peOffset = BitConverter.ToInt32(data.Slice(0x3C, 4));
|
||||
if (peOffset > 0 && peOffset + 6 < data.Length)
|
||||
{
|
||||
var machine = BitConverter.ToUInt16(data.Slice(peOffset + 4, 2));
|
||||
return machine switch
|
||||
{
|
||||
0x8664 => "x86_64",
|
||||
0x014C => "x86",
|
||||
0xAA64 => "aarch64",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
namespace StellaOps.Feedser.BinaryAnalysis.Fingerprinters;
|
||||
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using StellaOps.Feedser.BinaryAnalysis.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Simplified locality-sensitive hash fingerprinter.
|
||||
///
|
||||
/// NOTE: This is a simplified implementation for proof-of-concept.
|
||||
/// Production use should integrate with a full TLSH library (e.g., via P/Invoke to libtlsh).
|
||||
///
|
||||
/// This implementation captures key TLSH principles:
|
||||
/// - Sliding window analysis
|
||||
/// - Byte distribution histograms
|
||||
/// - Quartile-based digest
|
||||
/// - Fuzzy matching with Hamming distance
|
||||
/// </summary>
|
||||
public sealed class SimplifiedTlshFingerprinter : IBinaryFingerprinter
|
||||
{
|
||||
private const string Version = "1.0.0-simplified";
|
||||
private const int WindowSize = 5;
|
||||
private const int BucketCount = 256;
|
||||
private const int DigestSize = 32; // 32 bytes = 256 bits
|
||||
|
||||
public FingerprintMethod Method => FingerprintMethod.TLSH;
|
||||
|
||||
public async Task<BinaryFingerprint> ExtractAsync(
|
||||
string binaryPath,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var binaryData = await File.ReadAllBytesAsync(binaryPath, cancellationToken);
|
||||
var binaryName = Path.GetFileName(binaryPath);
|
||||
return await ExtractAsync(binaryData, binaryName, cveId, targetFunction, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<BinaryFingerprint> ExtractAsync(
|
||||
ReadOnlyMemory<byte> binaryData,
|
||||
string binaryName,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hash = ComputeLocalitySensitiveHash(binaryData.Span);
|
||||
var metadata = ExtractMetadata(binaryData.Span, binaryName);
|
||||
|
||||
var fingerprint = new BinaryFingerprint
|
||||
{
|
||||
FingerprintId = $"fingerprint:tlsh:{hash}",
|
||||
CveId = cveId,
|
||||
Method = FingerprintMethod.TLSH,
|
||||
FingerprintValue = hash,
|
||||
TargetBinary = binaryName,
|
||||
TargetFunction = targetFunction,
|
||||
Metadata = metadata,
|
||||
ExtractedAt = DateTimeOffset.UtcNow,
|
||||
ExtractorVersion = Version
|
||||
};
|
||||
|
||||
return Task.FromResult(fingerprint);
|
||||
}
|
||||
|
||||
public async Task<FingerprintMatchResult> MatchAsync(
|
||||
string candidatePath,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var candidateData = await File.ReadAllBytesAsync(candidatePath, cancellationToken);
|
||||
return await MatchAsync(candidateData, knownFingerprint, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<FingerprintMatchResult> MatchAsync(
|
||||
ReadOnlyMemory<byte> candidateData,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var candidateHash = ComputeLocalitySensitiveHash(candidateData.Span);
|
||||
var similarity = ComputeSimilarity(candidateHash, knownFingerprint.FingerprintValue);
|
||||
|
||||
// TLSH matching thresholds:
|
||||
// similarity > 0.90: High confidence match
|
||||
// similarity > 0.75: Medium confidence match
|
||||
// similarity > 0.60: Low confidence match
|
||||
var isMatch = similarity >= 0.60;
|
||||
var confidence = similarity switch
|
||||
{
|
||||
>= 0.90 => 0.85, // Tier 4 max confidence
|
||||
>= 0.75 => 0.70,
|
||||
>= 0.60 => 0.55,
|
||||
_ => 0.0
|
||||
};
|
||||
|
||||
var result = new FingerprintMatchResult
|
||||
{
|
||||
IsMatch = isMatch,
|
||||
Similarity = similarity,
|
||||
Confidence = confidence,
|
||||
MatchedFingerprintId = isMatch ? knownFingerprint.FingerprintId : null,
|
||||
Method = FingerprintMethod.TLSH,
|
||||
MatchDetails = new Dictionary<string, object>
|
||||
{
|
||||
["candidate_hash"] = candidateHash,
|
||||
["known_hash"] = knownFingerprint.FingerprintValue,
|
||||
["hamming_distance"] = ComputeHammingDistance(candidateHash, knownFingerprint.FingerprintValue)
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private static string ComputeLocalitySensitiveHash(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < WindowSize)
|
||||
{
|
||||
// For very small data, fall back to regular hash
|
||||
return Convert.ToHexString(SHA256.HashData(data)).ToLowerInvariant()[..DigestSize];
|
||||
}
|
||||
|
||||
// Step 1: Compute sliding window triplets (pearson hashing)
|
||||
var buckets = new int[BucketCount];
|
||||
for (int i = 0; i < data.Length - WindowSize + 1; i++)
|
||||
{
|
||||
var triplet = ComputeTripletHash(data.Slice(i, WindowSize));
|
||||
buckets[triplet % BucketCount]++;
|
||||
}
|
||||
|
||||
// Step 2: Compute quartiles (Q1, Q2, Q3)
|
||||
var sorted = buckets.OrderBy(b => b).ToArray();
|
||||
var q1 = sorted[BucketCount / 4];
|
||||
var q2 = sorted[BucketCount / 2];
|
||||
var q3 = sorted[3 * BucketCount / 4];
|
||||
|
||||
// Step 3: Generate digest based on quartile comparisons
|
||||
var digest = new byte[DigestSize];
|
||||
for (int i = 0; i < BucketCount && i / 8 < DigestSize; i++)
|
||||
{
|
||||
var byteIdx = i / 8;
|
||||
var bitIdx = i % 8;
|
||||
|
||||
// Set bit based on quartile position
|
||||
if (buckets[i] >= q3)
|
||||
{
|
||||
digest[byteIdx] |= (byte)(1 << bitIdx);
|
||||
}
|
||||
else if (buckets[i] >= q2)
|
||||
{
|
||||
digest[byteIdx] |= (byte)(1 << (bitIdx + 1) % 8);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Add length and checksum metadata
|
||||
var length = Math.Min(data.Length, 0xFFFF);
|
||||
var lengthBytes = BitConverter.GetBytes((ushort)length);
|
||||
digest[0] ^= lengthBytes[0];
|
||||
digest[1] ^= lengthBytes[1];
|
||||
|
||||
return Convert.ToHexString(digest).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static byte ComputeTripletHash(ReadOnlySpan<byte> window)
|
||||
{
|
||||
// Pearson hashing for the window
|
||||
byte hash = 0;
|
||||
foreach (var b in window)
|
||||
{
|
||||
hash = PearsonTable[(hash ^ b) % 256];
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private static double ComputeSimilarity(string hash1, string hash2)
|
||||
{
|
||||
if (hash1.Length != hash2.Length)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var distance = ComputeHammingDistance(hash1, hash2);
|
||||
var maxDistance = hash1.Length * 4; // Each hex char = 4 bits
|
||||
return 1.0 - ((double)distance / maxDistance);
|
||||
}
|
||||
|
||||
private static int ComputeHammingDistance(string hash1, string hash2)
|
||||
{
|
||||
var bytes1 = Convert.FromHexString(hash1);
|
||||
var bytes2 = Convert.FromHexString(hash2);
|
||||
|
||||
var distance = 0;
|
||||
for (int i = 0; i < Math.Min(bytes1.Length, bytes2.Length); i++)
|
||||
{
|
||||
var xor = (byte)(bytes1[i] ^ bytes2[i]);
|
||||
distance += CountBits(xor);
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
private static int CountBits(byte b)
|
||||
{
|
||||
var count = 0;
|
||||
while (b != 0)
|
||||
{
|
||||
count += b & 1;
|
||||
b >>= 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static FingerprintMetadata ExtractMetadata(ReadOnlySpan<byte> data, string binaryName)
|
||||
{
|
||||
// Detect binary format from magic bytes
|
||||
var format = DetectFormat(data);
|
||||
var architecture = DetectArchitecture(data, format);
|
||||
|
||||
return new FingerprintMetadata
|
||||
{
|
||||
Architecture = architecture,
|
||||
Format = format,
|
||||
Compiler = null, // Would require deeper analysis
|
||||
OptimizationLevel = null,
|
||||
HasDebugSymbols = false, // Simplified
|
||||
FileOffset = null,
|
||||
RegionSize = data.Length
|
||||
};
|
||||
}
|
||||
|
||||
private static string DetectFormat(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length < 4) return "unknown";
|
||||
|
||||
// ELF: 0x7F 'E' 'L' 'F'
|
||||
if (data[0] == 0x7F && data[1] == 'E' && data[2] == 'L' && data[3] == 'F')
|
||||
{
|
||||
return "ELF";
|
||||
}
|
||||
|
||||
// PE: 'M' 'Z'
|
||||
if (data[0] == 'M' && data[1] == 'Z')
|
||||
{
|
||||
return "PE";
|
||||
}
|
||||
|
||||
// Mach-O: 0xFEEDFACE or 0xFEEDFACF (32/64-bit)
|
||||
if (data.Length >= 4)
|
||||
{
|
||||
var magic = BitConverter.ToUInt32(data[..4]);
|
||||
if (magic == 0xFEEDFACE || magic == 0xFEEDFACF ||
|
||||
magic == 0xCEFAEDFE || magic == 0xCFFAEDFE)
|
||||
{
|
||||
return "Mach-O";
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private static string DetectArchitecture(ReadOnlySpan<byte> data, string format)
|
||||
{
|
||||
if (format == "ELF" && data.Length >= 18)
|
||||
{
|
||||
var machine = BitConverter.ToUInt16(data.Slice(18, 2));
|
||||
return machine switch
|
||||
{
|
||||
0x3E => "x86_64",
|
||||
0x03 => "x86",
|
||||
0xB7 => "aarch64",
|
||||
0x28 => "armv7",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
|
||||
if (format == "PE" && data.Length >= 0x3C + 4)
|
||||
{
|
||||
// PE offset is at 0x3C
|
||||
var peOffset = BitConverter.ToInt32(data.Slice(0x3C, 4));
|
||||
if (peOffset > 0 && peOffset + 6 < data.Length)
|
||||
{
|
||||
var machine = BitConverter.ToUInt16(data.Slice(peOffset + 4, 2));
|
||||
return machine switch
|
||||
{
|
||||
0x8664 => "x86_64",
|
||||
0x014C => "x86",
|
||||
0xAA64 => "aarch64",
|
||||
_ => "unknown"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// Pearson hash lookup table
|
||||
private static readonly byte[] PearsonTable = new byte[256]
|
||||
{
|
||||
// Standard Pearson hash permutation table
|
||||
98, 6, 85, 150, 36, 23, 112, 164, 135, 207, 169, 5, 26, 64, 165, 219,
|
||||
61, 20, 68, 89, 130, 63, 52, 102, 24, 229, 132, 245, 80, 216, 195, 115,
|
||||
90, 168, 156, 203, 177, 120, 2, 190, 188, 7, 100, 185, 174, 243, 162, 10,
|
||||
237, 18, 253, 225, 8, 208, 172, 244, 255, 126, 101, 79, 145, 235, 228, 121,
|
||||
123, 251, 67, 250, 161, 0, 107, 97, 241, 111, 181, 82, 249, 33, 69, 55,
|
||||
59, 153, 29, 9, 213, 167, 84, 93, 30, 46, 94, 75, 151, 114, 73, 222,
|
||||
197, 96, 210, 45, 16, 227, 248, 202, 51, 152, 252, 125, 81, 206, 215, 186,
|
||||
39, 158, 178, 187, 131, 136, 1, 49, 50, 17, 141, 91, 47, 129, 60, 99,
|
||||
154, 35, 86, 171, 105, 34, 38, 200, 147, 58, 77, 118, 173, 246, 76, 254,
|
||||
133, 232, 196, 144, 198, 124, 53, 4, 108, 74, 223, 234, 134, 230, 157, 139,
|
||||
189, 205, 199, 128, 176, 19, 211, 236, 127, 192, 231, 70, 233, 88, 146, 44,
|
||||
183, 201, 22, 83, 13, 214, 116, 109, 159, 32, 95, 226, 140, 220, 57, 12,
|
||||
221, 31, 209, 182, 143, 92, 149, 184, 148, 62, 113, 65, 37, 27, 106, 166,
|
||||
3, 14, 204, 72, 21, 41, 56, 66, 28, 193, 40, 217, 25, 54, 179, 117,
|
||||
238, 87, 240, 155, 180, 170, 242, 212, 191, 163, 78, 218, 137, 194, 175, 110,
|
||||
43, 119, 224, 71, 122, 142, 42, 160, 104, 48, 247, 103, 15, 11, 138, 239
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
namespace StellaOps.Feedser.BinaryAnalysis;
|
||||
|
||||
using StellaOps.Feedser.BinaryAnalysis.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for extracting binary fingerprints from compiled artifacts.
|
||||
/// </summary>
|
||||
public interface IBinaryFingerprinter
|
||||
{
|
||||
/// <summary>
|
||||
/// Fingerprinting method this implementation provides.
|
||||
/// </summary>
|
||||
FingerprintMethod Method { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Extract fingerprint from binary file.
|
||||
/// </summary>
|
||||
/// <param name="binaryPath">Path to binary file.</param>
|
||||
/// <param name="cveId">Associated CVE ID.</param>
|
||||
/// <param name="targetFunction">Optional function name to fingerprint.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Binary fingerprint.</returns>
|
||||
Task<BinaryFingerprint> ExtractAsync(
|
||||
string binaryPath,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Extract fingerprint from binary bytes.
|
||||
/// </summary>
|
||||
/// <param name="binaryData">Binary data.</param>
|
||||
/// <param name="binaryName">Binary name for identification.</param>
|
||||
/// <param name="cveId">Associated CVE ID.</param>
|
||||
/// <param name="targetFunction">Optional function name to fingerprint.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Binary fingerprint.</returns>
|
||||
Task<BinaryFingerprint> ExtractAsync(
|
||||
ReadOnlyMemory<byte> binaryData,
|
||||
string binaryName,
|
||||
string? cveId,
|
||||
string? targetFunction = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Match candidate binary against known fingerprint.
|
||||
/// </summary>
|
||||
/// <param name="candidatePath">Path to candidate binary.</param>
|
||||
/// <param name="knownFingerprint">Known fingerprint to match against.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Match result.</returns>
|
||||
Task<FingerprintMatchResult> MatchAsync(
|
||||
string candidatePath,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Match candidate binary bytes against known fingerprint.
|
||||
/// </summary>
|
||||
/// <param name="candidateData">Candidate binary data.</param>
|
||||
/// <param name="knownFingerprint">Known fingerprint to match against.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Match result.</returns>
|
||||
Task<FingerprintMatchResult> MatchAsync(
|
||||
ReadOnlyMemory<byte> candidateData,
|
||||
BinaryFingerprint knownFingerprint,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
namespace StellaOps.Feedser.BinaryAnalysis.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Binary fingerprint for matching patched code in compiled artifacts (Tier 4).
|
||||
/// </summary>
|
||||
public sealed record BinaryFingerprint
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique fingerprint identifier.
|
||||
/// Format: "fingerprint:{method}:{hash}"
|
||||
/// </summary>
|
||||
public required string FingerprintId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CVE ID this fingerprint is associated with.
|
||||
/// </summary>
|
||||
public required string? CveId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprinting method used.
|
||||
/// </summary>
|
||||
public required FingerprintMethod Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary hash or signature value.
|
||||
/// </summary>
|
||||
public required string FingerprintValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary file or symbol this fingerprint applies to.
|
||||
/// </summary>
|
||||
public required string TargetBinary { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional function or symbol name.
|
||||
/// </summary>
|
||||
public string? TargetFunction { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Metadata about the fingerprint.
|
||||
/// </summary>
|
||||
public required FingerprintMetadata Metadata { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When this fingerprint was extracted.
|
||||
/// </summary>
|
||||
public required DateTimeOffset ExtractedAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Version of the extraction tool.
|
||||
/// </summary>
|
||||
public required string ExtractorVersion { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fingerprinting method.
|
||||
/// </summary>
|
||||
public enum FingerprintMethod
|
||||
{
|
||||
/// <summary>
|
||||
/// Trend Micro Locality Sensitive Hash (fuzzy hashing).
|
||||
/// </summary>
|
||||
TLSH,
|
||||
|
||||
/// <summary>
|
||||
/// Function-level control flow graph hash.
|
||||
/// </summary>
|
||||
CFGHash,
|
||||
|
||||
/// <summary>
|
||||
/// Normalized instruction sequence hash.
|
||||
/// </summary>
|
||||
InstructionHash,
|
||||
|
||||
/// <summary>
|
||||
/// Symbol table hash.
|
||||
/// </summary>
|
||||
SymbolHash,
|
||||
|
||||
/// <summary>
|
||||
/// Section hash (e.g., .text section).
|
||||
/// </summary>
|
||||
SectionHash
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metadata for a binary fingerprint.
|
||||
/// </summary>
|
||||
public sealed record FingerprintMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Architecture (e.g., x86_64, aarch64, armv7).
|
||||
/// </summary>
|
||||
public required string Architecture { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Binary format (ELF, PE, Mach-O).
|
||||
/// </summary>
|
||||
public required string Format { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compiler and version if detected.
|
||||
/// </summary>
|
||||
public string? Compiler { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optimization level if detected.
|
||||
/// </summary>
|
||||
public string? OptimizationLevel { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Debug symbols present.
|
||||
/// </summary>
|
||||
public required bool HasDebugSymbols { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// File offset of the fingerprinted region.
|
||||
/// </summary>
|
||||
public long? FileOffset { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the fingerprinted region in bytes.
|
||||
/// </summary>
|
||||
public long? RegionSize { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of fingerprint matching.
|
||||
/// </summary>
|
||||
public sealed record FingerprintMatchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a match was found.
|
||||
/// </summary>
|
||||
public required bool IsMatch { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Similarity score (0.0-1.0).
|
||||
/// </summary>
|
||||
public required double Similarity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Confidence in the match (0.0-1.0).
|
||||
/// </summary>
|
||||
public required double Confidence { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Matching fingerprint ID.
|
||||
/// </summary>
|
||||
public string? MatchedFingerprintId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Method used for matching.
|
||||
/// </summary>
|
||||
public required FingerprintMethod Method { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Additional matching details.
|
||||
/// </summary>
|
||||
public Dictionary<string, object>? MatchDetails { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user