From 4977ae71de301d98f4c030e55acc15bcbe01d2f6 Mon Sep 17 00:00:00 2001 From: Gregory Lirent Date: Mon, 30 Jun 2025 18:15:07 +0300 Subject: [PATCH] Initial commit --- Configuration/AppSettings.cs | 19 + Exceptions/ProcessAccessDeniedException.cs | 8 + Models/MemoryRegionInfo.cs | 11 + Models/ProcessInfo.cs | 17 + Models/ProcessModuleInfo.cs | 25 ++ Program.cs | 59 +++ Properties/launchSettings.json | 23 ++ Services/ProcessMonitor.cs | 460 +++++++++++++++++++++ Services/WinApi.cs | 118 ++++++ app.manifest | 11 + appsettings.Development.json | 8 + appsettings.json | 16 + webmr-api.csproj | 28 ++ webmr-api.csproj.user | 9 + webmr-api.sln | 25 ++ 15 files changed, 837 insertions(+) create mode 100644 Configuration/AppSettings.cs create mode 100644 Exceptions/ProcessAccessDeniedException.cs create mode 100644 Models/MemoryRegionInfo.cs create mode 100644 Models/ProcessInfo.cs create mode 100644 Models/ProcessModuleInfo.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 Services/ProcessMonitor.cs create mode 100644 Services/WinApi.cs create mode 100644 app.manifest create mode 100644 appsettings.Development.json create mode 100644 appsettings.json create mode 100644 webmr-api.csproj create mode 100644 webmr-api.csproj.user create mode 100644 webmr-api.sln diff --git a/Configuration/AppSettings.cs b/Configuration/AppSettings.cs new file mode 100644 index 0000000..d0ae6db --- /dev/null +++ b/Configuration/AppSettings.cs @@ -0,0 +1,19 @@ +namespace WebmrAPI.Configuration +{ + public class AppSettings + { + public AgentSettings Agent { get; set; } = new AgentSettings(); + public WebServerSettings WebServer { get; set; } = new WebServerSettings(); + } + + public class AgentSettings + { + public int ScanIntervalSeconds { get; set; } = 5; // Default scan every 5 seconds + public string TargetProcessName { get; set; } = "example.exe"; // Default target process + } + + public class WebServerSettings + { + public string Url { get; set; } = "http://0.0.0.0:8080"; // Default listening URL + } +} \ No newline at end of file diff --git a/Exceptions/ProcessAccessDeniedException.cs b/Exceptions/ProcessAccessDeniedException.cs new file mode 100644 index 0000000..9e1f4f7 --- /dev/null +++ b/Exceptions/ProcessAccessDeniedException.cs @@ -0,0 +1,8 @@ +namespace WebmrAPI.Exceptions +{ + public class ProcessAccessDeniedException : Exception + { + public ProcessAccessDeniedException(string message) : base(message) { } + public ProcessAccessDeniedException(string message, Exception innerException) : base(message, innerException) { } + } +} \ No newline at end of file diff --git a/Models/MemoryRegionInfo.cs b/Models/MemoryRegionInfo.cs new file mode 100644 index 0000000..d534c73 --- /dev/null +++ b/Models/MemoryRegionInfo.cs @@ -0,0 +1,11 @@ +namespace WebmrAPI.Models +{ + public class MemoryRegionInfo + { + public string BaseAddress { get; set; } = string.Empty; + public long RegionSize { get; set; } + public string State { get; set; } = string.Empty; + public string Protect { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/Models/ProcessInfo.cs b/Models/ProcessInfo.cs new file mode 100644 index 0000000..fd29546 --- /dev/null +++ b/Models/ProcessInfo.cs @@ -0,0 +1,17 @@ +using System.Collections.Concurrent; + +namespace WebmrAPI.Models +{ + public class ProcessInfo + { + private readonly object _lock = new object(); + + public int ProcessId { get; set; } + public string ProcessName { get; set; } = string.Empty; + public string? BaseAddress { get; set; } + public long VirtualMemorySize { get; set; } + public string? CommandLine { get; set; } + + + } +} \ No newline at end of file diff --git a/Models/ProcessModuleInfo.cs b/Models/ProcessModuleInfo.cs new file mode 100644 index 0000000..208a91a --- /dev/null +++ b/Models/ProcessModuleInfo.cs @@ -0,0 +1,25 @@ +п»їusing System.Diagnostics; + +namespace WebmrAPI.Services +{ + public class ProcessModuleInfo + { + public string? ModuleName { get; set; } + public string? FileName { get; set; } + public string? BaseAddress { get; set; } + public long MemorySize { get; set; } + public string? EntrypointAddress { get; set; } + + public static ProcessModuleInfo FromProcessModule(ProcessModule module) + { + return new ProcessModuleInfo + { + ModuleName = module.ModuleName, + FileName = module.FileName, + BaseAddress = "0x" + module.BaseAddress.ToInt64().ToString("X"), + MemorySize = module.ModuleMemorySize, + EntrypointAddress = "0x" + module.EntryPointAddress.ToInt64().ToString("X") + }; + } + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..eb6f08c --- /dev/null +++ b/Program.cs @@ -0,0 +1,59 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using System.Diagnostics; +using System.Text.Json; +using WebmrAPI.Configuration; +using WebmrAPI.Models; // Добавляем using для моделей +using WebmrAPI.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Загрузка конфигурации +builder.Services.Configure(builder.Configuration); + +// Добавляем логирование +builder.Services.AddLogging(config => +{ + config.AddConsole(); + config.AddDebug(); +}); + +// Регистрация ProcessMonitor как Singleton и как IHostedService +builder.Services.AddSingleton(); +builder.Services.AddHostedService(sp => sp.GetRequiredService()); + +// Регистрация WinApi как Singleton (или можно сделать его статическим, как сейчас) +// Если WinApi станет нестатическим и будет иметь зависимости, регистрируйте его здесь: +// builder.Services.AddSingleton(); // Если это класс, а не статический + +var app = builder.Build(); + +// Привязываем URL из настроек +var appSettings = app.Services.GetRequiredService>().Value; +app.Urls.Add(appSettings.WebServer.Url); + + +// GET /api/processes +app.MapGet("/api/processes", (ProcessMonitor monitor, [FromQuery] bool pretty = false) => +{ + return Results.Content(JsonSerializer.Serialize(monitor.Processes, new JsonSerializerOptions { WriteIndented = pretty }), "application/json"); +}); + +// GET /api/processes/{pid}/memory_regions +app.MapGet("/api/processes/{pid}/memory_regions", (int pid, ProcessMonitor monitor, [FromQuery] bool pretty = false) => +{ + var regions = monitor.GetBufferedMemoryRegions(pid); + if (regions == null) { + return Results.NotFound($"Process with PID {pid} or its memory regions not found."); + } + return Results.Content(JsonSerializer.Serialize(regions, new JsonSerializerOptions { WriteIndented = pretty }), "application/json"); +}); + +// GET /api/status/last_modified +app.MapGet("/api/status/last_modified", (ProcessMonitor monitor, [FromQuery] bool pretty = false) => +{ + var date = new { LastModified = monitor.LastModifiedTimestamp.ToString("o") }; + return Results.Content(JsonSerializer.Serialize(date, new JsonSerializerOptions { WriteIndented = pretty }), "application/json"); +}); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..f7e2ab6 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,23 @@ +п»ї{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5171", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7170;http://localhost:5171", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Services/ProcessMonitor.cs b/Services/ProcessMonitor.cs new file mode 100644 index 0000000..3cd0032 --- /dev/null +++ b/Services/ProcessMonitor.cs @@ -0,0 +1,460 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using System.Management; +using System.Runtime.InteropServices; +using WebmrAPI.Configuration; +using WebmrAPI.Exceptions; +using WebmrAPI.Models; + +namespace WebmrAPI.Services +{ + public class ProcessMonitor : IHostedService, IDisposable + { + private DateTime _modifyTimestamp = DateTime.UtcNow; + + private Timer? _timer; + private readonly ILogger _logger; + private readonly AgentSettings _agentSettings; + + private readonly object _lock = new object(); + private ConcurrentDictionary _processesBuffer = new(); + private ConcurrentDictionary> _memoryRegionsBuffer = new(); + + + public DateTime LastModifiedTimestamp + { + get + { + lock (_lock) + { + return _modifyTimestamp; + } + } + + private set + { + _modifyTimestamp = value; + } + } + + public IEnumerable Processes + { + get + { + lock (_lock) + { + return _processesBuffer.Values.ToList(); + } + } + } + + public ProcessMonitor(ILogger logger, IOptions appSettings) + { + _logger = logger; + _agentSettings = appSettings.Value.Agent; + } + + + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("ProcessMonitor started. Scan interval: {ScanIntervalSeconds} seconds.", _agentSettings.ScanIntervalSeconds); + // Запускаем первый скан сразу, затем по таймеру + _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_agentSettings.ScanIntervalSeconds)); + return Task.CompletedTask; + } + + private void DoWork(object? state) + { + _logger.LogDebug("Initiating process scan..."); + try + { + var currentProcesses = new Dictionary(); + var currentMemoryRegions = new Dictionary>(); + bool changedDetected = false; + + // --- 1. Get all running processes and their command lines using WMI --- + // WMI is more reliable for getting command line of arbitrary processes + var searcher = new ManagementObjectSearcher("SELECT ProcessId, Name, CommandLine FROM Win32_Process"); + Dictionary wmiCommandLines = new Dictionary(); + Dictionary wmiProcessNames = new Dictionary(); + + foreach (ManagementObject queryObj in searcher.Get()) + { + int pid = Convert.ToInt32(queryObj["ProcessId"]); + string? commandLine = queryObj["CommandLine"]?.ToString(); + string? processName = queryObj["Name"]?.ToString(); + + wmiCommandLines[pid] = commandLine ?? string.Empty; + wmiProcessNames[pid] = processName ?? string.Empty; + } + + // --- 2. Iterate through System.Diagnostics.Process objects --- + System.Diagnostics.Process[] allProcesses = System.Diagnostics.Process.GetProcesses(); + + foreach (var process in allProcesses) + { + // Исключаем системные процессы, которые не имеют обычных модулей/памяти + if (process.Id == 0 || process.Id == 4) + { + _logger.LogDebug("Skipping special system process {ProcessName} (PID: {PID}).", process.ProcessName, process.Id); + process.Dispose(); // Всегда освобождаем Process объект + continue; + } + + try + { + string processName = wmiProcessNames.GetValueOrDefault(process.Id, process.ProcessName); // Prefer WMI name if available + string commandLine = wmiCommandLines.GetValueOrDefault(process.Id, string.Empty); + + var processInfo = new ProcessInfo + { + ProcessId = process.Id, + ProcessName = processName, + VirtualMemorySize = process.VirtualMemorySize64, + CommandLine = commandLine + }; + + // Try to get BaseAddress from MainModule. This can still throw Access Denied. + try + { + // Some processes (e.g., system processes like Idle, System) might not have a MainModule + if (process.MainModule != null) + { + processInfo.BaseAddress = $"0x{process.MainModule.BaseAddress.ToInt64():X}"; + } + else + { + _logger.LogDebug("Process {ProcessName} (PID: {PID}) has no MainModule.", processName, process.Id); + processInfo.BaseAddress = null; // Ensure it's explicitly null if not found + } + } + catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 5) // Access Denied + { + // Ошибка 5: Access Denied. Ошибка 0x80004005 (которая также может быть Win32Exception с общим кодом) + if (ex.NativeErrorCode == 5) + { + _logger.LogWarning("Access denied to MainModule for process {ProcessName} (PID: {PID}). BaseAddress will be null. Error: {ErrorMessage}", processName, process.Id, ex.Message); + } + else + { + // Для других Win32Exception, таких как "Unable to enumerate the process modules." (0x80004005) + _logger.LogWarning(ex, "Win32Exception getting MainModule for process {ProcessName} (PID: {PID}). BaseAddress will be null.", processName, process.Id); + } + processInfo.BaseAddress = null; + } + catch (InvalidOperationException ex) + { + _logger.LogDebug(ex, "Could not get MainModule for process {ProcessName} (PID: {PID}). Process might have exited or has no MainModule.", processName, process.Id); + processInfo.BaseAddress = null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error getting MainModule for process {ProcessName} (PID: {PID}).", processName, process.Id); + processInfo.BaseAddress = null; + } + + + currentProcesses.Add(process.Id, processInfo); + + // --- 3. Scan memory regions for the target process OR new processes --- + bool isTargetProcess = _agentSettings.TargetProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase); + + // We scan memory regions for: + // 1. The specific target process (if _agentSettings.TargetProcessName is set) + // 2. ALL processes if _agentSettings.TargetProcessName is empty/null + // 3. Any newly detected process that wasn't in our previous buffer (so we get its initial memory layout) + if (isTargetProcess || string.IsNullOrEmpty(_agentSettings.TargetProcessName) || !_processesBuffer.ContainsKey(process.Id)) + { + try + { + var regions = ScanMemoryRegionsForProcess(process); + if (regions.Any()) + { + currentMemoryRegions.Add(process.Id, regions); + } + else + { + _logger.LogDebug("No memory regions found or accessible for process {ProcessName} (PID: {PID}).", processName, process.Id); + } + } + catch (ProcessAccessDeniedException ex) + { + // Определяем список системных процессов, которые обычно отказывают в доступе + string[] commonSystemProcesses = { + "System", "System Idle Process", "Registry", "smss.exe", "csrss.exe", + "wininit.exe", "services.exe", "lsass.exe", "winlogon.exe", "fontdrvhost.exe", + "Memory Compression", "dwm.exe", "RuntimeBroker.exe", // Common UWP/System processes + "msrdc.exe", "svchost.exe", // svchost can be varied, but often restricted + "MsMpEng.exe", "MpDefenderCoreService.exe" // Windows Defender related + }; + + // Проверяем, является ли процесс одним из обычно защищенных + if (commonSystemProcesses.Contains(processName, StringComparer.OrdinalIgnoreCase)) + { + _logger.LogDebug(ex, "Access denied to memory regions for known system process {ProcessName} (PID: {PID}). This is expected.", processName, process.Id); + } + else + { + _logger.LogWarning(ex, "Access denied to memory regions for process {ProcessName} (PID: {PID}). Skipping memory scan for it. Elevated privileges needed?", processName, process.Id); + } + } + // Other exceptions from ScanMemoryRegionsForProcess will be caught by outer catch + } + } + catch (InvalidOperationException ex) + { + // Process might have exited between GetProcesses and accessing its properties + _logger.LogDebug(ex, "Process {ProcessName} (PID: {PID}) exited during scan. Skipping.", process.ProcessName, process.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error scanning process {ProcessName} (PID: {PID}) for basic info or WMI data.", process.ProcessName, process.Id); + } + finally + { + // Dispose the Process object to free resources associated with the handle + process.Dispose(); + } + } + + // --- 4. Compare with buffered data and update if changes detected --- + lock (_lock) + { + // Check for changes in processes (new, exited, basic info changed) + if (!AreProcessInfosEqual(_processesBuffer.Values, currentProcesses.Values)) + { + changedDetected = true; + _logger.LogInformation("Process list or basic info changed."); + } + + // Check for changes in memory regions for monitored processes + // Only compare if currentMemoryRegions contains the PID + foreach (var kvp in currentMemoryRegions) + { + // If process was previously monitored AND its regions are different OR if it's a new process with regions + if (!_memoryRegionsBuffer.ContainsKey(kvp.Key) || !AreMemoryRegionsEqual(_memoryRegionsBuffer[kvp.Key], kvp.Value)) + { + changedDetected = true; + _logger.LogInformation("Memory regions for PID {PID} changed.", kvp.Key); + break; // No need to check further, change detected + } + } + + // If a process disappeared (was in old buffer but not in new), it also counts as a change + if (_processesBuffer.Count != currentProcesses.Count) + { + // If counts are different, and we haven't already marked as changed by content, mark as changed + // This handles processes exiting. + if (!changedDetected) + { // Only set if not already set by content change + changedDetected = true; + _logger.LogInformation("Process count changed (some processes might have exited)."); + } + } + + if (changedDetected) + { + _processesBuffer = new ConcurrentDictionary(currentProcesses); + _memoryRegionsBuffer = new ConcurrentDictionary>(currentMemoryRegions); + LastModifiedTimestamp = DateTime.UtcNow; + _logger.LogInformation("Buffered data updated. New LastModifiedTimestamp: {Timestamp}", LastModifiedTimestamp); + } + else + { + _logger.LogDebug("No changes detected in processes or monitored memory regions."); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error during process monitoring cycle."); + } + } + + /// + /// Сканирует регионы памяти для конкретного процесса. + /// + private List ScanMemoryRegionsForProcess(System.Diagnostics.Process process) + { + var regions = new List(); + IntPtr processHandle = IntPtr.Zero; + try + { + // Open process with necessary access rights + processHandle = WinApi.OpenProcess( + WinApi.PROCESS_QUERY_INFORMATION | WinApi.PROCESS_VM_OPERATION | WinApi.PROCESS_VM_READ, // We also need VM_READ for VirtualQueryEx if it needs to access pages + false, // Do not inherit handle + (uint)process.Id + ); + + if (processHandle == IntPtr.Zero) + { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode == 5) // ERROR_ACCESS_DENIED + { + throw new ProcessAccessDeniedException($"Access denied to process {process.ProcessName} (PID: {process.Id}). Error code: {errorCode}"); + } + WinApi.ThrowWinApiErrorIfAny($"OpenProcess for PID {process.Id}"); + return regions; // Should not reach here if ThrowWinApiErrorIfAny throws + } + + IntPtr currentAddress = IntPtr.Zero; + long maxAddress = long.MaxValue; // Scan full 64-bit address space + + if (Environment.Is64BitProcess) + { + // On 64-bit, the user-mode address space typically ends at 0x7FFFFFFFFFFF + // No, maxAddress should be dynamic if needed, but for iteration it means "until VirtualQueryEx returns 0" + // Or 0x7FFFFFFFFFFF (2^47 -1, which is the user-mode address space limit on 64-bit Windows) + // For now, let VirtualQueryEx drive it. + } + + while (true) + { + if (currentAddress.ToInt64() >= (long)0x7FFFFFFFFFFF) + { + _logger.LogDebug("Reached effective end of 64-bit user-mode address space (0x{Address:X}) for PID {PID}.", currentAddress.ToInt64(), process.Id); + break; + } + + WinApi.MEMORY_BASIC_INFORMATION mbi; + // dwLength is the size of the buffer we pass in + int result = WinApi.VirtualQueryEx(processHandle, currentAddress, out mbi, (uint)Marshal.SizeOf(typeof(WinApi.MEMORY_BASIC_INFORMATION))); + + if (result == 0) // VirtualQueryEx returns 0 on failure or end of address space + { + int errorCode = Marshal.GetLastWin32Error(); + // ERROR_NO_MORE_FILES (18) - ожидаемо в конце адресного пространства + // ERROR_INVALID_PARAMETER (87) - часто возникает на высоких адресах или невалидных + if (errorCode != 0 && errorCode != 18 && errorCode != 87) // Добавляем 87 как ожидаемое завершение + { + _logger.LogWarning("VirtualQueryEx for PID {PID} at address 0x{Address:X} failed with unexpected error code: {ErrorCode}.", process.Id, currentAddress.ToInt64(), errorCode); + } + else if (errorCode == 87) + { + _logger.LogDebug("VirtualQueryEx for PID {PID} at address 0x{Address:X} failed with ERROR_INVALID_PARAMETER (87). End of scannable address space reached.", process.Id, currentAddress.ToInt64()); + } + break; // End of address space or an unrecoverable error + } + + // Only add committed or reserved regions + if (mbi.State == WinApi.MEM_COMMIT || mbi.State == WinApi.MEM_RESERVE) + { + regions.Add(new MemoryRegionInfo + { + BaseAddress = $"0x{mbi.BaseAddress.ToInt64():X}", + RegionSize = (long)mbi.RegionSize.ToUInt64(), + State = WinApi.GetMemoryStateString(mbi.State), + Protect = WinApi.GetMemoryProtectString(mbi.Protect), + Type = WinApi.GetMemoryTypeString(mbi.Type) + }); + } + + // Calculate next address to query from (BaseAddress + RegionSize) + // Ensure we don't overflow or loop indefinitely on bad data + long nextAddressLong = mbi.BaseAddress.ToInt64() + (long)mbi.RegionSize.ToUInt64(); + if (nextAddressLong <= currentAddress.ToInt64()) // Handle potential infinite loop if RegionSize is 0 or negative + { + _logger.LogWarning("VirtualQueryEx for PID {PID} advanced by non-positive amount. Breaking loop.", process.Id); + break; + } + currentAddress = new IntPtr(nextAddressLong); + + // Stop if we hit max address or overflow (which shouldn't happen with long.MaxValue for 64-bit) + if (nextAddressLong >= maxAddress && maxAddress != long.MaxValue) break; // In case we ever set a specific maxAddress + } + } + finally + { + if (processHandle != IntPtr.Zero) + { + WinApi.CloseHandle(processHandle); + } + } + return regions; + } + + /// + /// Сравнивает два списка ProcessInfo для выявления изменений. + /// + private bool AreProcessInfosEqual(ICollection oldList, ICollection newList) + { + if (oldList.Count != newList.Count) return false; + + var oldDict = oldList.ToDictionary(p => p.ProcessId); + var newDict = newList.ToDictionary(p => p.ProcessId); + + foreach (var newProc in newDict.Values) + { + if (!oldDict.TryGetValue(newProc.ProcessId, out var oldProc)) return false; // New process or process ID changed + + // Compare relevant fields. BaseAddress and CommandLine might change if process restarts or for some apps. + // We consider them part of "basic info" that signals a change. + if (oldProc.ProcessName != newProc.ProcessName || + oldProc.BaseAddress != newProc.BaseAddress || + oldProc.VirtualMemorySize != newProc.VirtualMemorySize || + oldProc.CommandLine != newProc.CommandLine) + { + return false; + } + } + return true; + } + + /// + /// Сравнивает два списка MemoryRegionInfo для выявления изменений. + /// + private bool AreMemoryRegionsEqual(List oldList, List newList) + { + if (oldList.Count != newList.Count) return false; + + // Sort by BaseAddress to ensure consistent comparison order + var oldSorted = oldList.OrderBy(r => r.BaseAddress).ToList(); + var newSorted = newList.OrderBy(r => r.BaseAddress).ToList(); + + for (int i = 0; i < oldSorted.Count; i++) + { + var oldRegion = oldSorted[i]; + var newRegion = newSorted[i]; + + if (oldRegion.BaseAddress != newRegion.BaseAddress || + oldRegion.RegionSize != newRegion.RegionSize || + oldRegion.State != newRegion.State || + oldRegion.Protect != newRegion.Protect || + oldRegion.Type != newRegion.Type) + { + return false; + } + } + return true; + } + + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("ProcessMonitor is stopping."); + _timer?.Change(Timeout.Infinite, 0); + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + + public List? GetBufferedMemoryRegions(int pid) + { + lock (_lock) // Ensure thread-safe access + { + if (_memoryRegionsBuffer.TryGetValue(pid, out var regions)) + { + return new List(regions); // Return a copy + } + return null; + } + } + } +} \ No newline at end of file diff --git a/Services/WinApi.cs b/Services/WinApi.cs new file mode 100644 index 0000000..1318e68 --- /dev/null +++ b/Services/WinApi.cs @@ -0,0 +1,118 @@ +using System.Runtime.InteropServices; +using WebmrAPI.Exceptions; + +namespace WebmrAPI.Services +{ + public static class WinApi + { + // --- Константы и флаги --- + + // Флаги доступа к процессу для OpenProcess + public const uint PROCESS_QUERY_INFORMATION = 0x0400; // Required to retrieve certain information about a process, such as its token, exit code, and priority class. + public const uint PROCESS_VM_READ = 0x0010; // Required to read memory in a process. + public const uint PROCESS_VM_OPERATION = 0x0008; // Required to perform an operation on the address space of a process (e.g., VirtualQueryEx). + + // Состояния памяти + public const uint MEM_COMMIT = 0x1000; // Allocated and committed + public const uint MEM_FREE = 0x10000; // Free (not reserved or committed) + public const uint MEM_RESERVE = 0x2000; // Reserved + + // Типы памяти + public const uint MEM_IMAGE = 0x1000000; // Mapped into the view of an image section + public const uint MEM_MAPPED = 0x40000; // Mapped into the view of a data section + public const uint MEM_PRIVATE = 0x20000; // Private (non-shared) + + // --- Структуры данных --- + + // https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information + [StructLayout(LayoutKind.Sequential)] + public struct MEMORY_BASIC_INFORMATION + { + public IntPtr BaseAddress; + public IntPtr AllocationBase; + public uint AllocationProtect; + public ushort PartitionId; // Added in Windows 10, version 1709 + public UIntPtr RegionSize; // Use UIntPtr for size, as it's pointer-sized + public uint State; + public uint Protect; + public uint Type; + } + + // --- P/Invoke Объявления Функций --- + + // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr OpenProcess( + uint dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, + uint dwProcessId + ); + + // https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); + + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-virtualqueryex + [DllImport("kernel32.dll", SetLastError = true)] + public static extern int VirtualQueryEx( + IntPtr hProcess, + IntPtr lpAddress, + out MEMORY_BASIC_INFORMATION lpBuffer, + uint dwLength + ); + + // --- Вспомогательные Методы --- + + /// + /// Получает человекочитаемое строковое представление для флага защиты памяти. + /// + public static string GetMemoryProtectString(uint protect) + { + // Здесь можно реализовать более детальное отображение флагов защиты. + // Для простоты, пока просто возвращаем шестнадцатеричное значение. + // В дальнейшем можно расширить для PAGE_READONLY, PAGE_READWRITE и т.д. + return $"0x{protect:X}"; + } + + /// + /// Получает человекочитаемое строковое представление для состояния памяти. + /// + public static string GetMemoryStateString(uint state) + { + switch (state) + { + case MEM_COMMIT: return "MEM_COMMIT"; + case MEM_FREE: return "MEM_FREE"; + case MEM_RESERVE: return "MEM_RESERVE"; + default: return $"Unknown (0x{state:X})"; + } + } + + /// + /// Получает человекочитаемое строковое представление для типа памяти. + /// + public static string GetMemoryTypeString(uint type) + { + switch (type) + { + case MEM_IMAGE: return "MEM_IMAGE"; + case MEM_MAPPED: return "MEM_MAPPED"; + case MEM_PRIVATE: return "MEM_PRIVATE"; + default: return $"Unknown (0x{type:X})"; + } + } + + /// + /// Проверяет последнюю ошибку WinAPI и выбрасывает исключение, если ошибка есть. + /// + public static void ThrowWinApiErrorIfAny(string methodName) + { + int errorCode = Marshal.GetLastWin32Error(); + if (errorCode != 0) + { + throw new ExternalException($"WinAPI call {methodName} failed with error code {errorCode}."); + } + } + } +} \ No newline at end of file diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..f2268cf --- /dev/null +++ b/app.manifest @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..4c5ca72 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,16 @@ +{ + "Agent": { + "ScanIntervalSeconds": 5, + "TargetProcessName": "" + }, + "WebServer": { + "Url": "http://0.0.0.0:8080" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "ProcessMemoryAgent.Services.ProcessMonitor": "Debug" + } + } +} \ No newline at end of file diff --git a/webmr-api.csproj b/webmr-api.csproj new file mode 100644 index 0000000..d59a65f --- /dev/null +++ b/webmr-api.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + true + true + true + WebmrAPI + ProcessMonitoringService + app.manifest + + 1.0.0.0 + 1.0.0.0 + 0.1.0 + OpenSource + Process Monitoring Agent + A service for detailed monitoring processes and memory regions. + Copyright В© 2024-2025 Gregory Lirent + en-US + + + + + + + diff --git a/webmr-api.csproj.user b/webmr-api.csproj.user new file mode 100644 index 0000000..c404400 --- /dev/null +++ b/webmr-api.csproj.user @@ -0,0 +1,9 @@ +п»ї + + + https + + + ProjectDebugger + + \ No newline at end of file diff --git a/webmr-api.sln b/webmr-api.sln new file mode 100644 index 0000000..7f910df --- /dev/null +++ b/webmr-api.sln @@ -0,0 +1,25 @@ +п»ї +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36221.1 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "webmr-api", "webmr-api.csproj", "{B423E3DA-00BB-52EC-AFB3-EAC37DECB4FD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B423E3DA-00BB-52EC-AFB3-EAC37DECB4FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B423E3DA-00BB-52EC-AFB3-EAC37DECB4FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B423E3DA-00BB-52EC-AFB3-EAC37DECB4FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B423E3DA-00BB-52EC-AFB3-EAC37DECB4FD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {FBA4F028-4190-4015-8BC5-0F0C354242D5} + EndGlobalSection +EndGlobal