MemScan-EDR/src-csharp/MemScanEDR/Services/AIAnalyzer.cs

451 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using MemScanEDR.Models;
namespace MemScanEDR.Services;
/// <summary>
/// AI 分析器 v2.0 —— 重构版
/// 核心变化:
/// - 默认使用本地 AI 管线DetectionPipeline基线+分类+规则三级联动)
/// - LLM 大模型仅用于辅助研判(可选,需手动开启)
/// - 启发式规则引擎作为降级方案保留
/// </summary>
public class AIAnalyzer
{
private readonly HttpClient _http = new();
private AIConfig _config;
private readonly DetectionPipeline _pipeline;
private readonly List<Signature> _signatures;
public AIAnalyzer(AIConfig config, List<Signature> signatures, ScanMode scanMode = ScanMode.Balanced)
{
_config = config;
_signatures = signatures;
_http.Timeout = TimeSpan.FromSeconds(config.Timeout);
_pipeline = new DetectionPipeline(scanMode);
}
public void UpdateConfig(AIConfig config)
{
_config = config;
_http.Timeout = TimeSpan.FromSeconds(config.Timeout);
}
/// <summary>
/// 分析进程 —— v2.0 核心入口
/// </summary>
public async Task<ScanResult> AnalyzeProcessAsync(ProcessInfo proc,
List<SignatureMatch>? matches = null, List<MemoryRegion>? suspiciousRegions = null)
{
// v2.0: 优先使用本地 AI 管线
if (_config.EnableLocalAI)
{
try
{
var result = await _pipeline.AnalyzeAsync(proc, _signatures);
// 如果启用了 LLM 辅助研判且本地 AI 判定为可疑/高危
if (_config.EnableLLMAssist &&
result.RiskLevel != RiskLevel.Safe &&
_config.Provider != AIProvider.Heuristic)
{
var llmResult = await LlmAssistedAnalysis(proc, result);
if (llmResult != null)
{
// LLM 结果作为补充参考,不覆盖本地 AI 的核心判定
result.Reasoning += $" | LLM研判: {llmResult.Reasoning}";
result.AIProvider += " + LLM";
}
}
return result;
}
catch (Exception ex)
{
// 管线异常时回退
return HeuristicAnalyze(proc, matches ?? new(), suspiciousRegions ?? new(),
$"管线异常回退: {ex.Message}");
}
}
// 未启用本地AI时的传统模式
if (_config.Provider == AIProvider.Heuristic)
{
return HeuristicAnalyze(proc, matches ?? new(), suspiciousRegions ?? new());
}
// 传统 LLM 模式(兼容 v1.0
return await LegacyApiAnalyze(proc, matches ?? new(), suspiciousRegions ?? new());
}
/// <summary>
/// LLM 辅助研判 —— 仅对本地 AI 已标记为可疑的进程做二次确认
/// </summary>
private async Task<ScanResult?> LlmAssistedAnalysis(ProcessInfo proc, ScanResult pipelineResult)
{
try
{
var prompt = BuildLlmAssistPrompt(proc, pipelineResult);
var response = await CallAIAsync(prompt);
return ParseLlmResponse(proc, response);
}
catch
{
return null;
}
}
private static string BuildLlmAssistPrompt(ProcessInfo proc, ScanResult pipelineResult)
{
var sb = new StringBuilder();
sb.AppendLine("=== 本地AI已标记为可疑的进程 ===");
sb.AppendLine($"PID: {proc.Pid}, 进程名: {proc.Name}, 路径: {proc.ExePath}");
sb.AppendLine($"风险等级: {pipelineResult.RiskLevel}, 评分: {pipelineResult.RiskScore:F2}");
sb.AppendLine($"指标: {string.Join(", ", pipelineResult.Indicators)}");
sb.AppendLine($"检测层: {pipelineResult.DetectionLayer}");
sb.AppendLine($"本地AI推理: {pipelineResult.Reasoning}");
sb.AppendLine("\n=== 请结合以上信息做最终研判 ===");
sb.AppendLine("请判断此进程是否为真正的威胁,并给出简短理由。");
sb.AppendLine("注意:.NET/Java/Node.js/Chrome等JIT运行时可能有合法RWX内存安全软件可能有注入行为。");
return sb.ToString();
}
private ScanResult ParseLlmResponse(ProcessInfo proc, string content)
{
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = RiskLevel.Safe,
RiskScore = 0,
Reasoning = content.Trim().Length > 200 ? content.Trim()[..200] : content.Trim(),
AIProvider = $"llm-assist/{_config.Model}"
};
}
/// <summary>
/// 传统 LLM API 分析(兼容 v1.0 模式)
/// </summary>
private async Task<ScanResult> LegacyApiAnalyze(ProcessInfo proc,
List<SignatureMatch> matches, List<MemoryRegion> suspiciousRegions)
{
var preScore = QuickPreScore(proc, matches, suspiciousRegions);
if (preScore < 0.05)
{
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = RiskLevel.Safe,
RiskScore = 0.0,
Indicators = new List<string>(),
Reasoning = $"PID {proc.Pid} ({proc.Name}) | v2.0 预过滤跳过",
AIProvider = "heuristic/pre-filter-v2",
DetectionLayer = "PreFilter",
ScanTime = DateTime.Now
};
}
try
{
var prompt = BuildLegacyPrompt(proc, matches, suspiciousRegions);
var response = await CallAIAsync(prompt);
return ParseLegacyResponse(proc, response);
}
catch (Exception ex)
{
return HeuristicAnalyze(proc, matches, suspiciousRegions,
$"API异常回退: {ex.Message}");
}
}
private static double QuickPreScore(ProcessInfo proc, List<SignatureMatch> matches,
List<MemoryRegion> suspiciousRegions)
{
double s = 0;
if (!proc.IsSigned && proc.IsTempPath) s += 0.30;
if (FeatureExtractor.IsSystemProcessName(proc.Name) && !proc.IsSystemPath) s += 0.25;
var suspPorts = new HashSet<int> { 4444, 5555, 6666, 7777, 1337, 31337, 9999, 12345, 27015 };
if (proc.Connections.Any(c => suspPorts.Contains(c.RemotePort))) s += 0.20;
if (matches.Count > 0) s += 0.20;
if (suspiciousRegions.Any(r => r.Protect.Contains("EXECUTE_READWRITE"))) s += 0.15;
return Math.Min(s, 1.0);
}
// ─── 启发式规则(保留作为降级方案)───
public ScanResult HeuristicAnalyze(ProcessInfo proc, List<SignatureMatch> matches,
List<MemoryRegion> suspiciousRegions, string? errorNote = null)
{
double score = 0;
var indicators = new List<string>();
var reasons = new List<string>();
// Rule 1: unsigned + temp path
if (!proc.IsSigned && proc.IsTempPath)
{
score += 0.30;
indicators.Add("unsigned_temp_path");
reasons.Add("unsigned executable in temp directory");
}
// Rule 2: process masquerading
if (FeatureExtractor.IsSystemProcessName(proc.Name) && !proc.IsSystemPath)
{
score += 0.25;
indicators.Add("process_masquerading");
reasons.Add("system process name in non-system path");
}
// Rule 3: suspicious ports
var suspPorts = new HashSet<int> { 4444, 5555, 6666, 7777, 1337, 31337 };
foreach (var conn in proc.Connections)
{
if (suspPorts.Contains(conn.RemotePort))
{
score += 0.20;
indicators.Add("suspicious_port");
reasons.Add($"suspicious remote port {conn.RemotePort}");
break;
}
}
// Rule 4: signature matches
if (matches.Count > 0)
{
var high = matches.Count(m => m.Severity == "high");
var med = matches.Count(m => m.Severity == "medium");
score += Math.Min(high * 0.20, 0.4);
score += Math.Min(med * 0.08, 0.2);
if (high > 0) { indicators.Add("high_severity_match"); reasons.Add($"{high} high-severity signatures"); }
if (matches.Any(m => m.Category == "shellcode")) { score += 0.15; indicators.Add("shellcode_detected"); }
if (matches.Any(m => m.Category == "c2")) { score += 0.20; indicators.Add("c2_pattern"); }
}
// Rule 5: suspicious memory regions (v2.0 improved: distinguish JIT)
var rwxCount = suspiciousRegions.Count(r =>
r.Protect.Contains("EXECUTE_READWRITE") && r.Type == "PRIVATE");
// v2.0 improvement: don't flag JIT runtimes
var isRuntime = proc.Name.ToLowerInvariant() switch
{
var n when n.Contains("dotnet") || n.Contains("java") ||
n.Contains("node") || n.Contains("python") ||
n.Contains("chrome") || n.Contains("msedge") ||
n.Contains("firefox") => true,
_ => false
};
if (rwxCount > 0 && !isRuntime)
{
score += Math.Min(rwxCount * 0.12, 0.25);
indicators.Add("rwx_private_memory");
}
score = Math.Min(score, 1.0);
var level = score >= 0.75 ? RiskLevel.High : score >= 0.50 ? RiskLevel.Suspicious : RiskLevel.Safe;
var reasoning = $"PID {proc.Pid} ({proc.Name}) | {string.Join("; ", reasons)} | score={score:F2}";
if (errorNote != null)
reasoning += $" | {errorNote}";
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = level,
RiskScore = score,
Indicators = indicators,
Reasoning = reasoning,
AIProvider = "heuristic/rule-engine-v2",
DetectionLayer = "Heuristic",
ScanTime = DateTime.Now
};
}
// ─── LLM API 调用 ───
private async Task<string> CallAIAsync(string userMessage)
{
if (_config.Provider == AIProvider.Claude)
return await CallClaudeAsync(userMessage);
var url = _config.Provider == AIProvider.Local
? $"{_config.BaseUrl}/api/chat"
: $"{_config.BaseUrl.TrimEnd('/')}/chat/completions";
object requestBody = _config.Provider == AIProvider.Local
? new { model = _config.Model, messages = new[] { new { role = "system", content = V2SystemPrompt }, new { role = "user", content = userMessage } }, stream = false }
: new { model = _config.Model, messages = new[] { new { role = "system", content = V2SystemPrompt }, new { role = "user", content = userMessage } }, max_tokens = _config.MaxTokens, temperature = _config.Temperature };
var json = JsonSerializer.Serialize(requestBody);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
if (!string.IsNullOrEmpty(_config.ApiKey))
request.Headers.Add("Authorization", $"Bearer {_config.ApiKey}");
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseJson);
if (_config.Provider == AIProvider.Local)
return doc.RootElement.GetProperty("message").GetProperty("content").GetString() ?? "";
return doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString() ?? "";
}
private async Task<string> CallClaudeAsync(string userMessage)
{
var url = $"{_config.BaseUrl.TrimEnd('/')}/v1/messages";
var requestBody = new
{
model = _config.Model,
max_tokens = _config.MaxTokens,
system = V2SystemPrompt,
messages = new[] { new { role = "user", content = userMessage } }
};
var json = JsonSerializer.Serialize(requestBody);
var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
request.Headers.Add("x-api-key", _config.ApiKey);
request.Headers.Add("anthropic-version", "2023-06-01");
var response = await _http.SendAsync(request);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(responseJson);
return doc.RootElement.GetProperty("content")[0].GetProperty("text").GetString() ?? "";
}
private string BuildLegacyPrompt(ProcessInfo proc, List<SignatureMatch> matches,
List<MemoryRegion> suspiciousRegions)
{
var sb = new StringBuilder();
sb.AppendLine("=== PROCESS DATA ===");
sb.AppendLine($"PID: {proc.Pid}");
sb.AppendLine($"Name: {proc.Name}");
sb.AppendLine($"Path: {(string.IsNullOrEmpty(proc.ExePath) ? "unknown" : proc.ExePath)}");
sb.AppendLine($"Parent: {proc.ParentName} (PPID: {proc.Ppid})");
sb.AppendLine($"CommandLine: {proc.CmdLine}");
sb.AppendLine($"Signed: {proc.IsSigned}");
sb.AppendLine($"Signer: {(string.IsNullOrEmpty(proc.Signer) ? "-" : proc.Signer)}");
sb.AppendLine($"SystemPath: {proc.IsSystemPath}");
sb.AppendLine($"TempPath: {proc.IsTempPath}");
if (proc.Connections.Count > 0)
{
sb.AppendLine($"\nNetworkConnections ({proc.Connections.Count}):");
foreach (var c in proc.Connections.Take(8))
sb.AppendLine($" {c.Protocol} {c.LocalAddr} -> {c.RemoteAddr} [{c.State}]");
}
if (matches.Count > 0)
{
sb.AppendLine($"\nMemorySignatures ({matches.Count}):");
foreach (var m in matches.Take(10))
sb.AppendLine($" [{m.Severity}] {m.SigId} @ {m.Address}: {m.Description}");
}
if (suspiciousRegions.Count > 0)
{
sb.AppendLine($"\nSuspiciousMemoryRegions ({suspiciousRegions.Count}):");
foreach (var r in suspiciousRegions.Take(5))
sb.AppendLine($" 0x{r.BaseAddress:X} size={r.Size} protect={r.Protect} type={r.Type}");
}
sb.AppendLine("\n=== ANALYZE THIS PROCESS ===");
return sb.ToString();
}
private static ScanResult ParseLegacyResponse(ProcessInfo proc, string content)
{
content = content.Trim();
if (content.StartsWith("```"))
content = string.Join('\n', content.Split('\n')[1..^1]);
try
{
using var doc = JsonDocument.Parse(content);
var root = doc.RootElement;
var riskLevel = root.GetProperty("risk_level").GetString() switch
{
"high" => RiskLevel.High,
"suspicious" => RiskLevel.Suspicious,
_ => RiskLevel.Safe
};
var indicators = new List<string>();
if (root.TryGetProperty("indicators", out var indArr))
foreach (var ind in indArr.EnumerateArray())
indicators.Add(ind.GetString() ?? "");
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = riskLevel,
RiskScore = root.GetProperty("risk_score").GetDouble(),
Indicators = indicators,
Reasoning = root.GetProperty("reasoning").GetString() ?? "",
AIProvider = "llm-api-v2",
DetectionLayer = "LLM",
ScanTime = DateTime.Now
};
}
catch
{
return new ScanResult
{
Pid = proc.Pid,
ProcessName = proc.Name,
RiskLevel = RiskLevel.Safe,
RiskScore = 0,
Reasoning = content,
AIProvider = "llm-api-v2",
DetectionLayer = "LLM",
ScanTime = DateTime.Now
};
}
}
public async Task<(bool ok, string message)> TestConnectionAsync()
{
if (_config.Provider == AIProvider.Heuristic)
{
return _config.EnableLocalAI
? (true, "AI 管线模式 (Pipeline v2.0, 无需 API)")
: (true, "启发式规则模式 (Heuristic v2.0, 无需 API)");
}
try
{
await CallAIAsync("ping");
return (true, $"OK - {_config.Provider}/{_config.Model}");
}
catch (Exception ex) { return (false, ex.Message); }
}
private const string V2SystemPrompt = @"You are a Windows memory security expert (v2.0) analyzing process threat levels.
=== CORE PRINCIPLE ===
The VAST majority (>95%) of processes are SAFE. Default to ""safe"" unless you see CLEAR threat evidence.
=== CRITICAL FALSE POSITIVE PREVENTION ===
1. .NET/Java/Node.js/Chrome/V8 processes LEGITIMATELY use RWX memory for JIT compilation. This is NOT suspicious.
2. Security/EDR products legitimately inject/hook processes. Their RWX + injection API usage is NORMAL.
3. Development/debugging tools legitimately use Process inspection APIs.
4. A single indicator ALONE is never enough for ""high"" risk.
=== RISK LEVELS ===
""safe"": Normal benign process, even with minor deviations (< 0.30)
""suspicious"": Multiple weak signals or single strong signal (0.30-0.70)
""high"": Clear evidence with multiple concurring signals (> 0.70)
=== JSON RESPONSE ===
{""risk_level"":""safe"",""risk_score"":0.0,""indicators"":[],""reasoning"":""""}";
}