using System.Buffers.Binary; using System.ComponentModel; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Memory; using Windows.Win32.System.Threading; using WxAgent.Core; namespace WxAgent.Windows; public sealed record ProcessAccessProbe(bool Success, string? Error); public static unsafe class ProcessMemoryScanner { private const int ChunkSize = 1024 * 1024; private const ulong MaximumRegionSize = 500UL * 1024 * 1024; private const ulong MaximumUserAddress = 0x00007FFFFFFEFFFF; public static ProcessAccessProbe Probe(int processId) { var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION | PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ, false, (uint)processId); if (process.IsNull) { return new ProcessAccessProbe(false, new Win32Exception(Marshal.GetLastWin32Error()).Message); } PInvoke.CloseHandle(process); return new ProcessAccessProbe(true, null); } public static IReadOnlyList Scan(int processId, CancellationToken cancellationToken) { var process = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION | PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ, false, (uint)processId); if (process.IsNull) { throw new WxAgentException(WxAgentErrorCode.ProcessAccessDenied, $"Cannot read Weixin process {processId}.", new Win32Exception(Marshal.GetLastWin32Error())); } try { var found = new List(ScanLegacyHex(process, cancellationToken)); var seen = new HashSet(found, WcdbKeyCandidateComparer.Instance); foreach (var candidate in ScanConfigCipher(process, cancellationToken)) { if (seen.Add(candidate)) found.Add(candidate); } return found; } finally { PInvoke.CloseHandle(process); } } private static IReadOnlyList ScanLegacyHex(HANDLE process, CancellationToken cancellationToken) { WcdbKeyCandidateScanner[] scanners = [new(), new(utf16: true)]; var found = new List(); ulong? contiguousEnd = null; ulong address = 0; while (address < MaximumUserAddress) { cancellationToken.ThrowIfCancellationRequested(); MEMORY_BASIC_INFORMATION region; if (PInvoke.VirtualQueryEx(process, (void*)address, ®ion, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) { break; } var baseAddress = (ulong)region.BaseAddress; var regionSize = (ulong)region.RegionSize; if (regionSize == 0 || baseAddress > ulong.MaxValue - regionSize) { break; } var next = baseAddress + regionSize; if (next <= address) { break; } if (!IsReadable(region) || regionSize > MaximumRegionSize) { BreakRun(scanners, found); contiguousEnd = null; } else { if (contiguousEnd != baseAddress) { BreakRun(scanners, found); } contiguousEnd = ReadRegion(process, baseAddress, regionSize, scanners, found, cancellationToken) ? next : null; } address = next; } BreakRun(scanners, found); return found; } private static IReadOnlyList ScanConfigCipher(HANDLE process, CancellationToken cancellationToken) { var typeName = Encoding.ASCII.GetBytes(WcdbConfigCipher.TypeName); var needles = new HashSet(); WalkRegions(process, cancellationToken, (buffer, length, address) => { using var scanner = new MemoryPatternScanner(typeName); foreach (var match in scanner.Feed(buffer.AsSpan(0, length), address)) needles.Add(match); }); if (needles.Count == 0) return []; var nodeBases = new HashSet(); foreach (var needle in needles) { cancellationToken.ThrowIfCancellationRequested(); var pair = new byte[16]; BinaryPrimitives.WriteUInt64LittleEndian(pair.AsSpan(0, 8), needle); BinaryPrimitives.WriteUInt64LittleEndian(pair.AsSpan(8, 8), (ulong)typeName.Length); using var pairScanner = new MemoryPatternScanner(pair); WalkRegions(process, cancellationToken, (buffer, length, address) => { foreach (var match in pairScanner.Feed(buffer.AsSpan(0, length), address)) nodeBases.Add(match - 0x10); }); } var found = new List(); foreach (var nodeBase in nodeBases) { cancellationToken.ThrowIfCancellationRequested(); var node = ReadBytes(process, nodeBase, 0x50); if (node is null) continue; try { var stringPointer = ReadUInt64(node, 0x10); var stringLength = ReadUInt64(node, 0x18); if (!needles.Contains(stringPointer) || stringLength != (ulong)typeName.Length) continue; var configPointer = ReadUInt64(node, 0x28); if (configPointer is < 0x10000 or > MaximumUserAddress) continue; var obj = ReadBytes(process, configPointer + 0x88, 0x28); if (obj is null) continue; var dataPointer = ReadUInt64(obj, 0x8); var dataLength = ReadUInt64(obj, 0x10); if (dataLength == 0 || dataLength > WcdbConfigCipher.MaximumBlobLength || dataPointer is < 0x10000 or > MaximumUserAddress) continue; var blob = ReadBytes(process, dataPointer, (int)dataLength); if (blob is null || blob.Length != (int)dataLength) continue; foreach (var candidate in WcdbConfigCipher.Decode(blob)) found.Add(candidate); } finally { CryptographicOperations.ZeroMemory(node); } } return found; } private static void WalkRegions(HANDLE process, CancellationToken cancellationToken, Action visit) { ulong address = 0; while (address < MaximumUserAddress) { cancellationToken.ThrowIfCancellationRequested(); MEMORY_BASIC_INFORMATION region; if (PInvoke.VirtualQueryEx(process, (void*)address, ®ion, (nuint)sizeof(MEMORY_BASIC_INFORMATION)) == 0) { break; } var baseAddress = (ulong)region.BaseAddress; var regionSize = (ulong)region.RegionSize; if (regionSize == 0 || baseAddress > ulong.MaxValue - regionSize) { break; } var next = baseAddress + regionSize; if (next <= address) { break; } if (IsReadable(region) && regionSize <= MaximumRegionSize) { var offset = 0UL; while (offset < regionSize) { cancellationToken.ThrowIfCancellationRequested(); var count = (int)Math.Min((ulong)ChunkSize, regionSize - offset); var buffer = GC.AllocateUninitializedArray(count); nuint bytesRead = 0; var ok = false; fixed (byte* destination = buffer) { ok = PInvoke.ReadProcessMemory(process, (void*)(baseAddress + offset), destination, (nuint)count, &bytesRead); } try { if (ok && bytesRead == (nuint)count) visit(buffer, count, baseAddress + offset); } finally { CryptographicOperations.ZeroMemory(buffer); } if (!ok || bytesRead != (nuint)count) break; offset += (ulong)count; } } address = next; } } private static byte[]? ReadBytes(HANDLE process, ulong address, int length) { if (length <= 0 || address > MaximumUserAddress - (ulong)length) return null; var buffer = GC.AllocateUninitializedArray(length); nuint bytesRead = 0; var ok = false; fixed (byte* destination = buffer) { ok = PInvoke.ReadProcessMemory(process, (void*)address, destination, (nuint)length, &bytesRead); } if (!ok || bytesRead != (nuint)length) { CryptographicOperations.ZeroMemory(buffer); return null; } return buffer; } private static ulong ReadUInt64(ReadOnlySpan buffer, int offset) => offset + 8 <= buffer.Length ? BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(offset, 8)) : 0; private static bool ReadRegion( HANDLE process, ulong baseAddress, ulong regionSize, IReadOnlyList scanners, List found, CancellationToken cancellationToken) { var tailIsContiguous = false; var offset = 0UL; while (offset < regionSize) { cancellationToken.ThrowIfCancellationRequested(); var count = (int)Math.Min((ulong)ChunkSize, regionSize - offset); var buffer = GC.AllocateUninitializedArray(count); nuint bytesRead = 0; var success = false; fixed (byte* destination = buffer) { success = PInvoke.ReadProcessMemory(process, (void*)(baseAddress + offset), destination, (nuint)count, &bytesRead); } try { if (bytesRead > 0) { var length = checked((int)Math.Min(bytesRead, (nuint)count)); foreach (var scanner in scanners) found.AddRange(scanner.Feed(buffer.AsSpan(0, length))); } } finally { CryptographicOperations.ZeroMemory(buffer); } if (!success || bytesRead != (nuint)count) { BreakRun(scanners, found); tailIsContiguous = false; } else { tailIsContiguous = true; } offset += (ulong)count; } return tailIsContiguous; } private static void BreakRun(IEnumerable scanners, List found) { foreach (var scanner in scanners) found.AddRange(scanner.Feed([], finalBlock: true)); } private static bool IsReadable(MEMORY_BASIC_INFORMATION region) { const uint memCommit = 0x1000; const uint pageNoAccess = 0x01; const uint pageGuard = 0x100; const uint readableMask = 0x02 | 0x04 | 0x08 | 0x20 | 0x40 | 0x80; var state = (uint)region.State; var protection = (uint)region.Protect; return state == memCommit && (protection & (pageNoAccess | pageGuard)) == 0 && (protection & readableMask) != 0; } private sealed class WcdbKeyCandidateComparer : IEqualityComparer { public static readonly WcdbKeyCandidateComparer Instance = new(); public bool Equals(WcdbKeyCandidate? x, WcdbKeyCandidate? y) => x is not null && y is not null && string.Equals(x.EncKey, y.EncKey, StringComparison.Ordinal); public int GetHashCode(WcdbKeyCandidate obj) => StringComparer.Ordinal.GetHashCode(obj.EncKey); } }