mirror of
https://github.com/praetorian-inc/wasmforge
synced 2026-06-22 22:52:31 +00:00
438 lines
19 KiB
C#
438 lines
19 KiB
C#
// WfVault.cs — managed helpers for vaultcli.dll (Windows Vault enumeration).
|
|
//
|
|
// Mirrors the WfNetapi pattern: Resolve→Invoke→CopyHostToWasm against the
|
|
// host-side mod_load/mod_resolve/mod_invoke bridge. Reading host-allocated
|
|
// VAULT_ITEM_* structs requires copying their bytes into WASM memory first
|
|
// (Marshal.PtrToStructure assumes the IntPtr is addressable in the current
|
|
// process — false on wasm32, where IntPtr is 4 bytes).
|
|
//
|
|
// The high-level EnumerateAll() returns parsed entries so the consumer
|
|
// (WindowsVaultCommand) can keep its DTO shape without dealing with host
|
|
// pointers directly.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
|
|
namespace WasmForge.Helpers
|
|
{
|
|
public static unsafe class WfVault
|
|
{
|
|
[DllImport("env", EntryPoint = "mod_load")]
|
|
private static extern uint mod_load(uint namePtr);
|
|
|
|
[DllImport("env", EntryPoint = "mod_resolve")]
|
|
private static extern uint mod_resolve(uint libHandle, uint namePtr);
|
|
|
|
[DllImport("env", EntryPoint = "mod_invoke")]
|
|
private static extern ulong mod_invoke(
|
|
ulong procHandle, uint nargs,
|
|
ulong a0, ulong a1, ulong a2, ulong a3,
|
|
ulong a4, ulong a5, ulong a6, ulong a7,
|
|
ulong a8, ulong a9, ulong a10, ulong a11,
|
|
ulong a12, ulong a13, ulong a14,
|
|
ulong ret1Ptr, ulong errPtr);
|
|
|
|
// ── Cached library + proc handles ───────────────────────────────
|
|
private static uint _vaultcli;
|
|
private static uint _hEnumerateVaults;
|
|
private static uint _hOpenVault;
|
|
private static uint _hEnumerateItems;
|
|
private static uint _hGetItemWin8;
|
|
private static uint _hGetItemWin7;
|
|
private static uint _hCloseVault;
|
|
private static uint _hVaultFree;
|
|
private static uint _ntdll;
|
|
private static uint _hRtlMoveMemory;
|
|
private static uint _advapi32;
|
|
private static uint _hConvertSidToStringSidW;
|
|
private static uint _kernel32;
|
|
private static uint _hLocalFree;
|
|
|
|
private static uint Resolve(string dll, ref uint cachedLib, string fn, ref uint cachedProc)
|
|
{
|
|
if (cachedProc != 0) return cachedProc;
|
|
if (cachedLib == 0)
|
|
{
|
|
byte[] db = Encoding.ASCII.GetBytes(dll + "\0");
|
|
fixed (byte* dp = db) cachedLib = mod_load((uint)(IntPtr)dp);
|
|
if (cachedLib == 0) return 0;
|
|
}
|
|
byte[] fb = Encoding.ASCII.GetBytes(fn + "\0");
|
|
fixed (byte* fp = fb) cachedProc = mod_resolve(cachedLib, (uint)(IntPtr)fp);
|
|
return cachedProc;
|
|
}
|
|
|
|
private static uint Invoke(uint proc, uint nargs,
|
|
ulong a0 = 0, ulong a1 = 0, ulong a2 = 0, ulong a3 = 0,
|
|
ulong a4 = 0, ulong a5 = 0, ulong a6 = 0, ulong a7 = 0)
|
|
{
|
|
ulong ret1 = 0, err = 0;
|
|
ulong r0 = mod_invoke((ulong)proc, nargs,
|
|
a0, a1, a2, a3, a4, a5, a6, a7, 0, 0, 0, 0, 0, 0, 0,
|
|
(ulong)(uint)(IntPtr)(&ret1),
|
|
(ulong)(uint)(IntPtr)(&err));
|
|
return (uint)r0;
|
|
}
|
|
|
|
private static bool CopyHostToWasm(ulong hostAddr, uint wasmPtr, uint len)
|
|
{
|
|
if (hostAddr == 0 || wasmPtr == 0 || len == 0) return false;
|
|
uint pCopy = Resolve("ntdll.dll", ref _ntdll, "RtlMoveMemory", ref _hRtlMoveMemory);
|
|
if (pCopy == 0) return false;
|
|
Invoke(pCopy, 3, (ulong)wasmPtr, hostAddr, (ulong)len);
|
|
return true;
|
|
}
|
|
|
|
private static ulong Read8(byte* p, int off)
|
|
{
|
|
return ((ulong)p[off+0]) | ((ulong)p[off+1] << 8) |
|
|
((ulong)p[off+2] << 16) | ((ulong)p[off+3] << 24) |
|
|
((ulong)p[off+4] << 32) | ((ulong)p[off+5] << 40) |
|
|
((ulong)p[off+6] << 48) | ((ulong)p[off+7] << 56);
|
|
}
|
|
|
|
private static uint Read4(byte* p, int off)
|
|
{
|
|
return (uint)(p[off+0] | (p[off+1] << 8) | (p[off+2] << 16) | (p[off+3] << 24));
|
|
}
|
|
|
|
private static string ReadWStringFromHost(ulong hostAddr, int maxChars)
|
|
{
|
|
if (hostAddr == 0 || maxChars <= 0) return "";
|
|
byte[] buf = new byte[maxChars * 2];
|
|
fixed (byte* bp = buf)
|
|
{
|
|
if (!CopyHostToWasm(hostAddr, (uint)(IntPtr)bp, (uint)buf.Length)) return "";
|
|
}
|
|
int charLen = 0;
|
|
for (int i = 0; i < maxChars; i++)
|
|
{
|
|
if (buf[2*i] == 0 && buf[2*i + 1] == 0) break;
|
|
charLen++;
|
|
}
|
|
if (charLen == 0) return "";
|
|
char[] chars = new char[charLen];
|
|
for (int i = 0; i < charLen; i++)
|
|
chars[i] = (char)(buf[2*i] | (buf[2*i + 1] << 8));
|
|
return new string(chars);
|
|
}
|
|
|
|
private static Guid ReadGuidFromHost(ulong hostAddr)
|
|
{
|
|
if (hostAddr == 0) return Guid.Empty;
|
|
byte[] buf = new byte[16];
|
|
fixed (byte* bp = buf)
|
|
{
|
|
if (!CopyHostToWasm(hostAddr, (uint)(IntPtr)bp, 16)) return Guid.Empty;
|
|
}
|
|
return new Guid(buf);
|
|
}
|
|
|
|
private static Guid ReadGuidInline(byte* p, int off)
|
|
{
|
|
byte[] g = new byte[16];
|
|
for (int i = 0; i < 16; i++) g[i] = p[off + i];
|
|
return new Guid(g);
|
|
}
|
|
|
|
// ── Public data types ───────────────────────────────────────────
|
|
|
|
public class VaultEntryData
|
|
{
|
|
public Guid SchemaGuid;
|
|
public string? Resource;
|
|
public string? Identity;
|
|
public string? PackageSid;
|
|
public string? CredentialString; // when authenticator is a UTF-16 string
|
|
public byte[]? CredentialBytes; // when authenticator is a byte array
|
|
public DateTime LastModifiedUtc;
|
|
}
|
|
|
|
public class VaultData
|
|
{
|
|
public Guid VaultGuid;
|
|
public List<VaultEntryData> Entries = new List<VaultEntryData>();
|
|
}
|
|
|
|
// ── High-level enumeration ──────────────────────────────────────
|
|
|
|
public static List<VaultData> EnumerateAll()
|
|
{
|
|
var results = new List<VaultData>();
|
|
|
|
uint pEnumV = Resolve("vaultcli.dll", ref _vaultcli, "VaultEnumerateVaults", ref _hEnumerateVaults);
|
|
uint pOpen = Resolve("vaultcli.dll", ref _vaultcli, "VaultOpenVault", ref _hOpenVault);
|
|
uint pEnumI = Resolve("vaultcli.dll", ref _vaultcli, "VaultEnumerateItems", ref _hEnumerateItems);
|
|
uint pClose = Resolve("vaultcli.dll", ref _vaultcli, "VaultCloseVault", ref _hCloseVault);
|
|
uint pFree = Resolve("vaultcli.dll", ref _vaultcli, "VaultFree", ref _hVaultFree);
|
|
uint pGetW8 = Resolve("vaultcli.dll", ref _vaultcli, "VaultGetItem", ref _hGetItemWin8);
|
|
// WIN7 entry point is the same export, just a different arg count
|
|
_hGetItemWin7 = _hGetItemWin8;
|
|
if (pEnumV == 0 || pOpen == 0 || pEnumI == 0) return results;
|
|
|
|
bool isWin8 = Environment.OSVersion.Version > new Version("6.2");
|
|
int vaultItemSize = isWin8 ? 80 : 72;
|
|
|
|
// Step 1: enumerate vault GUIDs.
|
|
int vaultCount = 0;
|
|
ulong vaultGuidArrayHost = 0;
|
|
uint rc = Invoke(pEnumV, 3,
|
|
0u,
|
|
(ulong)(uint)(IntPtr)(&vaultCount),
|
|
(ulong)(uint)(IntPtr)(&vaultGuidArrayHost));
|
|
if (rc != 0 || vaultCount <= 0 || vaultGuidArrayHost == 0) return results;
|
|
if (vaultCount > 64) vaultCount = 64; // sanity cap
|
|
|
|
// Pull the GUID array into WASM memory.
|
|
byte[] guidBuf = new byte[vaultCount * 16];
|
|
fixed (byte* gp = guidBuf)
|
|
{
|
|
if (!CopyHostToWasm(vaultGuidArrayHost, (uint)(IntPtr)gp, (uint)guidBuf.Length))
|
|
{
|
|
return results;
|
|
}
|
|
|
|
for (int i = 0; i < vaultCount; i++)
|
|
{
|
|
Guid vaultGuid = ReadGuidInline(gp, i * 16);
|
|
var vd = new VaultData { VaultGuid = vaultGuid };
|
|
|
|
// Step 2: open the vault.
|
|
ulong vaultHandle = 0;
|
|
Guid gMut = vaultGuid;
|
|
uint openRc = Invoke(pOpen, 3,
|
|
(ulong)(uint)(IntPtr)(&gMut),
|
|
0u,
|
|
(ulong)(uint)(IntPtr)(&vaultHandle));
|
|
if (openRc != 0 || vaultHandle == 0)
|
|
{
|
|
results.Add(vd); // empty entries list — same shape as native on open-fail
|
|
continue;
|
|
}
|
|
|
|
// Step 3: enumerate items.
|
|
int itemCount = 0;
|
|
ulong itemArrayHost = 0;
|
|
uint itemRc = Invoke(pEnumI, 4,
|
|
vaultHandle,
|
|
512u, // chunkSize matches original
|
|
(ulong)(uint)(IntPtr)(&itemCount),
|
|
(ulong)(uint)(IntPtr)(&itemArrayHost));
|
|
if (itemRc == 0 && itemCount > 0 && itemArrayHost != 0)
|
|
{
|
|
if (itemCount > 4096) itemCount = 4096;
|
|
ParseVaultItems(vd, vaultHandle, itemArrayHost, itemCount, vaultItemSize, isWin8, pGetW8);
|
|
}
|
|
|
|
if (pClose != 0)
|
|
{
|
|
ulong vh = vaultHandle;
|
|
Invoke(pClose, 1, (ulong)(uint)(IntPtr)(&vh));
|
|
}
|
|
|
|
results.Add(vd);
|
|
}
|
|
}
|
|
|
|
// VaultFree takes the buffer pointer directly (PVOID Buffer), not a
|
|
// pointer-to-pointer — passing &vg would translate to a stack
|
|
// address rather than the host buffer address. Reviewer-caught
|
|
// bug; latent on lab box because vaultCount=0 short-circuits.
|
|
if (pFree != 0) Invoke(pFree, 1, vaultGuidArrayHost);
|
|
|
|
return results;
|
|
}
|
|
|
|
private static void ParseVaultItems(VaultData vd, ulong vaultHandle, ulong itemArrayHost,
|
|
int itemCount, int vaultItemSize, bool isWin8, uint pGetItem)
|
|
{
|
|
byte[] items = new byte[itemCount * vaultItemSize];
|
|
fixed (byte* ip = items)
|
|
{
|
|
if (!CopyHostToWasm(itemArrayHost, (uint)(IntPtr)ip, (uint)items.Length)) return;
|
|
|
|
for (int j = 0; j < itemCount; j++)
|
|
{
|
|
int b = j * vaultItemSize;
|
|
// VAULT_ITEM_WIN8 layout (x64):
|
|
// 0: Guid SchemaId (16)
|
|
// 16: IntPtr pszCredentialFriendlyName (8)
|
|
// 24: IntPtr pResourceElement (8)
|
|
// 32: IntPtr pIdentityElement (8)
|
|
// 40: IntPtr pAuthenticatorElement (8)
|
|
// 48: IntPtr pPackageSid (8) // win8+ only
|
|
// 56: ulong LastModified (8)
|
|
// WIN7 omits pPackageSid; LastModified is at offset 48.
|
|
Guid schemaId = ReadGuidInline(ip, b + 0);
|
|
ulong pResource = Read8(ip, b + 24);
|
|
ulong pIdentity = Read8(ip, b + 32);
|
|
ulong pAuth = Read8(ip, b + 40);
|
|
ulong pPackageSid = isWin8 ? Read8(ip, b + 48) : 0UL;
|
|
ulong lastModified = isWin8 ? Read8(ip, b + 56) : Read8(ip, b + 48);
|
|
|
|
// VaultGetItem returns a pointer to a newly-allocated
|
|
// VAULT_ITEM_*. The pAuthenticatorElement field on the
|
|
// RETURNED item is populated; the input item's
|
|
// pAuthenticatorElement is typically null/stub.
|
|
ulong passwordItemPtr = 0;
|
|
Guid sidMut = schemaId;
|
|
uint getRc;
|
|
if (isWin8)
|
|
{
|
|
getRc = Invoke(pGetItem, 8,
|
|
vaultHandle,
|
|
(ulong)(uint)(IntPtr)(&sidMut),
|
|
pResource,
|
|
pIdentity,
|
|
pPackageSid,
|
|
0UL, // IntPtr.Zero
|
|
0UL, // arg6 = 0
|
|
(ulong)(uint)(IntPtr)(&passwordItemPtr));
|
|
}
|
|
else
|
|
{
|
|
getRc = Invoke(pGetItem, 7,
|
|
vaultHandle,
|
|
(ulong)(uint)(IntPtr)(&sidMut),
|
|
pResource,
|
|
pIdentity,
|
|
0UL,
|
|
0UL,
|
|
(ulong)(uint)(IntPtr)(&passwordItemPtr));
|
|
}
|
|
|
|
ulong pAuthFromReturned = 0;
|
|
if (getRc == 0 && passwordItemPtr != 0)
|
|
{
|
|
// Read the returned item's pAuthenticatorElement (same offset 40 on both layouts).
|
|
byte[] ret = new byte[vaultItemSize];
|
|
fixed (byte* rp = ret)
|
|
{
|
|
if (CopyHostToWasm(passwordItemPtr, (uint)(IntPtr)rp, (uint)ret.Length))
|
|
{
|
|
pAuthFromReturned = Read8(rp, 40);
|
|
}
|
|
}
|
|
}
|
|
if (pAuthFromReturned == 0) pAuthFromReturned = pAuth;
|
|
|
|
var entry = new VaultEntryData
|
|
{
|
|
SchemaGuid = schemaId,
|
|
Resource = pResource != 0 ? ReadVaultElementString(pResource) : null,
|
|
Identity = pIdentity != 0 ? ReadVaultElementString(pIdentity) : null,
|
|
PackageSid = pPackageSid != 0 ? ReadVaultElementSid(pPackageSid) : null,
|
|
LastModifiedUtc = DateTime.FromFileTimeUtc((long)lastModified),
|
|
};
|
|
|
|
if (pAuthFromReturned != 0)
|
|
{
|
|
var (s, bytes) = ReadVaultElementAuth(pAuthFromReturned);
|
|
entry.CredentialString = s;
|
|
entry.CredentialBytes = bytes;
|
|
}
|
|
|
|
vd.Entries.Add(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── VAULT_ITEM_ELEMENT readers ──────────────────────────────────
|
|
//
|
|
// Layout (LayoutKind.Explicit, x64):
|
|
// 0: int SchemaElementId
|
|
// 4: (padding)
|
|
// 8: int Type (VAULT_ELEMENT_TYPE enum)
|
|
// 12: (padding)
|
|
// 16: value union
|
|
//
|
|
// String: IntPtr at +16 → UTF-16 string in host memory
|
|
// Sid: IntPtr at +16 → PSID in host memory
|
|
// ByteArray: { int Length @+16; IntPtr pData @+24 }
|
|
|
|
private const int VAULT_ELEMENT_HEADER = 16;
|
|
|
|
private static (int type, byte[] body)? ReadElementHeader(ulong hostPtr, int bodyLen)
|
|
{
|
|
byte[] buf = new byte[VAULT_ELEMENT_HEADER + bodyLen];
|
|
fixed (byte* bp = buf)
|
|
{
|
|
if (!CopyHostToWasm(hostPtr, (uint)(IntPtr)bp, (uint)buf.Length)) return null;
|
|
}
|
|
int type = (int)((uint)buf[8] | ((uint)buf[9] << 8) | ((uint)buf[10] << 16) | ((uint)buf[11] << 24));
|
|
return (type, buf);
|
|
}
|
|
|
|
private static string? ReadVaultElementString(ulong hostPtr)
|
|
{
|
|
var h = ReadElementHeader(hostPtr, 8);
|
|
if (h == null) return null;
|
|
// Type 7 = String; type 0 = Boolean (1 byte); fall through and best-effort.
|
|
ulong strHost;
|
|
fixed (byte* bp = h.Value.body) strHost = Read8(bp, VAULT_ELEMENT_HEADER);
|
|
if (strHost == 0) return null;
|
|
return ReadWStringFromHost(strHost, 1024);
|
|
}
|
|
|
|
private static string? ReadVaultElementSid(ulong hostPtr)
|
|
{
|
|
var h = ReadElementHeader(hostPtr, 8);
|
|
if (h == null) return null;
|
|
ulong sidHost;
|
|
fixed (byte* bp = h.Value.body) sidHost = Read8(bp, VAULT_ELEMENT_HEADER);
|
|
if (sidHost == 0) return null;
|
|
|
|
uint pConv = Resolve("advapi32.dll", ref _advapi32, "ConvertSidToStringSidW", ref _hConvertSidToStringSidW);
|
|
uint pLocalFree = Resolve("kernel32.dll", ref _kernel32, "LocalFree", ref _hLocalFree);
|
|
if (pConv == 0) return null;
|
|
|
|
ulong sidWPtr = 0;
|
|
Invoke(pConv, 2, sidHost, (ulong)(uint)(IntPtr)(&sidWPtr));
|
|
if (sidWPtr == 0) return null;
|
|
string s = ReadWStringFromHost(sidWPtr, 256);
|
|
if (pLocalFree != 0) Invoke(pLocalFree, 1, sidWPtr);
|
|
return s;
|
|
}
|
|
|
|
// Authenticator element: usually String (web/domain passwords), occasionally ByteArray.
|
|
// Returns (string, null) for String elements, (null, bytes) for ByteArray elements.
|
|
private static (string? str, byte[]? bytes) ReadVaultElementAuth(ulong hostPtr)
|
|
{
|
|
var h = ReadElementHeader(hostPtr, 24);
|
|
if (h == null) return (null, null);
|
|
int type = h.Value.type;
|
|
byte[] body = h.Value.body;
|
|
|
|
switch (type)
|
|
{
|
|
case 7: // String
|
|
{
|
|
ulong strHost;
|
|
fixed (byte* bp = body) strHost = Read8(bp, VAULT_ELEMENT_HEADER);
|
|
if (strHost == 0) return (null, null);
|
|
return (ReadWStringFromHost(strHost, 4096), null);
|
|
}
|
|
case 8: // ByteArray
|
|
{
|
|
uint byteLen;
|
|
ulong byteDataHost;
|
|
fixed (byte* bp = body)
|
|
{
|
|
byteLen = Read4(bp, VAULT_ELEMENT_HEADER + 0);
|
|
byteDataHost = Read8(bp, VAULT_ELEMENT_HEADER + 8);
|
|
}
|
|
if (byteLen == 0 || byteDataHost == 0) return (null, new byte[0]);
|
|
if (byteLen > 65536) byteLen = 65536;
|
|
byte[] data = new byte[byteLen];
|
|
fixed (byte* dp = data) CopyHostToWasm(byteDataHost, (uint)(IntPtr)dp, byteLen);
|
|
return (null, data);
|
|
}
|
|
default:
|
|
return (null, null);
|
|
}
|
|
}
|
|
}
|
|
}
|