Add files via upload

This commit is contained in:
Alloy Secure Group
2026-07-22 10:08:27 -06:00
committed by GitHub
parent 5719c89f0a
commit 82fb086730
+409
View File
@@ -0,0 +1,409 @@
// BindlinkSentinelSim.cs
//
// Test harness for BindlinkSentinel's detection logic. C# 5, .NET Framework 4.8.
// No string interpolation. No admin required. No bind links are created.
//
// What it does: a shadow bind link over a trusted path is observable to a
// user-mode reader as (a) bytes that no longer match a clean baseline and
// (b) a signature that no longer validates. This harness reproduces that
// observable effect in a temp sandbox by baselining a file and then swapping
// its contents, then asserts the sensor's sweep and decoy logic react.
//
// It does NOT create a bind link and does NOT redirect any real system path.
// For an end-to-end test against the real bindflt path (and the mapping-
// enumeration stub), create a real mapping with Bitdefender's bindutil in a
// disposable, snapshotted VM. See the project README.
//
// Build:
// csc /langversion:5 /target:exe /out:BindlinkSentinelSim.exe BindlinkSentinelSim.cs
//
// Run:
// BindlinkSentinelSim.exe
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
namespace BindlinkSentinelSim
{
internal static class Program
{
private static int _passed;
private static int _failed;
private static int Main()
{
string sandbox = Path.Combine(
Path.GetTempPath(), "BindlinkSentinelSim");
try
{
if (Directory.Exists(sandbox))
{
Directory.Delete(sandbox, true);
}
Directory.CreateDirectory(sandbox);
Console.WriteLine("Sandbox: " + sandbox);
Console.WriteLine();
TestSignatureChecker(sandbox);
TestTrustedPathRedirectEmulation(sandbox);
TestDecoyWatcher(sandbox);
}
finally
{
try { Directory.Delete(sandbox, true); }
catch (IOException) { }
}
Console.WriteLine();
Console.WriteLine(string.Format(
"Results: {0} passed, {1} failed", _passed, _failed));
return _failed == 0 ? 0 : 1;
}
// --------------------------------------------------------------------
// Assertions
// --------------------------------------------------------------------
private static void Check(string name, bool condition)
{
if (condition)
{
_passed++;
Console.WriteLine(" PASS " + name);
}
else
{
_failed++;
Console.WriteLine(" FAIL " + name);
}
}
// --------------------------------------------------------------------
// Test 1: the signature checker recognizes signed and unsigned files.
// --------------------------------------------------------------------
private static void TestSignatureChecker(string sandbox)
{
Console.WriteLine("Test 1: signature checker sanity");
// A genuine, unmodified system binary should verify (read-only, we
// do not touch it). This confirms WinVerifyTrust wiring is correct.
string known = Path.Combine(
Environment.SystemDirectory, "winver.exe");
if (File.Exists(known))
{
Check("signed system binary verifies as signed",
VerifyEmbeddedSignature(known));
}
else
{
Console.WriteLine(" SKIP winver.exe not found");
}
// An arbitrary blob is not validly signed.
string junk = Path.Combine(sandbox, "unsigned_payload.bin");
File.WriteAllBytes(junk, RandomBytes(4096));
Check("arbitrary payload reads as unsigned",
!VerifyEmbeddedSignature(junk));
Console.WriteLine();
}
// --------------------------------------------------------------------
// Test 2: emulate a shadow bind link over a trusted path and assert the
// sensor's sweep raises the expected alerts.
// --------------------------------------------------------------------
private static void TestTrustedPathRedirectEmulation(string sandbox)
{
Console.WriteLine("Test 2: trusted-path redirect emulation");
// Stand in for a trusted, signed binary at a watched path.
string trustedPath = Path.Combine(sandbox, "trusted_app.exe");
string cleanSource = Path.Combine(
Environment.SystemDirectory, "winver.exe");
if (!File.Exists(cleanSource))
{
Console.WriteLine(" SKIP no clean source binary available");
Console.WriteLine();
return;
}
File.Copy(cleanSource, trustedPath, true);
// Baseline as if captured on a clean host: the clean hash, signed.
string cleanHash = ComputeSha256(trustedPath);
Dictionary<string, string> baseline =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
baseline[trustedPath] = string.Concat(cleanHash, "|1");
string[] watchlist = new string[] { trustedPath };
// Sweep while clean: no alerts expected.
List<string> clean = Sweep(watchlist, baseline);
Check("clean state raises no alerts", clean.Count == 0);
// Emulate the redirect: the same path now returns payload bytes,
// exactly what a reader sees when a shadow bind link points this
// path at an attacker file. The original bytes are gone here, which
// is the one way the emulation differs from a real bind link (a
// real link leaves the on-disk file untouched). The detection
// signals the sensor uses are identical either way.
File.WriteAllBytes(trustedPath, RandomBytes(8192));
List<string> dirty = Sweep(watchlist, baseline);
Check("hash-divergence alert fires",
ContainsSubstring(dirty, "hash divergence"));
Check("signature-invalid alert fires",
ContainsSubstring(dirty, "signature no longer valid"));
Console.WriteLine();
}
// --------------------------------------------------------------------
// Test 3: touching a decoy lure is detected.
// --------------------------------------------------------------------
private static void TestDecoyWatcher(string sandbox)
{
Console.WriteLine("Test 3: decoy watcher");
string decoyDir = Path.Combine(sandbox, "decoy");
Directory.CreateDirectory(decoyDir);
string bait = Path.Combine(decoyDir, "credentials.txt");
File.WriteAllText(bait, "bait");
bool fired = false;
object gate = new object();
using (FileSystemWatcher w = new FileSystemWatcher(decoyDir))
{
w.IncludeSubdirectories = true;
w.NotifyFilter = NotifyFilters.FileName
| NotifyFilters.LastWrite
| NotifyFilters.Size;
FileSystemEventHandler onChange =
delegate(object s, FileSystemEventArgs e)
{
lock (gate) { fired = true; }
};
w.Created += onChange;
w.Changed += onChange;
w.Deleted += onChange;
w.EnableRaisingEvents = true;
// Attacker recon or staging touches the bait.
File.AppendAllText(bait, " tampered");
// Give the watcher a moment to deliver the event.
for (int i = 0; i < 50; i++)
{
lock (gate) { if (fired) break; }
Thread.Sleep(20);
}
}
Check("decoy modification is detected", fired);
Console.WriteLine();
}
// --------------------------------------------------------------------
// Detection logic under test (mirrors the sensor's SweepWatchlist).
// --------------------------------------------------------------------
private static List<string> Sweep(
string[] watchlist, Dictionary<string, string> baseline)
{
List<string> alerts = new List<string>();
foreach (string path in watchlist)
{
string liveHash = ComputeSha256(path);
if (liveHash == null)
{
alerts.Add("watched path unreadable: " + path);
continue;
}
bool liveSigned = VerifyEmbeddedSignature(path);
string stored;
if (!baseline.TryGetValue(path, out stored))
{
continue;
}
string baseHash = stored;
bool baseSigned = false;
int bar = stored.IndexOf('|');
if (bar >= 0)
{
baseHash = stored.Substring(0, bar);
baseSigned = stored.Substring(bar + 1) == "1";
}
if (!string.Equals(
liveHash, baseHash, StringComparison.OrdinalIgnoreCase))
{
alerts.Add("hash divergence on trusted path: " + path);
}
if (baseSigned && !liveSigned)
{
alerts.Add("signature no longer valid on trusted path: " + path);
}
}
return alerts;
}
private static bool ContainsSubstring(List<string> items, string needle)
{
foreach (string s in items)
{
if (s.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
}
return false;
}
// --------------------------------------------------------------------
// Shared primitives (same approach as the sensor)
// --------------------------------------------------------------------
private static byte[] RandomBytes(int count)
{
byte[] b = new byte[count];
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(b);
}
return b;
}
private static string ComputeSha256(string path)
{
try
{
using (FileStream fs = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.Read))
using (SHA256 sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(fs);
StringBuilder sb = new StringBuilder(hash.Length * 2);
for (int i = 0; i < hash.Length; i++)
{
sb.Append(hash[i].ToString("x2"));
}
return sb.ToString();
}
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}
private static readonly Guid WINTRUST_ACTION_GENERIC_VERIFY_V2 =
new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE");
private const uint WTD_UI_NONE = 2;
private const uint WTD_REVOKE_NONE = 0;
private const uint WTD_CHOICE_FILE = 1;
private const uint WTD_STATEACTION_VERIFY = 1;
private const uint WTD_STATEACTION_CLOSE = 2;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WINTRUST_FILE_INFO
{
public uint cbStruct;
[MarshalAs(UnmanagedType.LPWStr)] public string pcwszFilePath;
public IntPtr hFile;
public IntPtr pgKnownSubject;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WINTRUST_DATA
{
public uint cbStruct;
public IntPtr pPolicyCallbackData;
public IntPtr pSIPClientData;
public uint dwUIChoice;
public uint fdwRevocationChecks;
public uint dwUnionChoice;
public IntPtr pFile;
public uint dwStateAction;
public IntPtr hWVTStateData;
[MarshalAs(UnmanagedType.LPWStr)] public string pwszURLReference;
public uint dwProvFlags;
public uint dwUIContext;
}
[DllImport("wintrust.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern int WinVerifyTrust(
IntPtr hwnd,
[MarshalAs(UnmanagedType.LPStruct)] Guid pgActionID,
IntPtr pWVTData);
private static bool VerifyEmbeddedSignature(string path)
{
IntPtr pFile = IntPtr.Zero;
IntPtr pData = IntPtr.Zero;
try
{
WINTRUST_FILE_INFO fileInfo = new WINTRUST_FILE_INFO();
fileInfo.cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_FILE_INFO));
fileInfo.pcwszFilePath = path;
fileInfo.hFile = IntPtr.Zero;
fileInfo.pgKnownSubject = IntPtr.Zero;
pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_FILE_INFO)));
Marshal.StructureToPtr(fileInfo, pFile, false);
WINTRUST_DATA data = new WINTRUST_DATA();
data.cbStruct = (uint)Marshal.SizeOf(typeof(WINTRUST_DATA));
data.dwUIChoice = WTD_UI_NONE;
data.fdwRevocationChecks = WTD_REVOKE_NONE;
data.dwUnionChoice = WTD_CHOICE_FILE;
data.pFile = pFile;
data.dwStateAction = WTD_STATEACTION_VERIFY;
pData = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINTRUST_DATA)));
Marshal.StructureToPtr(data, pData, false);
IntPtr invalidHandle = new IntPtr(-1);
int status = WinVerifyTrust(
invalidHandle, WINTRUST_ACTION_GENERIC_VERIFY_V2, pData);
WINTRUST_DATA closeData =
(WINTRUST_DATA)Marshal.PtrToStructure(pData, typeof(WINTRUST_DATA));
closeData.dwStateAction = WTD_STATEACTION_CLOSE;
Marshal.StructureToPtr(closeData, pData, false);
WinVerifyTrust(
invalidHandle, WINTRUST_ACTION_GENERIC_VERIFY_V2, pData);
return status == 0;
}
finally
{
if (pData != IntPtr.Zero)
{
Marshal.DestroyStructure(pData, typeof(WINTRUST_DATA));
Marshal.FreeHGlobal(pData);
}
if (pFile != IntPtr.Zero)
{
Marshal.DestroyStructure(pFile, typeof(WINTRUST_FILE_INFO));
Marshal.FreeHGlobal(pFile);
}
}
}
}
}