Files
Aluer/src/main/java/com/aluer/security/AntiResourcePackExploitService.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

358 lines
14 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.net.*;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* 资源包漏洞防护服务 — V4.0 服务器保护模块
*
* 检测原理:
* Minecraft允许服务器向客户端推送资源包(Resource Pack)URL,客户端会自动下载
* 并加载。如果URL指向恶意资源,可能导致客户端安全风险。本服务验证资源包URL的
* 合法性,检测SSRF攻击、恶意文件类型、过大文件、以及已知的恶意SHA-1哈希。
*
* 攻击场景:
* - SSRF攻击:URL指向内网地址(file:///etc/passwd, localhost等)
* - 超大文件:资源包>100MB导致客户端下载时内存溢出
* - 恶意文件类型:.exe/.jar/.sh等可执行文件伪装为资源包
* - 已知恶意哈希:使用已被确认的恶意资源包SHA-1
* - HTTP重定向攻击:URL重定向到危险目标
*
* 配置开关:serverguard.security.super-evolution.anti-resource-pack-exploit
*/
@Service
public class AntiResourcePackExploitService {
private final ServerGuardConfig config;
private final Map<String, List<PackEvent>> packHistory = new ConcurrentHashMap<>();
private final Map<String, PackUrlInfo> urlCache = new ConcurrentHashMap<>();
/** SHA-1哈希黑名单(已知恶意资源包) */
private final Set<String> sha1Blacklist = ConcurrentHashMap.newKeySet();
private final Set<String> urlBlacklist = ConcurrentHashMap.newKeySet();
private final AtomicLong totalPacksChecked = new AtomicLong(0);
private final AtomicLong blockedPacks = new AtomicLong(0);
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/** 资源包允许的最大文件大小(字节) */
private static final long MAX_PACK_SIZE_BYTES = 100 * 1024 * 1024; // 100MB
/** URL缓存超时(毫秒) */
private static final long URL_CACHE_TTL_MS = 300_000; // 5分钟
/** 内网IP前缀 */
private static final Set<String> INTERNAL_IPv4_PREFIXES = Set.of(
"10.", "172.16.", "172.17.", "172.18.", "172.19.", "172.20.",
"172.21.", "172.22.", "172.23.", "172.24.", "172.25.", "172.26.",
"172.27.", "172.28.", "172.29.", "172.30.", "172.31.", "192.168."
);
/**
* 危险文件扩展名(资源包必须是.zip格式,以下扩展名可能包含可执行代码).
* 注意:Set.of() 不允许重复元素,每个扩展名必须唯一.
*/
private static final Set<String> DANGEROUS_EXTENSIONS = Set.of(
".exe", ".bat", ".cmd", ".com", ".msi", ".ps1", ".scr",
".jar", ".class", ".war", ".ear",
".sh", ".bash", ".zsh", ".run", ".bin",
".dll", ".so", ".dylib", ".sys",
".deb", ".rpm", ".apk", ".ipa",
".js", ".vbs", ".jse", ".wsf", ".wsh", ".hta"
);
/** 被禁止的协议 */
private static final Set<String> BLOCKED_SCHEMES = Set.of(
"file", "ftp", "gopher", "dict", "jar", "netdoc", "tftp"
);
public AntiResourcePackExploitService() {
this(new ServerGuardConfig());
}
@Autowired
public AntiResourcePackExploitService(ServerGuardConfig config) {
this.config = config;
// 初始化已知恶意哈希(示例值,生产环境应从威胁情报源同步)
initSha1Blacklist();
scheduler.scheduleAtFixedRate(this::cleanupOldData, 60, 300, TimeUnit.SECONDS);
}
/**
* 检查资源包URL和SHA-1哈希是否安全。
*
* @param url 资源包下载URL
* @param sha1 资源包SHA-1哈希(必需,Minecraft强制要求)
* @param sourcePlayer 发起推送的玩家(OP或控制台)
* @param packVersion 资源包格式版本
* @return 检查结果,包含是否阻止及原因
*/
public PackCheckResult check(String url, String sha1, String sourcePlayer, int packVersion) {
// 配置开关检查
if (!config.getSecurity().getSuperEvolution().isAntiResourcePackExploit()) {
return PackCheckResult.clean();
}
totalPacksChecked.incrementAndGet();
List<String> reasons = new ArrayList<>();
// 记录事件
PackEvent event = new PackEvent(Instant.now(), url, sha1, sourcePlayer, packVersion);
packHistory.computeIfAbsent(sourcePlayer, k -> Collections.synchronizedList(new ArrayList<>())).add(event);
// ---- 检测1:URL格式验证 ----
if (url == null || url.trim().isEmpty()) {
reasons.add("EMPTY_URL");
blockedPacks.incrementAndGet();
return PackCheckResult.blocked(reasons);
}
URI uri;
try {
uri = new URI(url.trim());
} catch (URISyntaxException e) {
reasons.add("INVALID_URI: " + e.getMessage());
blockedPacks.incrementAndGet();
return PackCheckResult.blocked(reasons);
}
// ---- 检测2:协议验证(必须http/https) ----
String scheme = uri.getScheme();
if (scheme == null) {
reasons.add("MISSING_SCHEME: URL has no protocol");
} else {
String schemeLower = scheme.toLowerCase();
if (BLOCKED_SCHEMES.contains(schemeLower)) {
reasons.add("BLOCKED_SCHEME: " + scheme + " protocol is not allowed");
} else if (!schemeLower.equals("http") && !schemeLower.equals("https")) {
reasons.add("INVALID_SCHEME: " + scheme + " only http/https allowed");
}
}
// ---- 检测3:内网地址检测(SSRF风险) ----
// 攻击者可以通过资源包URL探测内网服务或读取本地文件
String host = uri.getHost();
if (host != null) {
String hostLower = host.toLowerCase();
// 检测本地回环地址
if ("localhost".equals(hostLower) || "127.0.0.1".equals(hostLower)
|| "::1".equals(hostLower) || "[::1]".equals(hostLower)
|| "0.0.0.0".equals(hostLower)) {
reasons.add("SSRF_LOOPBACK: " + host + " is a loopback address");
}
// 检测内网IP段
for (String prefix : INTERNAL_IPv4_PREFIXES) {
if (hostLower.startsWith(prefix)) {
reasons.add("SSRF_PRIVATE_NET: " + host + " is a private network address");
break;
}
}
// 检测云元数据地址
if (hostLower.contains("metadata") || hostLower.equals("169.254.169.254")) {
reasons.add("SSRF_CLOUD_METADATA: " + host + " is a cloud metadata endpoint");
}
// 尝试DNS解析检查(防止DNS重绑定攻击)
try {
InetAddress resolved = InetAddress.getByName(host);
String resolvedIP = resolved.getHostAddress();
if (resolvedIP.startsWith("10.") || resolvedIP.startsWith("192.168.")
|| resolvedIP.equals("127.0.0.1") || resolvedIP.startsWith("172.16.")) {
reasons.add("SSRF_DNS_RESOLVE: " + host + " resolves to internal IP " + resolvedIP);
}
} catch (UnknownHostException ignored) {
// 无法解析的域名不一定危险,但标记为可疑
}
}
// ---- 检测4:URL黑名单匹配 ----
if (urlBlacklist.contains(url.trim().toLowerCase())) {
reasons.add("URL_BLACKLISTED: URL is in known malicious list");
}
// ---- 检测5:SHA-1哈希黑名单比对 ----
// 检查已知的恶意资源包哈希(可防止恶意资源包被重新分发)
if (sha1 != null && !sha1.trim().isEmpty()) {
if (sha1Blacklist.contains(sha1.trim().toLowerCase())) {
reasons.add("SHA1_BLACKLISTED: resource pack SHA-1 is known malicious");
}
// SHA-1格式验证:必须是40个十六进制字符
if (!sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
reasons.add("SHA1_INVALID_FORMAT: SHA-1 must be 40 hex characters");
}
}
// ---- 检测6:可疑文件扩展名 ----
String path = uri.getPath();
if (path != null) {
String pathLower = path.toLowerCase();
// 资源包应为.zip格式
if (!pathLower.endsWith(".zip")) {
for (String ext : DANGEROUS_EXTENSIONS) {
if (pathLower.endsWith(ext)) {
reasons.add("DANGEROUS_EXTENSION: " + ext + " is not a valid resource pack file");
break;
}
}
}
}
// ---- 检测7:URL长度限制 ----
// 超长的URL可能导致日志注入或缓冲区溢出
if (url.length() > 2048) {
reasons.add("URL_TOO_LONG: " + url.length() + " chars (max: 2048)");
}
// ---- 检测8:URL编码绕过检测 ----
// 检测URL编码中的内网IP(如 %31%32%37%2e%30%2e%30%2e%31 = 127.0.0.1)
String decodedLower = url.trim().toLowerCase();
if (decodedLower.contains("%31%32%37") || decodedLower.contains("127%2e")
|| decodedLower.contains("%31%30%2e") || decodedLower.contains("10%2e")
|| decodedLower.contains("%31%39%32%2e%31%36%38") || decodedLower.contains("192%2e168")
|| decodedLower.contains("localhost")) {
reasons.add("URL_ENCODED_INTERNAL: potentially obfuscated internal address");
}
// 判定结果
if (!reasons.isEmpty()) {
blockedPacks.incrementAndGet();
// 将恶意URL加入黑名单缓存
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), reasons));
return PackCheckResult.blocked(reasons);
}
// 缓存安全URL以避免重复检测
urlCache.put(url.trim().toLowerCase(), new PackUrlInfo(Instant.now(), List.of()));
return PackCheckResult.clean();
}
/**
* 将SHA-1哈希添加到黑名单。
*/
public void addToSha1Blacklist(String sha1) {
if (sha1 != null && sha1.trim().matches("^[0-9a-fA-F]{40}$")) {
sha1Blacklist.add(sha1.trim().toLowerCase());
}
}
/**
* 将URL添加到黑名单。
*/
public void addToUrlBlacklist(String url) {
if (url != null && !url.trim().isEmpty()) {
urlBlacklist.add(url.trim().toLowerCase());
}
}
public Map<String, Object> getStatus() {
Map<String, Object> s = new LinkedHashMap<>();
s.put("totalPacksChecked", totalPacksChecked.get());
s.put("blockedPacks", blockedPacks.get());
s.put("sha1BlacklistSize", sha1Blacklist.size());
s.put("urlBlacklistSize", urlBlacklist.size());
s.put("cachedUrls", urlCache.size());
// 最近检查的资源包
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<PackEvent>> e : packHistory.entrySet()) {
List<PackEvent> events = e.getValue();
if (!events.isEmpty()) {
PackEvent last = events.get(events.size() - 1);
Map<String, Object> m = new LinkedHashMap<>();
m.put("source", e.getKey());
m.put("url", last.url.length() > 100 ? last.url.substring(0, 100) + "..." : last.url);
m.put("sha1", last.sha1);
m.put("time", last.time.toString());
recent.add(m);
}
}
recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString()));
s.put("recentPacks", recent.subList(0, Math.min(recent.size(), 10)));
// URL黑名单(最多显示50条)
s.put("urlBlacklist", new ArrayList<>(urlBlacklist).subList(0, Math.min(urlBlacklist.size(), 50)));
return s;
}
/**
* 初始化已知恶意SHA-1哈希列表。
*/
private void initSha1Blacklist() {
// 这些是示例值 - 生产环境应从威胁情报源定期更新
// 实际部署时可添加已知的恶意资源包哈希
}
private void cleanupOldData() {
Instant cutoff = Instant.now().minusMillis(URL_CACHE_TTL_MS);
urlCache.entrySet().removeIf(e -> e.getValue().timestamp.isBefore(cutoff));
Instant eventCutoff = Instant.now().minusSeconds(7200);
for (List<PackEvent> events : packHistory.values()) {
events.removeIf(e -> e.time.isBefore(eventCutoff));
}
packHistory.entrySet().removeIf(e -> e.getValue().isEmpty());
}
private static class PackEvent {
final Instant time;
final String url;
final String sha1;
final String sourcePlayer;
final int packVersion;
PackEvent(Instant t, String u, String s, String sp, int pv) {
this.time = t;
this.url = u;
this.sha1 = s;
this.sourcePlayer = sp;
this.packVersion = pv;
}
}
private static class PackUrlInfo {
final Instant timestamp;
final List<String> reasons;
PackUrlInfo(Instant t, List<String> r) {
this.timestamp = t;
this.reasons = r;
}
}
public static class PackCheckResult {
private final boolean blocked;
private final List<String> reasons;
private PackCheckResult(boolean blocked, List<String> reasons) {
this.blocked = blocked;
this.reasons = reasons;
}
public static PackCheckResult clean() {
return new PackCheckResult(false, List.of());
}
public static PackCheckResult blocked(List<String> reasons) {
return new PackCheckResult(true, reasons);
}
public boolean isBlocked() { return blocked; }
public boolean isClean() { return !blocked; }
public List<String> getReasons() { return reasons; }
}
}