Initial commit

This commit is contained in:
Gregory Lirent 2025-06-30 18:15:07 +03:00
commit 4977ae71de
15 changed files with 837 additions and 0 deletions

View File

@ -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
}
}

View File

@ -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) { }
}
}

View File

@ -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;
}
}

17
Models/ProcessInfo.cs Normal file
View File

@ -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; }
}
}

View File

@ -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")
};
}
}
}

59
Program.cs Normal file
View File

@ -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<AppSettings>(builder.Configuration);
// Äîáàâëÿåì ëîãèðîâàíèå
builder.Services.AddLogging(config =>
{
config.AddConsole();
config.AddDebug();
});
// Ðåãèñòðàöèÿ ProcessMonitor êàê Singleton è êàê IHostedService
builder.Services.AddSingleton<ProcessMonitor>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<ProcessMonitor>());
// Ðåãèñòðàöèÿ WinApi êàê Singleton (èëè ìîæíî ñäåëàòü åãî ñòàòè÷åñêèì, êàê ñåé÷àñ)
// Åñëè WinApi ñòàíåò íåñòàòè÷åñêèì è áóäåò èìåòü çàâèñèìîñòè, ðåãèñòðèðóéòå åãî çäåñü:
// builder.Services.AddSingleton<WinApi>(); // Åñëè ýòî êëàññ, à íå ñòàòè÷åñêèé
var app = builder.Build();
// Ïðèâÿçûâàåì URL èç íàñòðîåê
var appSettings = app.Services.GetRequiredService<IOptions<AppSettings>>().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();

View File

@ -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"
}
}
}
}

460
Services/ProcessMonitor.cs Normal file
View File

@ -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<ProcessMonitor> _logger;
private readonly AgentSettings _agentSettings;
private readonly object _lock = new object();
private ConcurrentDictionary<int, ProcessInfo> _processesBuffer = new();
private ConcurrentDictionary<int, List<MemoryRegionInfo>> _memoryRegionsBuffer = new();
public DateTime LastModifiedTimestamp
{
get
{
lock (_lock)
{
return _modifyTimestamp;
}
}
private set
{
_modifyTimestamp = value;
}
}
public IEnumerable<ProcessInfo> Processes
{
get
{
lock (_lock)
{
return _processesBuffer.Values.ToList();
}
}
}
public ProcessMonitor(ILogger<ProcessMonitor> logger, IOptions<AppSettings> 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<int, ProcessInfo>();
var currentMemoryRegions = new Dictionary<int, List<MemoryRegionInfo>>();
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<int, string> wmiCommandLines = new Dictionary<int, string>();
Dictionary<int, string> wmiProcessNames = new Dictionary<int, string>();
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<int, ProcessInfo>(currentProcesses);
_memoryRegionsBuffer = new ConcurrentDictionary<int, List<MemoryRegionInfo>>(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.");
}
}
/// <summary>
/// Ñêàíèðóåò ðåãèîíû ïàìÿòè äëÿ êîíêðåòíîãî ïðîöåññà.
/// </summary>
private List<MemoryRegionInfo> ScanMemoryRegionsForProcess(System.Diagnostics.Process process)
{
var regions = new List<MemoryRegionInfo>();
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;
}
/// <summary>
/// Ñðàâíèâàåò äâà ñïèñêà ProcessInfo äëÿ âûÿâëåíèÿ èçìåíåíèé.
/// </summary>
private bool AreProcessInfosEqual(ICollection<ProcessInfo> oldList, ICollection<ProcessInfo> 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;
}
/// <summary>
/// Ñðàâíèâàåò äâà ñïèñêà MemoryRegionInfo äëÿ âûÿâëåíèÿ èçìåíåíèé.
/// </summary>
private bool AreMemoryRegionsEqual(List<MemoryRegionInfo> oldList, List<MemoryRegionInfo> 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<MemoryRegionInfo>? GetBufferedMemoryRegions(int pid)
{
lock (_lock) // Ensure thread-safe access
{
if (_memoryRegionsBuffer.TryGetValue(pid, out var regions))
{
return new List<MemoryRegionInfo>(regions); // Return a copy
}
return null;
}
}
}
}

118
Services/WinApi.cs Normal file
View File

@ -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
);
// --- Âñïîìîãàòåëüíûå Ìåòîäû ---
/// <summary>
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ ôëàãà çàùèòû ïàìÿòè.
/// </summary>
public static string GetMemoryProtectString(uint protect)
{
// Çäåñü ìîæíî ðåàëèçîâàòü áîëåå äåòàëüíîå îòîáðàæåíèå ôëàãîâ çàùèòû.
// Äëÿ ïðîñòîòû, ïîêà ïðîñòî âîçâðàùàåì øåñòíàäöàòåðè÷íîå çíà÷åíèå.
// Â äàëüíåéøåì ìîæíî ðàñøèðèòü äëÿ PAGE_READONLY, PAGE_READWRITE è ò.ä.
return $"0x{protect:X}";
}
/// <summary>
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ ñîñòîÿíèÿ ïàìÿòè.
/// </summary>
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})";
}
}
/// <summary>
/// Ïîëó÷àåò ÷åëîâåêî÷èòàåìîå ñòðîêîâîå ïðåäñòàâëåíèå äëÿ òèïà ïàìÿòè.
/// </summary>
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})";
}
}
/// <summary>
/// Ïðîâåðÿåò ïîñëåäíþþ îøèáêó WinAPI è âûáðàñûâàåò èñêëþ÷åíèå, åñëè îøèáêà åñòü.
/// </summary>
public static void ThrowWinApiErrorIfAny(string methodName)
{
int errorCode = Marshal.GetLastWin32Error();
if (errorCode != 0)
{
throw new ExternalException($"WinAPI call {methodName} failed with error code {errorCode}.");
}
}
}
}

11
app.manifest Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="ProcessMonitoringService.app"/>
<trustInfo>
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

16
appsettings.json Normal file
View File

@ -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"
}
}
}

28
webmr-api.csproj Normal file
View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<UseAppHost>true</UseAppHost>
<EnableCETCompat>true</EnableCETCompat>
<RootNamespace>WebmrAPI</RootNamespace>
<AssemblyName>ProcessMonitoringService</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Version>0.1.0</Version>
<Company>OpenSource</Company>
<Product>Process Monitoring Agent</Product>
<Description>A service for detailed monitoring processes and memory regions.</Description>
<Copyright>Copyright © 2024-2025 Gregory Lirent</Copyright>
<NeutralLanguage>en-US</NeutralLanguage>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Management" Version="8.0.0" />
</ItemGroup>
</Project>

9
webmr-api.csproj.user Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
</Project>

25
webmr-api.sln Normal file
View File

@ -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