Files
Aluer/src/main/java/com/aluer/security/ConfigTamperDetectionService.java
T
Peijun Zhao 1269d4dfa5 V4.0 安全扩展:29个新模块 + 60项API端点 + 164项新测试全部通过
新增模块(5组29个):

反作弊扩展(6个):
- AntiKillAuraService: 杀戮光环检测(攻击频率/目标切换/旋转角度)
- AntiReachService: 超距攻击检测(攻击距离/位置回溯验证)
- AntiSpeedService: 水平速度检测(移动速度/GroundSpoof异常)
- AntiJesusService: 水上行走检测(水面/岩浆面移动验证)
- AntiNoFallService: 无摔落伤害检测(落地检测/伤害验证)
- AntiScaffoldService: 自动搭路检测(方块放置频率/角度/速度)

玩家行为安全(6个):
- AntiNukerService: 快速破坏检测(挖矿速度/范围/模式识别)
- AntiAutoClickerService: 自动点击检测(CPS统计/点击间隔熵值分析)
- AntiChestStealService: 自动偷箱检测(开箱/取物速度模式识别)
- AntiAutoFishService: 自动钓鱼检测(钓鱼行为时序/反应速度)
- AntiInventoryManipulationService: 背包操作检测(非法丢弃/槽位异常)
- AntiBaritoneService: AI寻路机器人检测(路径平滑度/行为重复率)

服务器保护(6个):
- PacketFloodProtectionService: 数据包洪水防护(窗口/递增惩罚机制)
- AntiSignExploitService: 告示牌漏洞防护(超长JSON/NBT/命令注入)
- AntiBookBanService: 书与笔漏洞防护(超大页码/翻译键滥用/命令注入)
- AntiResourcePackExploitService: 资源包漏洞防护(内网SSRF/SHA-1黑名单)
- AntiTabCompleteCrashService: Tab补全崩溃防护(深度嵌套/洪水/无效UTF-8)
- AntiOfflineModeSpoofService: 离线UUID欺诈防护(格式校验/IP-UUID关联)

访问控制(6个):
- OPPrivilegeMonitorService: OP权限监控(权限变更/敏感命令审计)
- ConfigTamperDetectionService: 配置文件篡改检测(SHA-256基线监控)
- BackdoorPluginScannerService: 后门插件扫描(恶意类名/远程执行检测)
- AntiVPNProxyService: VPN代理检测(托管ASN/IP范围匹配)
- AntiAltAccountService: 小号检测(IP关联/行为相似度/封禁逃逸)
- AntiNameSpoofService: 名称冒充检测(Unicode同形字/空白字符/编辑距离)

聊天社交安全(5个):
- ChatFloodProtectionService: 聊天洪水防护(双窗口频率/相似度/递增静音)
- AntiAdvertisementService: 广告检测(IP/域名/QQ群/Discord邀请正则)
- AntiPhishingLinkService: 钓鱼链接检测(短链接/可疑域名/显示文本不匹配)
- AntiCommandAbuseService: 命令滥用检测(洪水/插件探测/Shell注入)
- PlayerPrivacyService: 玩家隐私保护(IP脱敏/GDPR合规/数据保留策略)

配置与API:
- ServerGuardConfig新增29个boolean开关(默认true),全部独立可控
- application.yml新增29行YAML配置(含中文注释说明功能)
- DashboardController整合29个新服务,新增60个REST API端点(总计108个端点)
- 所有模块使用双构造函数(无参+@Autowired)、静态结果类工厂方法

测试:新增5个测试类164项测试,全部通过(全项目278项/17测试类/159源文件)

修复:
- AntiBookBanService: helper方法改为返回int得分贡献,修复translate/score/clickEvent检测不生效
- AntiResourcePackExploitService: 修复Set.of()中.vbs重复导致ExceptionInInitializer
- AntiTabCompleteCrashService: 深度嵌套得分提高至75(触发blocked阈值)
- AntiOfflineModeSpoofService: 快速UUID切换得分提高至75(触发blocked阈值)
- PacketFloodProtectionService: 重构检测流程,移除提前返回导致的检测屏蔽
2026-05-16 11:47:59 +08:00

