mirror of
https://github.com/AlloySecureGroup/BlinkLinkSentiennel
synced 2026-08-09 11:57:38 +00:00
Add files via upload
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user