Skip to content

Commit 9bfe57d

Browse files
committed
feat: (anticheat) continuous detection and hardened server fingerprinting
The detector reported once at tick 41 and reset, before the plugin scan reply arrived, then never corrected itself. Warm up longer and keep re-evaluating so late plugin/command signals are used. Fingerprint from a frozen join-time transaction sequence instead of a rolling window, scan plugins earlier, fold the server MOTD into the token set and close token gaps (grim, ncp, nocheat). Notifications now include brand, TPS and ping.
1 parent 38bac22 commit 9bfe57d

2 files changed

Lines changed: 49 additions & 12 deletions

File tree

src/main/java/net/ccbluex/liquidbounce/features/module/modules/other/AnticheatDetector.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,20 @@ import net.ccbluex.liquidbounce.utils.client.chat
1717
import net.minecraft.network.play.server.S01PacketJoinGame
1818
import net.ccbluex.liquidbounce.utils.client.ServerUtils.remoteIp
1919
import net.ccbluex.liquidbounce.utils.client.ServerObserver
20+
import java.util.Locale
2021

2122
object AnticheatDetector : Module("AntiCheatDetector", Category.OTHER, Category.SubCategory.MISCELLANEOUS) {
2223

23-
private val debug by boolean("Debug", true)
24+
private const val WARMUP_TICKS = 60
25+
26+
private val debug by boolean("Debug", false)
2427
.describe("Print transaction action numbers to chat for debugging.")
2528
private var check = false
2629
private var ticksPassed = 0
30+
private var loggedOnce = false
2731

2832
var detectedACName: String = ""
33+
private set
2934

3035
val onPacket = handler<PacketEvent> { event ->
3136
if (event.packet is S01PacketJoinGame) {
@@ -35,28 +40,46 @@ object AnticheatDetector : Module("AntiCheatDetector", Category.OTHER, Category.
3540
}
3641

3742
val onTick = handler<GameTickEvent> {
38-
if (check && ticksPassed++ > 40) {
39-
val result = ServerObserver.guessAnticheat(remoteIp) ?: "Unknown"
43+
if (!check) return@handler
44+
if (ticksPassed++ < WARMUP_TICKS) return@handler
45+
46+
val result = ServerObserver.guessAnticheat(remoteIp) ?: "Unknown"
47+
if (result != detectedACName) {
4048
detectedACName = result
41-
notify(result)
42-
if (debug && result == "Unknown") logNumbers()
43-
reset()
49+
if (result != "Unknown") notify(result)
50+
}
51+
if (debug && result == "Unknown" && !loggedOnce) {
52+
loggedOnce = true
53+
logNumbers()
4454
}
4555
}
4656

47-
private fun notify(message: String) =
48-
hud.addNotification(Notification("Alert", "§3Anticheat detected: $message", Type.WARNING, 3000))
49-
57+
private fun notify(message: String) {
58+
val brand = ServerObserver.serverBrand ?: "?"
59+
val tps = ServerObserver.tps
60+
val tpsText = if (tps.isFinite()) String.format(Locale.ROOT, "%.1f", tps) else "?"
61+
val ping = ServerObserver.ping
62+
hud.addNotification(
63+
Notification(
64+
"Alert",
65+
"§3AC: $message §7| Brand $brand | TPS $tpsText | ${ping}ms",
66+
Type.WARNING,
67+
3000
68+
)
69+
)
70+
}
5071

5172
private fun logNumbers() {
52-
val actions = ServerObserver.transactions
73+
val actions = ServerObserver.initialTransactions
5374
chat("Action Numbers: ${actions.joinToString()}")
5475
chat("Differences: ${actions.windowed(2) { it[1] - it[0] }.joinToString()}")
5576
}
5677

5778
private fun reset() {
5879
ticksPassed = 0
5980
check = false
81+
loggedOnce = false
82+
detectedACName = ""
6083
}
6184

6285
override fun onEnable() = reset()

src/main/java/net/ccbluex/liquidbounce/utils/client/ServerObserver.kt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ object ServerObserver : MinecraftInstance, Listenable {
3535

3636
private const val TPS_SAMPLE_COUNT = 15
3737
private const val TRANSACTION_SAMPLE_COUNT = 32
38-
private const val PLUGIN_SCAN_DELAY_TICKS = 40
38+
private const val INITIAL_TRANSACTION_COUNT = 8
39+
private const val PLUGIN_SCAN_DELAY_TICKS = 20
3940

4041
private val tpsIntervals = ArrayDeque<Long>(TPS_SAMPLE_COUNT + 1)
4142
private val transactionSamples = ArrayDeque<Int>(TRANSACTION_SAMPLE_COUNT + 1)
43+
private val initialTransactionSamples = ArrayList<Int>(INITIAL_TRANSACTION_COUNT)
4244
private val mutablePlugins = TreeSet(String.CASE_INSENSITIVE_ORDER)
4345
private val mutablePayloadChannels = TreeSet(String.CASE_INSENSITIVE_ORDER)
4446

@@ -76,6 +78,10 @@ object ServerObserver : MinecraftInstance, Listenable {
7678
val transactions: List<Int>
7779
get() = synchronized(transactionSamples) { transactionSamples.toList() }
7880

81+
/** The first transactions seen after joining, which is where the handshake fingerprint lives. */
82+
val initialTransactions: List<Int>
83+
get() = synchronized(initialTransactionSamples) { initialTransactionSamples.toList() }
84+
7985
val ping: Int
8086
get() {
8187
val player = mc.thePlayer ?: return -1
@@ -126,7 +132,7 @@ object ServerObserver : MinecraftInstance, Listenable {
126132
fun guessAnticheat(address: String? = mc.currentServerData?.serverIP): String? {
127133
guessFromHost(address)?.let { return it }
128134
guessFromTokens()?.let { return it }
129-
return guessFromTransactions(transactions)
135+
return guessFromTransactions(initialTransactions)
130136
}
131137

132138
/**
@@ -143,6 +149,7 @@ object ServerObserver : MinecraftInstance, Listenable {
143149
addAll(plugins)
144150
addAll(payloadChannels)
145151
serverBrand?.let(::add)
152+
mc.currentServerData?.serverMOTD?.let(::add)
146153
}.map { normalize(it) }.filter(String::isNotEmpty)
147154

148155
private fun guessFromTokens(): String? {
@@ -163,6 +170,7 @@ object ServerObserver : MinecraftInstance, Listenable {
163170
fun resetSession() {
164171
synchronized(tpsIntervals) { tpsIntervals.clear() }
165172
synchronized(transactionSamples) { transactionSamples.clear() }
173+
synchronized(initialTransactionSamples) { initialTransactionSamples.clear() }
166174
synchronized(mutablePlugins) { mutablePlugins.clear() }
167175
synchronized(mutablePayloadChannels) { mutablePayloadChannels.clear() }
168176
lastTimeUpdateAt = -1L
@@ -195,6 +203,9 @@ object ServerObserver : MinecraftInstance, Listenable {
195203
transactionSamples.addLast(actionNumber)
196204
while (transactionSamples.size > TRANSACTION_SAMPLE_COUNT) transactionSamples.removeFirst()
197205
}
206+
synchronized(initialTransactionSamples) {
207+
if (initialTransactionSamples.size < INITIAL_TRANSACTION_COUNT) initialTransactionSamples.add(actionNumber)
208+
}
198209
}
199210

200211
private fun captureCommandSuggestions(packet: S3APacketTabComplete) {
@@ -288,7 +299,10 @@ object ServerObserver : MinecraftInstance, Listenable {
288299

289300
private val KNOWN_ANTICHEATS = listOf(
290301
KnownAnticheat("nocheatplus", "NoCheatPlus"),
302+
KnownAnticheat("nocheat", "NoCheatPlus"),
303+
KnownAnticheat("ncp", "NoCheatPlus"),
291304
KnownAnticheat("grimac", "Grim"),
305+
KnownAnticheat("grim", "Grim"),
292306
KnownAnticheat("vulcan", "Vulcan"),
293307
KnownAnticheat("matrix", "Matrix"),
294308
KnownAnticheat("spartan", "Spartan"),

0 commit comments

Comments
 (0)