package client.anticheat; import client.MapleCharacter; import client.SkillFactory; import constants.GameConstants; import java.awt.Point; import java.lang.ref.WeakReference; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import server.Timer.CheatTimer; import tools.StringUtil; public class CheatTracker { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock rL = lock.readLock(), wL = lock.writeLock(); private final Map<CheatingOffense, CheatingOffenseEntry> offenses = new LinkedHashMap<>(); private WeakReference<MapleCharacter> chr; // For keeping track of speed attack hack. private long lastAttackTime = 0; private int lastAttackTickCount = 0; private byte Attack_tickResetCount = 0; private long Server_ClientAtkTickDiff = 0; private long lastDamage = 0; private long takingDamageSince; private int numSequentialDamage = 0; private long lastDamageTakenTime = 0; private byte numZeroDamageTaken = 0; private int numSequentialSummonAttack = 0; private long summonSummonTime = 0; private int numSameDamage = 0; private Point lastMonsterMove; private int monsterMoveCount; private int attacksWithoutHit = 0; private byte dropsPerSecond = 0; private long lastDropTime = 0; private byte msgsPerSecond = 0; private long lastMsgTime = 0; private ScheduledFuture<?> invalidationTask; private int gm_message = 0; private int lastTickCount = 0, tickSame = 0; private long lastSmegaTime = 0, lastBBSTime = 0, lastASmegaTime = 0; //private int lastFamiliarTickCount = 0; //private byte Familiar_tickResetCount = 0; //private long Server_ClientFamiliarTickDiff = 0; private int numSequentialFamiliarAttack = 0; private long familiarSummonTime = 0; public CheatTracker(final MapleCharacter chr) { start(chr); } public final void checkAttack(final int skillId, final int tickcount) { final int AtkDelay = GameConstants.getAttackDelay(skillId, skillId == 0 ? null : SkillFactory.getSkill(skillId)); if ((tickcount - lastAttackTickCount) < AtkDelay) { registerOffense(CheatingOffense.FASTATTACK); } lastAttackTime = System.currentTimeMillis(); if (chr.get() != null && lastAttackTime - chr.get().getChangeTime() > 600000) { //chr was afk for 10 mins and is now attacking chr.get().setChangeTime(); } final long STime_TC = lastAttackTime - tickcount; // hack = - more if (Server_ClientAtkTickDiff - STime_TC > 1000) { // 250 is the ping, TODO registerOffense(CheatingOffense.FASTATTACK2); } // if speed hack, client tickcount values will be running at a faster pace // For lagging, it isn't an issue since TIME is running simotaniously, client // will be sending values of older time // System.out.println("Delay [" + skillId + "] = " + (tickcount - lastAttackTickCount) + ", " + (Server_ClientAtkTickDiff - STime_TC)); Attack_tickResetCount++; // Without this, the difference will always be at 100 if (Attack_tickResetCount >= (AtkDelay <= 200 ? 1 : 4)) { Attack_tickResetCount = 0; Server_ClientAtkTickDiff = STime_TC; } updateTick(tickcount); lastAttackTickCount = tickcount; } //unfortunately PVP does not give a tick count public final void checkPVPAttack(final int skillId) { final int AtkDelay = GameConstants.getAttackDelay(skillId, skillId == 0 ? null : SkillFactory.getSkill(skillId)); final long STime_TC = System.currentTimeMillis() - lastAttackTime; // hack = - more if (STime_TC < AtkDelay) { // 250 is the ping, TODO registerOffense(CheatingOffense.FASTATTACK); } lastAttackTime = System.currentTimeMillis(); } public final long getLastAttack() { return lastAttackTime; } public final void checkTakeDamage(final int damage) { numSequentialDamage++; lastDamageTakenTime = System.currentTimeMillis(); // System.out.println("tb" + timeBetweenDamage); // System.out.println("ns" + numSequentialDamage); // System.out.println(timeBetweenDamage / 1500 + "(" + timeBetweenDamage / numSequentialDamage + ")"); if (lastDamageTakenTime - takingDamageSince / 500 < numSequentialDamage) { registerOffense(CheatingOffense.FAST_TAKE_DAMAGE); } if (lastDamageTakenTime - takingDamageSince > 4500) { takingDamageSince = lastDamageTakenTime; numSequentialDamage = 0; } /* (non-thieves) Min Miss Rate: 2% Max Miss Rate: 80% (thieves) Min Miss Rate: 5% Max Miss Rate: 95%*/ if (damage == 0) { numZeroDamageTaken++; if (numZeroDamageTaken >= 35) { // Num count MSEA a/b players numZeroDamageTaken = 0; registerOffense(CheatingOffense.HIGH_AVOID); } } else if (damage != -1) { numZeroDamageTaken = 0; } } public final void checkSameDamage(final int dmg, final double expected) { if (dmg == 999999) { return; } if (dmg > 2000 && lastDamage == dmg && chr.get() != null && (chr.get().getLevel() < 175 || dmg > expected * 2)) { numSameDamage++; if (numSameDamage > 5) { registerOffense(CheatingOffense.SAME_DAMAGE, numSameDamage + " times, damage " + dmg + ", expected " + expected + " [Level: " + chr.get().getLevel() + ", Job: " + chr.get().getJob() + "]"); numSameDamage = 0; } } else { lastDamage = dmg; numSameDamage = 0; } } public final void checkMoveMonster(final Point pos) { if (pos == lastMonsterMove) { monsterMoveCount++; if (monsterMoveCount > 10) { registerOffense(CheatingOffense.MOVE_MONSTERS, "Position: " + pos.x + ", " + pos.y); monsterMoveCount = 0; } } else { lastMonsterMove = pos; monsterMoveCount = 1; } } public final void resetSummonAttack() { summonSummonTime = System.currentTimeMillis(); numSequentialSummonAttack = 0; } public final boolean checkSummonAttack() { numSequentialSummonAttack++; return (System.currentTimeMillis() - summonSummonTime) / (1000 + 1) >= numSequentialSummonAttack; } public final void resetFamiliarAttack() { familiarSummonTime = System.currentTimeMillis(); numSequentialFamiliarAttack = 0; //lastFamiliarTickCount = 0; //Familiar_tickResetCount = 0; //Server_ClientFamiliarTickDiff = 0; } public final boolean checkFamiliarAttack(final MapleCharacter chr) { /*final int tickdifference = (tickcount - lastFamiliarTickCount); if (tickdifference < 500) { chr.getCheatTracker().registerOffense(CheatingOffense.FAST_SUMMON_ATTACK); } final long STime_TC = System.currentTimeMillis() - tickcount; final long S_C_Difference = Server_ClientFamiliarTickDiff - STime_TC; if (S_C_Difference > 500) { chr.getCheatTracker().registerOffense(CheatingOffense.FAST_SUMMON_ATTACK); } Familiar_tickResetCount++; if (Familiar_tickResetCount > 4) { Familiar_tickResetCount = 0; Server_ClientFamiliarTickDiff = STime_TC; } lastFamiliarTickCount = tickcount;*/ numSequentialFamiliarAttack++; //estimated // System.out.println(numMPRegens + "/" + allowedRegens); if ((System.currentTimeMillis() - familiarSummonTime) / (600 + 1) < numSequentialFamiliarAttack) { registerOffense(CheatingOffense.FAST_SUMMON_ATTACK); return false; } return true; } public final void checkDrop() { checkDrop(false); } public final void checkDrop(final boolean dc) { if ((System.currentTimeMillis() - lastDropTime) < 1000) { dropsPerSecond++; if (dropsPerSecond >= (dc ? 32 : 16) && chr.get() != null && !chr.get().isGM()) { if (dc) { chr.get().getClient().getSession().close(); } else { chr.get().getClient().setMonitored(true); } } } else { dropsPerSecond = 0; } lastDropTime = System.currentTimeMillis(); } public final void checkMsg() { //ALL types of msg. caution with number of msgsPerSecond if ((System.currentTimeMillis() - lastMsgTime) < 1000) { //luckily maplestory has auto-check for too much msging msgsPerSecond++; if (msgsPerSecond > 10 && chr.get() != null && !chr.get().isGM()) { chr.get().getClient().getSession().close(); } } else { msgsPerSecond = 0; } lastMsgTime = System.currentTimeMillis(); } public final int getAttacksWithoutHit() { return attacksWithoutHit; } public final void setAttacksWithoutHit(final boolean increase) { if (increase) { this.attacksWithoutHit++; } else { this.attacksWithoutHit = 0; } } public final void registerOffense(final CheatingOffense offense) { registerOffense(offense, null); } public final void registerOffense(final CheatingOffense offense, final String param) { final MapleCharacter chrhardref = chr.get(); if (chrhardref == null || !offense.isEnabled() || chrhardref.isClone() || chrhardref.isGM()) { return; } //System.out.println("OFFENSE REGISTERED: " + offense.name() + " on " + chrhardref.getName()); CheatingOffenseEntry entry = null; rL.lock(); try { entry = offenses.get(offense); } finally { rL.unlock(); } if (entry != null && entry.isExpired()) { expireEntry(entry); entry = null; gm_message = 0; } if (entry == null) { entry = new CheatingOffenseEntry(offense, chrhardref.getId()); } if (param != null) { entry.setParam(param); } entry.incrementCount(); if (offense.shouldAutoban(entry.getCount())) { final byte type = offense.getBanType(); if (type == 1) { // AutobanManager.getInstance().autoban(chrhardref.getClient(), StringUtil.makeEnumHumanReadable(offense.name())); } else if (type == 2) { chrhardref.getClient().getSession().close(); } gm_message = 0; return; } wL.lock(); try { offenses.put(offense, entry); } finally { wL.unlock(); } switch (offense) { // case HIGH_DAMAGE_MAGIC: case HIGH_DAMAGE_MAGIC_2: // case HIGH_DAMAGE: case HIGH_DAMAGE_2: case ATTACK_FARAWAY_MONSTER: case ATTACK_FARAWAY_MONSTER_SUMMON: case SAME_DAMAGE: gm_message++; if (gm_message % 100 == 0) { // World.Broadcast.broadcastGMMessage(CWvsContext.serverNotice(6, "[GM Message] " + MapleCharacterUtil.makeMapleReadable(chrhardref.getName()) + " (level " + chrhardref.getLevel() + ") suspected of hacking! " + StringUtil.makeEnumHumanReadable(offense.name()) + (param == null ? "" : (" - " + param)))); } if (gm_message >= 300 && chrhardref.getLevel() < (offense == CheatingOffense.SAME_DAMAGE ? 175 : 150)) { final Timestamp created = chrhardref.getClient().getCreated(); long time = System.currentTimeMillis(); if (created != null) { time = created.getTime(); } if (time + (15 * 24 * 60 * 60 * 1000) >= System.currentTimeMillis()) { //made within 15 //AutobanManager.getInstance().autoban(chrhardref.getClient(), StringUtil.makeEnumHumanReadable(offense.name()) + " over 500 times " + (param == null ? "" : (" - " + param))); } else { gm_message = 0; //World.Broadcast.broadcastGMMessage(CWvsContext.serverNotice(6, "[GM Message] " + MapleCharacterUtil.makeMapleReadable(chrhardref.getName()) + " (level " + chrhardref.getLevel() + ") suspected of autoban! " + StringUtil.makeEnumHumanReadable(offense.name()) + (param == null ? "" : (" - " + param)))); //FileoutputUtil.log(FileoutputUtil.Hacker_Log, "[GM Message] " + MapleCharacterUtil.makeMapleReadable(chrhardref.getName()) + " (level " + chrhardref.getLevel() + ") suspected of autoban! " + StringUtil.makeEnumHumanReadable(offense.name()) + (param == null ? "" : (" - " + param))); } } break; } CheatingOffensePersister.getInstance().persistEntry(entry); } public void updateTick(int newTick) { if (newTick <= lastTickCount) { //definitely packet spamming or the added feature in many PEs which is to generate random tick if (tickSame >= 5 && chr.get() != null && !chr.get().isGM()) { chr.get().getClient().getSession().close(); } else { tickSame++; } } else { tickSame = 0; } lastTickCount = newTick; } public boolean canSmega() { if (lastSmegaTime + 15000 > System.currentTimeMillis() && chr.get() != null && !chr.get().isGM()) { return false; } lastSmegaTime = System.currentTimeMillis(); return true; } public boolean canAvatarSmega() { if (lastASmegaTime + 300000 > System.currentTimeMillis() && chr.get() != null && !chr.get().isGM()) { return false; } lastASmegaTime = System.currentTimeMillis(); return true; } public boolean canBBS() { if (lastBBSTime + 60000 > System.currentTimeMillis() && chr.get() != null && !chr.get().isGM()) { return false; } lastBBSTime = System.currentTimeMillis(); return true; } public final void expireEntry(final CheatingOffenseEntry coe) { wL.lock(); try { offenses.remove(coe.getOffense()); } finally { wL.unlock(); } } public final int getPoints() { int ret = 0; CheatingOffenseEntry[] offenses_copy; rL.lock(); try { offenses_copy = offenses.values().toArray(new CheatingOffenseEntry[offenses.size()]); } finally { rL.unlock(); } for (final CheatingOffenseEntry entry : offenses_copy) { if (entry.isExpired()) { expireEntry(entry); } else { ret += entry.getPoints(); } } return ret; } public final Map<CheatingOffense, CheatingOffenseEntry> getOffenses() { return Collections.unmodifiableMap(offenses); } public final String getSummary() { final StringBuilder ret = new StringBuilder(); final List<CheatingOffenseEntry> offenseList = new ArrayList<>(); rL.lock(); try { for (final CheatingOffenseEntry entry : offenses.values()) { if (!entry.isExpired()) { offenseList.add(entry); } } } finally { rL.unlock(); } Collections.sort(offenseList, new Comparator<CheatingOffenseEntry>() { @Override public final int compare(final CheatingOffenseEntry o1, final CheatingOffenseEntry o2) { final int thisVal = o1.getPoints(); final int anotherVal = o2.getPoints(); return (thisVal < anotherVal ? 1 : (thisVal == anotherVal ? 0 : -1)); } }); final int to = Math.min(offenseList.size(), 4); for (int x = 0; x < to; x++) { ret.append(StringUtil.makeEnumHumanReadable(offenseList.get(x).getOffense().name())); ret.append(": "); ret.append(offenseList.get(x).getCount()); if (x != to - 1) { ret.append(" "); } } return ret.toString(); } public final void dispose() { if (invalidationTask != null) { invalidationTask.cancel(false); } invalidationTask = null; chr = new WeakReference<>(null); } public final void start(final MapleCharacter chr) { this.chr = new WeakReference<>(chr); invalidationTask = CheatTimer.getInstance().register(new InvalidationTask(), 60000); takingDamageSince = System.currentTimeMillis(); } private final class InvalidationTask implements Runnable { @Override public final void run() { CheatingOffenseEntry[] offenses_copy; rL.lock(); try { offenses_copy = offenses.values().toArray(new CheatingOffenseEntry[offenses.size()]); } finally { rL.unlock(); } for (CheatingOffenseEntry offense : offenses_copy) { if (offense.isExpired()) { expireEntry(offense); } } if (chr.get() == null) { dispose(); } } } }