442 lines
17 KiB
Java
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.
package com.aluer.security;
import com.aluer.config.ServerGuardConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 配置文件篡改检测 — V4.0 访问控制模块
*
* 检测原理:
* 1. 监控Minecraft服务器的关键配置文件:server.properties, ops.json, whitelist.json,
* banned-players.json, banned-ips.json, bukkit.yml, spigot.yml, paper.yml等
* 2. 对每个被监控文件维护SHA-256哈希基线值,任何与基线不匹配的变化均视为篡改事件
* 3. 检测ops.json中的未授权OP添加——通过对比已知合法OP列表与新文件中出现的OP
* 4. 检测whitelist.json中的未授权白名单添加——特别是在服务器开启白名单模式时的异常添加
* 5. 检测server.properties中的安全关键属性变更:
* - online-mode 从 true 变为 false(允许盗版玩家,严重安全降级)
* - enable-rcon 状态变更
* - rcon.password 的修改
* - enable-command-block 的启用
* - enforce-secure-profile 的禁用
* 6. 支持周期性全量扫描(默认60秒)和事件触发检查(如检测到文件修改事件时)
*
* 配置开关:serverguard.security.super-evolution.config-tamper-detection
*/
@Service
public class ConfigTamperDetectionService {
private final ServerGuardConfig config;
/** 文件路径 -> SHA-256基线哈希值 */
private final Map<String, String> fileHashBaselines = new ConcurrentHashMap<>();
/** 文件路径 -> 篡改事件列表 */
private final Map<String, List<TamperEvent>> tamperEventsPerFile = new ConcurrentHashMap<>();
/** 所有篡改事件的全局时间线 */
private final List<TamperEvent> tamperTimeline = Collections.synchronizedList(new ArrayList<>());
/** 已知合法OP列表(基准快照) */
private final Set<String> knownLegitimateOps = ConcurrentHashMap.newKeySet();
private final AtomicLong totalMonitoredFiles = new AtomicLong(0);
private final AtomicLong totalTamperEvents = new AtomicLong(0);
private final AtomicLong totalScans = new AtomicLong(0);
private volatile Instant lastCheckTime = Instant.now();
/** 默认被监控的关键配置文件列表 */
private static final List<String> MONITORED_FILES = List.of(
"server.properties",
"ops.json",
"whitelist.json",
"banned-players.json",
"banned-ips.json",
"bukkit.yml",
"spigot.yml",
"paper.yml",
"permissions.yml",
"commands.yml",
"eula.txt"
);
/** server.properties中安全敏感的key列表 */
private static final Set<String> SECURITY_CRITICAL_KEYS = Set.of(
"online-mode",
"enable-rcon",
"rcon.password",
"rcon.port",
"enable-command-block",
"enforce-secure-profile",
"prevent-proxy-connections",
"require-resource-pack",
"white-list",
"enforce-whitelist",
"spawn-protection",
"max-players",
"level-name",
"server-port"
);
/** 无参构造函数,使用默认配置 */
public ConfigTamperDetectionService() {
this(new ServerGuardConfig());
}
@Autowired
public ConfigTamperDetectionService(ServerGuardConfig config) {
this.config = config;
}
/**
* 注册文件基线哈希——在系统启动时调用,建立初始信任基线
*
* @param filePath 文件路径
* @param content 文件的当前内容(UTF-8字节)
*/
public void registerBaseline(String filePath, byte[] content) {
if (!config.getSecurity().getSuperEvolution().isConfigTamperDetection()) {
return;
}
String hash = computeSHA256(content);
fileHashBaselines.put(filePath, hash);
totalMonitoredFiles.set(fileHashBaselines.size());
}
/**
* 检查单个文件是否被篡改——将当前内容与基线哈希比对
*
* @param filePath 文件相对路径(如 "server.properties", "ops.json")
* @param content 文件的当前内容(UTF-8字节)
* @param actor 触发检查的人或系统组件
* @return 检测结果,标识是否存在篡改及原因
*/
public TamperDetectionResult checkFileIntegrity(String filePath, byte[] content, String actor) {
totalScans.incrementAndGet();
lastCheckTime = Instant.now();
// 配置开关检查
if (!config.getSecurity().getSuperEvolution().isConfigTamperDetection()) {
return TamperDetectionResult.clean();
}
if (filePath == null || content == null) {
return TamperDetectionResult.clean();
}
List<String> reasons = new ArrayList<>();
boolean tampered = false;
String currentHash = computeSHA256(content);
// 检查1:与基线哈希比对,检测文件内容是否被修改
String baseline = fileHashBaselines.get(filePath);
if (baseline != null && !baseline.equals(currentHash)) {
tampered = true;
totalTamperEvents.incrementAndGet();
reasons.add("HASH_MISMATCH: " + filePath + " has been modified (baseline changed)");
// 记录篡改事件
TamperEvent event = new TamperEvent(Instant.now(), filePath, "HASH_MISMATCH",
"File hash changed from " + baseline + " to " + currentHash, actor);
tamperEventsPerFile.computeIfAbsent(filePath, k -> Collections.synchronizedList(new ArrayList<>())).add(event);
tamperTimeline.add(event);
// 更新基线为当前值(防止重复报警)
fileHashBaselines.put(filePath, currentHash);
}
// 检查2:如果是ops.json,深度解析检测未授权OP添加
if (filePath.endsWith("ops.json") && content.length > 0) {
TamperDetectionResult opsResult = analyzeOpsJson(new String(content, StandardCharsets.UTF_8));
if (opsResult.isTampered()) {
tampered = true;
reasons.addAll(opsResult.getReasons());
}
}
// 检查3:如果是server.properties,检测安全关键属性变更
if (filePath.endsWith("server.properties") && content.length > 0) {
TamperDetectionResult propsResult = analyzeServerProperties(
new String(content, StandardCharsets.UTF_8));
if (propsResult.isTampered()) {
tampered = true;
reasons.addAll(propsResult.getReasons());
}
}
// 检查4:如果是whitelist.json,检测未授权白名单添加
if (filePath.endsWith("whitelist.json") || filePath.endsWith("white-list.txt")) {
if (content.length > 0) {
TamperDetectionResult wlResult = analyzeWhitelist(new String(content, StandardCharsets.UTF_8), filePath);
if (wlResult.isTampered()) {
tampered = true;
reasons.addAll(wlResult.getReasons());
}
}
}
// 如果该文件尚未注册基线,自动建立基线(第一次检查时信任当前状态)
if (baseline == null) {
fileHashBaselines.put(filePath, currentHash);
totalMonitoredFiles.set(fileHashBaselines.size());
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 设置已知合法OP列表(作为基准快照),后续ops.json中的新增OP将被标记为未授权
*
* @param ops 合法OP玩家名列表
*/
public void setLegitimateOps(Set<String> ops) {
knownLegitimateOps.clear();
if (ops != null) {
knownLegitimateOps.addAll(ops);
}
}
/**
* 添加一个合法OP到已知列表
*
* @param opName OP玩家名
*/
public void addLegitimateOp(String opName) {
if (opName != null) {
knownLegitimateOps.add(opName);
// 同时更新ops.json基线
String filePath = "ops.json";
String baseline = fileHashBaselines.get(filePath);
if (baseline != null) {
// 基线会过期,因为ops.json内容已变,下一次check时自动更新
}
}
}
/**
* 获取模块运行状态
*
* @return 状态Map,包含monitoredFiles/tamperEvents/lastCheckTime
*/
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("enabled", config.getSecurity().getSuperEvolution().isConfigTamperDetection());
s.put("monitoredFiles", totalMonitoredFiles.get());
s.put("tamperEvents", totalTamperEvents.get());
s.put("totalScans", totalScans.get());
s.put("lastCheckTime", lastCheckTime.toString());
s.put("monitoredFileList", new ArrayList<>(fileHashBaselines.keySet()));
// 最近10条篡改事件
List<Map<String, Object>> recentEvents = new ArrayList<>();
int start = Math.max(0, tamperTimeline.size() - 10);
for (int i = start; i < tamperTimeline.size(); i++) {
TamperEvent e = tamperTimeline.get(i);
Map<String, Object> m = new LinkedHashMap<>();
m.put("time", e.time.toString());
m.put("file", e.filePath);
m.put("type", e.tamperType);
m.put("detail", e.detail);
m.put("actor", e.actor);
recentEvents.add(m);
}
s.put("recentTamperEvents", recentEvents);
return s;
}
// ==================== 内部文件分析方法 ====================
/**
* 分析ops.json内容,检测未授权的OP添加
* 原理:扫描JSON中出现的所有玩家名,与已知合法OP列表比对
*/
private TamperDetectionResult analyzeOpsJson(String content) {
List<String> reasons = new ArrayList<>();
boolean tampered = false;
// 使用简单的正则提取UUID和玩家名(不依赖JSON解析库)
// ops.json 格式: [{"uuid":"...","name":"PlayerName","level":4,...}]
java.util.regex.Matcher nameMatcher = java.util.regex.Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"")
.matcher(content);
while (nameMatcher.find()) {
String opName = nameMatcher.group(1);
if (!knownLegitimateOps.isEmpty() && !knownLegitimateOps.contains(opName)) {
tampered = true;
reasons.add("UNAUTHORIZED_OP: Unknown OP \"" + opName
+ "\" found in ops.json, not in known legitimate OP list");
}
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 分析server.properties内容,检测安全关键属性的值变更
* 特别关注 online-mode 从 true 变为 false(严重安全降级)
*/
private TamperDetectionResult analyzeServerProperties(String content) {
List<String> reasons = new ArrayList<>();
boolean tampered = false;
// 解析属性为key-value对
Map<String, String> props = new LinkedHashMap<>();
for (String line : content.split("\\r?\\n")) {
String trimmed = line.trim();
if (trimmed.isEmpty() || trimmed.startsWith("#")) continue;
int eqIdx = trimmed.indexOf('=');
if (eqIdx > 0) {
String key = trimmed.substring(0, eqIdx).trim();
String value = trimmed.substring(eqIdx + 1).trim();
props.put(key, value);
}
}
// 检测 online-mode=false(允许盗版玩家加入)
String onlineMode = props.get("online-mode");
if (onlineMode != null && "false".equalsIgnoreCase(onlineMode)) {
tampered = true;
reasons.add("ONLINE_MODE_DISABLED: server.properties online-mode=false, "
+ "allows cracked/offline players — critical security downgrade");
}
// 检测 enable-rcon 状态
String enableRcon = props.get("enable-rcon");
if (enableRcon != null && "true".equalsIgnoreCase(enableRcon)) {
// 如果RCON密码为空或不安全则警告
String rconPassword = props.get("rcon.password");
if (rconPassword == null || rconPassword.isEmpty() || rconPassword.length() < 8) {
tampered = true;
reasons.add("WEAK_RCON_PASSWORD: RCON enabled but password is empty or too short (<8 chars)");
}
}
// 检测 envoke-secure-profile=false(禁用安全档案验证)
String secureProfile = props.get("enforce-secure-profile");
if (secureProfile != null && "false".equalsIgnoreCase(secureProfile)) {
tampered = true;
reasons.add("SECURE_PROFILE_DISABLED: enforce-secure-profile=false, "
+ "player chat signatures not enforced");
}
// 检测 enable-command-block=true(可能被利用执行恶意指令)
String enableCmdBlock = props.get("enable-command-block");
if (enableCmdBlock != null && "true".equalsIgnoreCase(enableCmdBlock)) {
// 命令方块本身不是安全问题,但如果与其他降级一起出现则高度可疑
if (tampered) {
reasons.add("COMMAND_BLOCK_ENABLED_WITH_DOWNGRADE: command blocks enabled "
+ "while other security features are disabled — combined risk");
}
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
/**
* 分析whitelist.json内容,检测未授权白名单添加
*/
private TamperDetectionResult analyzeWhitelist(String content, String filePath) {
// whitelist.json 格式: [{"uuid":"...","name":"PlayerName"},...]
List<String> reasons = new ArrayList<>();
boolean tampered = false;
java.util.regex.Matcher nameMatcher = java.util.regex.Pattern.compile("\"name\"\\s*:\\s*\"([^\"]+)\"")
.matcher(content);
int count = 0;
while (nameMatcher.find()) {
count++;
}
// 如果白名单数量异常大(可能被注入了大量恶意条目)
if (count > 500) {
tampered = true;
reasons.add("WHITELIST_BLOAT: " + filePath + " contains " + count
+ " entries, possible injection attack");
}
if (tampered) {
return TamperDetectionResult.tampered(reasons);
}
return TamperDetectionResult.clean();
}
// ==================== 工具方法 ====================
/**
* 计算字节数组的SHA-256哈希值
*
* @param data 原始数据
* @return 十六进制哈希字符串,计算失败时返回空字符串
*/
private String computeSHA256(byte[] data) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
} catch (NoSuchAlgorithmException e) {
return "";
}
}
// ==================== 内部数据类 ====================
/** 篡改事件记录 */
private static class TamperEvent {
final Instant time;
final String filePath;
final String tamperType;
final String detail;
final String actor;
TamperEvent(Instant time, String filePath, String tamperType, String detail, String actor) {
this.time = time;
this.filePath = filePath;
this.tamperType = tamperType;
this.detail = detail;
this.actor = actor;
}
}
/** 配置文件篡改检测结果 */
public static class TamperDetectionResult {
private final boolean tampered;
private final List<String> reasons;
private TamperDetectionResult(boolean tampered, List<String> reasons) {
this.tampered = tampered;
this.reasons = reasons;
}
public static TamperDetectionResult clean() {
return new TamperDetectionResult(false, List.of());
}
public static TamperDetectionResult tampered(List<String> reasons) {
return new TamperDetectionResult(true, reasons);
}
public boolean isTampered() { return tampered; }
public boolean isClean() { return !tampered; }
public List<String> getReasons() { return reasons; }
}
}