新增模块(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: 重构检测流程,移除提前返回导致的检测屏蔽
358 lines
14 KiB
Java
358 lines
14 KiB
Java
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; }
|
||
}
|
||
}
|