├── .github ├── FUNDING.yml └── workflows │ └── gradle.yml ├── settings.gradle ├── assets ├── jSus-dark.png └── jSus-light.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── .gitignore ├── src └── main │ └── java │ └── com │ └── github │ └── NeRdTheNed │ └── jSus │ ├── detector │ ├── checker │ │ ├── IChecker.java │ │ ├── TestResult.java │ │ ├── UncommonJVMInstructionChecker.java │ │ ├── CallsNekoClientLikeChecker.java │ │ ├── CallsMethodChecker.java │ │ ├── StringChecker.java │ │ ├── WeirdStringConstructionMethodsChecker.java │ │ ├── ObfuscatorChecker.java │ │ └── Checkers.java │ ├── CheckResult.java │ └── CheckerTask.java │ ├── result │ ├── printer │ │ ├── Printer.java │ │ ├── JSONPrinter.java │ │ └── HumanReadablePrinter.java │ ├── CheckResults.java │ ├── FileScanResults.java │ └── ArchiveScanResults.java │ ├── util │ ├── Pair.java │ └── Util.java │ ├── CMDMain.java │ └── Scanner.java ├── LICENSE ├── README.md ├── gradlew.bat └── gradlew /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: NeRdTheNed 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'jSus' 2 | -------------------------------------------------------------------------------- /assets/jSus-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeRdTheNed/jSus/HEAD/assets/jSus-dark.png -------------------------------------------------------------------------------- /assets/jSus-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeRdTheNed/jSus/HEAD/assets/jSus-light.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NeRdTheNed/jSus/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | # Eclipse 8 | .settings/ 9 | bin/ 10 | tmp/ 11 | .metadata 12 | .classpath 13 | .project 14 | *.tmp 15 | *.bak 16 | *.swp 17 | *~.nib 18 | local.properties 19 | .loadpath 20 | .factorypath 21 | 22 | # macOS junk files 23 | .DS_Store 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/IChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.List; 4 | 5 | import org.objectweb.asm.tree.ClassNode; 6 | 7 | public interface IChecker { 8 | 9 | String getName(); 10 | 11 | TestResult.TestResultLevel getPossibleHighestResult(); 12 | 13 | List testClass(ClassNode clazz); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/CheckResult.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector; 2 | 3 | import java.util.List; 4 | 5 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult; 6 | 7 | public class CheckResult { 8 | 9 | public final String fileName; 10 | public final String checkerName; 11 | public final List checkerResults; 12 | 13 | public CheckResult(String fileName, String checkerName, List checkerResults) { 14 | this.fileName = fileName; 15 | this.checkerName = checkerName; 16 | this.checkerResults = checkerResults; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD Zero Clause License 2 | 3 | Copyright (c) 2023 Ned Loynd 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/printer/Printer.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result.printer; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import com.github.NeRdTheNed.jSus.result.ArchiveScanResults; 8 | 9 | public abstract class Printer { 10 | protected final Map scanResults; 11 | 12 | protected Printer(Map scanResults) { 13 | this.scanResults = scanResults; 14 | } 15 | 16 | protected Printer(String archiveName, ArchiveScanResults scanResult) { 17 | this(new HashMap<>()); 18 | scanResults.put(archiveName, scanResult); 19 | } 20 | 21 | public abstract void print(PrintWriter writer); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/CheckerTask.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import org.objectweb.asm.tree.ClassNode; 6 | 7 | import com.github.NeRdTheNed.jSus.detector.checker.IChecker; 8 | 9 | public class CheckerTask implements Callable { 10 | 11 | public final IChecker checker; 12 | 13 | public final ClassNode clazz; 14 | 15 | public CheckerTask(IChecker checker, ClassNode clazz) { 16 | this.checker = checker; 17 | this.clazz = clazz; 18 | } 19 | 20 | @Override 21 | public CheckResult call() throws Exception { 22 | return new CheckResult(clazz.name + ".class", checker.getName(), checker.testClass(clazz)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/util/Pair.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.util; 2 | 3 | import java.util.Objects; 4 | 5 | public class Pair { 6 | @Override 7 | public int hashCode() { 8 | return Objects.hash(k, v); 9 | } 10 | 11 | @Override 12 | public boolean equals(Object obj) { 13 | if (this == obj) { 14 | return true; 15 | } 16 | 17 | if (!(obj instanceof Pair)) { 18 | return false; 19 | } 20 | 21 | final Pair other = (Pair) obj; 22 | return Objects.equals(k, other.k) && Objects.equals(v, other.v); 23 | } 24 | 25 | public final K k; 26 | public final V v; 27 | public Pair(K k, V v) { 28 | this.k = k; 29 | this.v = v; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/TestResult.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | public class TestResult { 4 | public enum TestResultLevel { 5 | VIRUS("red"), 6 | STRONG_SUS("magenta"), 7 | SUS("yellow"), 8 | BENIGN("blue"), 9 | VERY_BENIGN("green"); 10 | 11 | public final String color; 12 | 13 | TestResultLevel(String color) { 14 | this.color = color; 15 | } 16 | } 17 | 18 | public final TestResultLevel result; 19 | public final String reason; 20 | public final int amount; 21 | 22 | public TestResult(TestResultLevel result, String reason, int amount) { 23 | this.result = result; 24 | this.reason = reason; 25 | this.amount = amount; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jSus 2 | 3 |

4 | 5 | 6 | A screenshot of suspicious behaviour jSus found from scanning a file 7 | 8 |

9 | 10 | WIP Java / Minecraft focused malware false positive generator. 11 | 12 | In general, the point of this project is to flag potentially suspicious behaviour in a program, even if it's for completely innocent reasons. 13 | 14 | Code is largely unfinished and is of bad quality. Please also use a real detector, like [jNeedle](https://github.com/KosmX/jneedle), [nekodetector](https://github.com/MCRcortex/nekodetector), or the [Overwolf tools](https://support.curseforge.com/en/support/solutions/articles/9000228509-june-2023-infected-mods-detection-tool/)! 15 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Build on push or pull request 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | - name: Set up JDK 8 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: 'zulu' 17 | java-version: 8 18 | - name: Validate Gradle wrapper 19 | uses: gradle/wrapper-validation-action@v1 20 | - name: Build with Gradle 21 | uses: gradle/gradle-build-action@v2 22 | with: 23 | gradle-version: wrapper 24 | cache-read-only: ${{ github.ref != 'refs/heads/master' }} 25 | arguments: build 26 | - name: Upload artifacts 27 | uses: actions/upload-artifact@v3 28 | with: 29 | name: Package 30 | path: build/libs 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/printer/JSONPrinter.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result.printer; 2 | 3 | import java.io.IOException; 4 | import java.io.PrintWriter; 5 | import java.util.Map; 6 | 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import com.github.NeRdTheNed.jSus.result.ArchiveScanResults; 9 | 10 | public class JSONPrinter extends Printer { 11 | 12 | public JSONPrinter(Map scanResults) { 13 | super(scanResults); 14 | } 15 | 16 | public JSONPrinter(String archiveName, ArchiveScanResults scanResult) { 17 | super(archiveName, scanResult); 18 | } 19 | 20 | @Override 21 | public void print(PrintWriter writer) { 22 | try { 23 | final String jsonString = new ObjectMapper().writeValueAsString(scanResults); 24 | writer.write(jsonString); 25 | } catch (final IOException e) { 26 | e.printStackTrace(); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/CheckResults.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 7 | import com.fasterxml.jackson.annotation.JsonAnySetter; 8 | import com.fasterxml.jackson.annotation.JsonIgnore; 9 | 10 | public class CheckResults { 11 | @JsonIgnore 12 | public final Map checkResultsMap; 13 | 14 | @JsonAnyGetter 15 | private Map getCheckResultsMap() { 16 | return checkResultsMap; 17 | } 18 | 19 | @JsonAnySetter 20 | private void addToCheckResultsMap(String str, Integer integer) { 21 | checkResultsMap.put(str, integer); 22 | } 23 | 24 | public CheckResults(Map checkResultsMap) { 25 | this.checkResultsMap = checkResultsMap; 26 | } 27 | 28 | public CheckResults(String result, int amount) { 29 | this(new HashMap<>()); 30 | checkResultsMap.put(result, amount); 31 | } 32 | 33 | public CheckResults(String result) { 34 | this(result, 1); 35 | } 36 | 37 | public CheckResults() { 38 | this(new HashMap<>()); 39 | } 40 | 41 | public CheckResults add(String result, int amount) { 42 | checkResultsMap.merge(result, amount, Integer::sum); 43 | return this; 44 | } 45 | 46 | public CheckResults add(String result) { 47 | return add(result, 1); 48 | } 49 | 50 | public CheckResults add(Map results) { 51 | results.forEach(this::add); 52 | return this; 53 | } 54 | 55 | public CheckResults add(CheckResults result) { 56 | return add(result.checkResultsMap); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/CMDMain.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus; 2 | 3 | import java.io.File; 4 | import java.util.concurrent.Callable; 5 | 6 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult; 7 | 8 | import picocli.CommandLine; 9 | import picocli.CommandLine.Command; 10 | import picocli.CommandLine.Option; 11 | import picocli.CommandLine.Parameters; 12 | 13 | @Command(name = "jSus", mixinStandardHelpOptions = true, version = "jSus alpha", 14 | description = "Scans jar files for suspicious behaviour.") 15 | public class CMDMain implements Callable { 16 | @Parameters(index = "0", description = "The file / directory to scan") 17 | private File file; 18 | 19 | @Option(names = { "--level", "-l" }, defaultValue = "VERY_BENIGN", description = "What level of sus to log. Valid values: ${COMPLETION-CANDIDATES}") 20 | TestResult.TestResultLevel level; 21 | 22 | @Option(names = { "--verbose", "-v" }, description = "Enable verbose logging. Separate from sus level.") 23 | boolean verbose = false; 24 | 25 | // TODO Check if color support works on Windows 26 | @Option(names = { "--colour", "--color", "-c" }, negatable = true, defaultValue = "true", fallbackValue = "true", description = "Enable color output (may not work on Windows).") 27 | boolean color = true; 28 | 29 | @Option(names = { "--json", "-j" }, description = "Log results as JSON (experimental, format will likely change)") 30 | boolean json = false; 31 | 32 | @Override 33 | public Integer call() throws Exception { 34 | if (!json) { 35 | System.out.println("jSus: Starting scan of " + file); 36 | } 37 | 38 | final boolean didSus = Scanner.detectSus(file, verbose, json ? TestResult.TestResultLevel.VERY_BENIGN : level, color, json); 39 | 40 | if (!json) { 41 | System.out.println("jSus: Finished scan of " + file); 42 | } 43 | 44 | return didSus ? 1 : CommandLine.ExitCode.OK; 45 | } 46 | 47 | public static void main(String[] args) { 48 | final int exitCode = new CommandLine(new CMDMain()).execute(args); 49 | System.exit(exitCode); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/FileScanResults.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result; 2 | 3 | import java.util.EnumMap; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 8 | 9 | public class FileScanResults { 10 | public boolean errorReadingFile; 11 | public final Map> scanResultGroups; 12 | 13 | public FileScanResults(boolean errorReadingFile, Map> scanResultGroups) { 14 | this.errorReadingFile = errorReadingFile; 15 | this.scanResultGroups = scanResultGroups; 16 | } 17 | 18 | public FileScanResults(boolean errorReadingFile) { 19 | this(errorReadingFile, new EnumMap<>(TestResultLevel.class)); 20 | } 21 | 22 | public FileScanResults() { 23 | this(false); 24 | } 25 | 26 | public FileScanResults add(TestResultLevel level, Map res) { 27 | scanResultGroups.merge(level, res, (existing, toAdd) -> { toAdd.forEach((group, newRes) -> existing.merge(group, newRes, CheckResults::add)); return existing; }); 28 | return this; 29 | } 30 | 31 | public FileScanResults add(TestResultLevel level, String group, CheckResults res) { 32 | scanResultGroups.computeIfAbsent(level, k -> new HashMap<>()).merge(group, res, CheckResults::add); 33 | return this; 34 | } 35 | 36 | public FileScanResults add(TestResultLevel level, String group, String reason, int amount) { 37 | scanResultGroups.computeIfAbsent(level, k -> new HashMap<>()).computeIfAbsent(group, k -> new CheckResults()).add(reason, amount); 38 | return this; 39 | } 40 | 41 | public FileScanResults add(TestResultLevel level, String group, String reason) { 42 | return add(level, group, reason, 1); 43 | } 44 | 45 | public FileScanResults add(Map> groups) { 46 | groups.forEach(this::add); 47 | return this; 48 | } 49 | 50 | public FileScanResults add(FileScanResults res) { 51 | if (res.errorReadingFile) { 52 | errorReadingFile = true; 53 | } 54 | 55 | return add(res.scanResultGroups); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/UncommonJVMInstructionChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.objectweb.asm.Opcodes; 7 | import org.objectweb.asm.tree.AbstractInsnNode; 8 | import org.objectweb.asm.tree.ClassNode; 9 | import org.objectweb.asm.tree.MethodNode; 10 | 11 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 12 | import com.github.NeRdTheNed.jSus.util.Util; 13 | 14 | // I have accidentally created a Kotlin detector, send help 15 | public class UncommonJVMInstructionChecker implements IChecker { 16 | 17 | private static TestResult.TestResultLevel getLevelForNoOpAmount(int amount) { 18 | if (amount > 200) { 19 | return TestResult.TestResultLevel.STRONG_SUS; 20 | } 21 | 22 | if (amount > 100) { 23 | return TestResult.TestResultLevel.SUS; 24 | } 25 | 26 | if (amount > 20) { 27 | return TestResult.TestResultLevel.BENIGN; 28 | } 29 | 30 | return TestResult.TestResultLevel.VERY_BENIGN; 31 | } 32 | 33 | @Override 34 | public String getName() { 35 | return "Uncommon JVM instruction checker"; 36 | } 37 | 38 | // TODO Check for uses of LDC that Javac wouldn't produce (LDC 1 instead of ICONST_1 ect) 39 | @Override 40 | public List testClass(ClassNode clazz) { 41 | final List res = new ArrayList<>(); 42 | int foundNoOps = 0; 43 | 44 | for (final MethodNode methodNode : clazz.methods) { 45 | for (final AbstractInsnNode ins : methodNode.instructions) { 46 | final int opcode = ins.getOpcode(); 47 | 48 | // TODO More functionality 49 | switch (opcode) { 50 | case Opcodes.NOP: 51 | foundNoOps++; 52 | break; 53 | 54 | default: 55 | break; 56 | } 57 | } 58 | } 59 | 60 | if (foundNoOps > 0) { 61 | res.add(new TestResult(getLevelForNoOpAmount(foundNoOps), "Found uncommon JVM opcode " + Util.opcodeName(Opcodes.NOP) + " at class " + clazz.name, foundNoOps)); 62 | } 63 | 64 | return res; 65 | } 66 | 67 | @Override 68 | public TestResultLevel getPossibleHighestResult() { 69 | return TestResult.TestResultLevel.STRONG_SUS; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/ArchiveScanResults.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.fasterxml.jackson.annotation.JsonInclude; 7 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 8 | 9 | public class ArchiveScanResults { 10 | public boolean errorReadingArchive; 11 | @JsonInclude(value = Include.NON_EMPTY, content = Include.NON_NULL) 12 | public final Map recursiveArchiveScanResults; 13 | @JsonInclude(value = Include.NON_EMPTY, content = Include.NON_NULL) 14 | public final Map fileScanResults; 15 | 16 | public ArchiveScanResults(boolean errorReadingArchive, Map recursiveArchiveScanResults, Map fileScanResults) { 17 | this.errorReadingArchive = errorReadingArchive; 18 | this.recursiveArchiveScanResults = recursiveArchiveScanResults; 19 | this.fileScanResults = fileScanResults; 20 | } 21 | 22 | public ArchiveScanResults(Map recursiveArchiveScanResults, Map fileScanResults) { 23 | this(false, recursiveArchiveScanResults, fileScanResults); 24 | } 25 | 26 | public ArchiveScanResults(boolean errorReadingArchive, Map fileScanResults) { 27 | this(errorReadingArchive, new HashMap<>(), fileScanResults); 28 | } 29 | 30 | public ArchiveScanResults(Map fileScanResults) { 31 | this(false, fileScanResults); 32 | } 33 | 34 | public ArchiveScanResults() { 35 | this(new HashMap<>()); 36 | } 37 | 38 | public ArchiveScanResults add(String jij, ArchiveScanResults recursiveRes) { 39 | recursiveArchiveScanResults.merge(jij, recursiveRes, ArchiveScanResults::add); 40 | return this; 41 | } 42 | 43 | public ArchiveScanResults addRec(Map recursiveRes) { 44 | recursiveRes.forEach(this::add); 45 | return this; 46 | } 47 | 48 | public ArchiveScanResults add(String fileName, FileScanResults res) { 49 | fileScanResults.merge(fileName, res, FileScanResults::add); 50 | return this; 51 | } 52 | 53 | public ArchiveScanResults add(Map res) { 54 | res.forEach(this::add); 55 | return this; 56 | } 57 | 58 | public ArchiveScanResults add(ArchiveScanResults archRes) { 59 | if (archRes.errorReadingArchive) { 60 | errorReadingArchive = true; 61 | } 62 | 63 | return addRec(archRes.recursiveArchiveScanResults).add(archRes.fileScanResults); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/CallsNekoClientLikeChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.math.BigInteger; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.UUID; 7 | 8 | import org.objectweb.asm.Opcodes; 9 | import org.objectweb.asm.tree.AbstractInsnNode; 10 | import org.objectweb.asm.tree.ClassNode; 11 | import org.objectweb.asm.tree.MethodInsnNode; 12 | import org.objectweb.asm.tree.MethodNode; 13 | 14 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 15 | 16 | public class CallsNekoClientLikeChecker implements IChecker { 17 | 18 | @Override 19 | public String getName() { 20 | return "Calls method with NekoClient-like generated name from "; 21 | } 22 | 23 | @Override 24 | public TestResultLevel getPossibleHighestResult() { 25 | return TestResult.TestResultLevel.STRONG_SUS; 26 | } 27 | 28 | @Override 29 | public List testClass(ClassNode clazz) { 30 | final List res = new ArrayList<>(); 31 | 32 | for (final MethodNode methodNode : clazz.methods) { 33 | if ("".equals(methodNode.name)) { 34 | for (final AbstractInsnNode ins : methodNode.instructions) { 35 | final int opcode = ins.getOpcode(); 36 | 37 | if (opcode == Opcodes.INVOKESTATIC) { 38 | final MethodInsnNode methodInsNode = (MethodInsnNode) ins; 39 | final String methodName = methodInsNode.name; 40 | 41 | if ((methodName.length() != 33) || (methodName.charAt(0) != '_')) { 42 | continue; 43 | } 44 | 45 | final String methodOwner = methodInsNode.owner; 46 | final String methodDesc = methodInsNode.desc; 47 | 48 | if ("()V".equals(methodDesc) && methodOwner.equals(clazz.name)) { 49 | try { 50 | final BigInteger part1 = new BigInteger(methodName.substring(1, 17), 16); 51 | final BigInteger part2 = new BigInteger(methodName.substring(17, 33), 16); 52 | new UUID(part1.longValue(), part2.longValue()); 53 | // Valid UUID: likely NekoClient 54 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Call to method " + methodOwner + "." + methodName + " found at class " + clazz.name, 1)); 55 | } catch (final Exception e) { 56 | // Ignored 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | 64 | return res; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/CallsMethodChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.objectweb.asm.tree.AbstractInsnNode; 7 | import org.objectweb.asm.tree.ClassNode; 8 | import org.objectweb.asm.tree.MethodInsnNode; 9 | import org.objectweb.asm.tree.MethodNode; 10 | 11 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 12 | import com.github.NeRdTheNed.jSus.util.Util; 13 | 14 | public class CallsMethodChecker implements IChecker { 15 | 16 | private final int compareOpcode; 17 | private final String compareMethodOwner; 18 | private final String compareMethodName; 19 | private final String compareMethodDesc; 20 | 21 | private final boolean fuzzyList; 22 | 23 | private final String name; 24 | 25 | private final TestResult.TestResultLevel result; 26 | 27 | private boolean compareOpcodeMatchWildcard(int toComp) { 28 | if (Util.isOpcodeMethodInvoke(compareOpcode)) { 29 | return compareOpcode == toComp; 30 | } 31 | 32 | return Util.isOpcodeMethodInvoke(toComp); 33 | } 34 | 35 | public CallsMethodChecker(int compareOpcode, String compareMethodOwner, String compareMethodName, String compareMethodDesc, TestResult.TestResultLevel result) { 36 | this.compareOpcode = compareOpcode; 37 | this.compareMethodOwner = compareMethodOwner; 38 | this.compareMethodName = compareMethodName; 39 | this.compareMethodDesc = compareMethodDesc; 40 | this.result = result; 41 | fuzzyList = (compareMethodOwner == null) || (compareMethodName == null); 42 | name = "Calls " + (compareMethodOwner == null ? "*" : compareMethodOwner) + "." + (compareMethodName == null ? "*" : compareMethodName) + " checker"; 43 | } 44 | 45 | @Override 46 | public String getName() { 47 | return name; 48 | } 49 | 50 | @Override 51 | public TestResultLevel getPossibleHighestResult() { 52 | return result; 53 | } 54 | 55 | // TODO Add option to print constants used in parameters to calls 56 | @Override 57 | public List testClass(ClassNode clazz) { 58 | final List res = new ArrayList<>(); 59 | int foundNonFuzzy = 0; 60 | 61 | for (final MethodNode methodNode : clazz.methods) { 62 | for (final AbstractInsnNode ins : methodNode.instructions) { 63 | final int opcode = ins.getOpcode(); 64 | 65 | if (compareOpcodeMatchWildcard(opcode)) { 66 | final MethodInsnNode methodInsNode = (MethodInsnNode) ins; 67 | final String methodOwner = methodInsNode.owner; 68 | final String methodName = methodInsNode.name; 69 | final String methodDesc = methodInsNode.desc; 70 | 71 | if (((compareMethodName == null) || compareMethodName.equals(methodName)) && 72 | ((compareMethodDesc == null) || compareMethodDesc.equals(methodDesc)) && 73 | ((compareMethodOwner == null) || compareMethodOwner.equals(methodOwner))) { 74 | if (fuzzyList) { 75 | res.add(new TestResult(result, "Call to method " + methodOwner + "." + methodName + " found at class " + clazz.name, 1)); 76 | } else { 77 | foundNonFuzzy++; 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | if (foundNonFuzzy > 0) { 85 | res.add(new TestResult(result, "Call to method " + compareMethodOwner + "." + compareMethodName + " found at class " + clazz.name, foundNonFuzzy)); 86 | } 87 | 88 | return res; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/StringChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Comparator; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | import java.util.Optional; 10 | import java.util.regex.Pattern; 11 | import java.util.stream.Stream; 12 | 13 | import org.objectweb.asm.Opcodes; 14 | import org.objectweb.asm.tree.AbstractInsnNode; 15 | import org.objectweb.asm.tree.ClassNode; 16 | import org.objectweb.asm.tree.LdcInsnNode; 17 | import org.objectweb.asm.tree.MethodNode; 18 | 19 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 20 | import com.github.NeRdTheNed.jSus.util.Pair; 21 | import com.github.NeRdTheNed.jSus.util.Util; 22 | 23 | public class StringChecker implements IChecker { 24 | private final String name; 25 | private final Map susMap; 26 | private final Map susPatternMap; 27 | 28 | private final TestResultLevel maxRes; 29 | 30 | public StringChecker(String name, Map susMap) { 31 | this(name, susMap, new HashMap<>()); 32 | } 33 | 34 | public StringChecker(String name, Map susMap, Map susPatternMap) { 35 | this.name = name + " string checker"; 36 | this.susMap = susMap; 37 | this.susPatternMap = susPatternMap; 38 | maxRes = Stream.concat(susMap.values().stream(), susPatternMap.values().stream()).distinct().min(Comparator.naturalOrder()).orElse(null); 39 | } 40 | 41 | private void testStringImpl(Map, Integer> foundStrings, String toCheck) { 42 | TestResult.TestResultLevel testResult = susMap.get(toCheck); 43 | 44 | if (testResult == null) { 45 | // TODO Better code 46 | final Optional> possible = susPatternMap.entrySet().stream().filter(pattern -> pattern.getKey().matcher(toCheck).find()).findFirst(); 47 | 48 | if (possible.isPresent()) { 49 | testResult = possible.get().getValue(); 50 | } 51 | } 52 | 53 | if (testResult != null) { 54 | foundStrings.merge(new Pair<>(toCheck, testResult), 1, Integer::sum); 55 | } 56 | } 57 | 58 | private void testString(Map, Integer> foundStrings, String toCheck) { 59 | testStringImpl(foundStrings, toCheck); 60 | // TODO Try figuring out a better way to do this 61 | /*try { 62 | final String possibleStr = new String(Util.decoder.decode(toCheck)); 63 | testStringImpl(foundStrings, possibleStr); 64 | } catch (final IllegalArgumentException e) { 65 | // Invalid Base64, ignored 66 | }*/ 67 | } 68 | 69 | @Override 70 | public List testClass(ClassNode clazz) { 71 | final List res = new ArrayList<>(); 72 | final Map, Integer> foundStrings = new HashMap<>(); 73 | 74 | for (final MethodNode methodNode : clazz.methods) { 75 | for (final AbstractInsnNode ins : methodNode.instructions) { 76 | final int opcode = ins.getOpcode(); 77 | 78 | if (opcode == Opcodes.LDC) { 79 | final LdcInsnNode ldc = (LdcInsnNode) ins; 80 | 81 | if (ldc.cst instanceof String) { 82 | final String toCheck = (String) ldc.cst; 83 | testString(foundStrings, toCheck); 84 | } 85 | } else { 86 | final String possibleString = Util.tryComputeConstantString(ins); 87 | 88 | if (possibleString != null) { 89 | testString(foundStrings, possibleString); 90 | } 91 | } 92 | } 93 | } 94 | 95 | foundStrings.forEach((pair, count) -> res.add(new TestResult(pair.v, "String " + pair.k + " found at class " + clazz.name, count))); 96 | return res; 97 | } 98 | 99 | @Override 100 | public String getName() { 101 | return name; 102 | } 103 | 104 | @Override 105 | public TestResultLevel getPossibleHighestResult() { 106 | return maxRes; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/result/printer/HumanReadablePrinter.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.result.printer; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.Map; 5 | import java.util.Map.Entry; 6 | 7 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult; 8 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 9 | import com.github.NeRdTheNed.jSus.result.ArchiveScanResults; 10 | import com.github.NeRdTheNed.jSus.result.CheckResults; 11 | import com.github.NeRdTheNed.jSus.result.FileScanResults; 12 | 13 | import picocli.CommandLine.Help.Ansi; 14 | 15 | public class HumanReadablePrinter extends Printer { 16 | private final TestResult.TestResultLevel level; 17 | private final boolean color; 18 | 19 | private static final String FOUND_SUS_ARCH_BASE = "Found sus for archive"; 20 | private static final String FOUND_SUS_ARCH_COL = Ansi.AUTO.string("@|bold,yellow " + FOUND_SUS_ARCH_BASE + "|@ "); 21 | private static final String FOUND_SUS_ARCH_NOCOL = FOUND_SUS_ARCH_BASE + " "; 22 | 23 | private String getLevelText(TestResult.TestResultLevel level) { 24 | return color ? Ansi.AUTO.string("@|" + level.color + " " + level + "|@") : level.toString(); 25 | } 26 | 27 | public HumanReadablePrinter(Map scanResults, TestResultLevel level, boolean color) { 28 | super(scanResults); 29 | this.level = level; 30 | this.color = color; 31 | } 32 | 33 | public HumanReadablePrinter(String archiveName, ArchiveScanResults scanResult, TestResult.TestResultLevel level, boolean color) { 34 | super(archiveName, scanResult); 35 | this.level = level; 36 | this.color = color; 37 | } 38 | 39 | private boolean checkLevel(ArchiveScanResults results) { 40 | return results.errorReadingArchive || 41 | results.fileScanResults.values().stream().anyMatch(e -> e.scanResultGroups.keySet().stream().anyMatch(f -> level.ordinal() >= f.ordinal())) || 42 | results.recursiveArchiveScanResults.values().stream().anyMatch(this::checkLevel); 43 | } 44 | 45 | private void print(PrintWriter writer, String archiveName, ArchiveScanResults archiveResults, String formatBase, boolean first) { 46 | if (checkLevel(archiveResults)) { 47 | writer.printf(formatBase, (first ? "Results for archive " : "Results for bundled archive ") + archiveName); 48 | formatBase = " " + formatBase; 49 | } else if (first) { 50 | return; 51 | } 52 | 53 | if (archiveResults.errorReadingArchive) { 54 | writer.printf(formatBase, "Error while reading archive " + archiveName + ", scan results may be missing"); 55 | } 56 | 57 | // TODO Good code 58 | final String indentOne = " " + formatBase; 59 | final String indentTwo = " " + indentOne; 60 | final String indentThree = " " + indentTwo; 61 | final String indentFour = " " + indentThree; 62 | archiveResults.recursiveArchiveScanResults.forEach((e, f) -> print(writer, e, f, indentOne, false)); 63 | boolean firstSusLog = true; 64 | 65 | for (final Entry fileResultGroup : archiveResults.fileScanResults.entrySet()) { 66 | boolean firstSusLogForFile = true; 67 | 68 | for (final Entry> scanGroup : fileResultGroup.getValue().scanResultGroups.entrySet()) { 69 | final TestResultLevel groupLevel = scanGroup.getKey(); 70 | 71 | if (level.ordinal() >= groupLevel.ordinal()) { 72 | if (firstSusLog) { 73 | writer.printf(formatBase, (color ? FOUND_SUS_ARCH_COL : FOUND_SUS_ARCH_NOCOL) + archiveName); 74 | firstSusLog = false; 75 | } 76 | 77 | if (firstSusLogForFile) { 78 | writer.printf(indentOne, "Found sus for file " + fileResultGroup.getKey()); 79 | firstSusLogForFile = false; 80 | } 81 | 82 | writer.printf(indentTwo, "Sus level " + getLevelText(groupLevel)); 83 | final Map resultMap = scanGroup.getValue(); 84 | 85 | for (final Entry checkEntry : resultMap.entrySet()) { 86 | writer.printf(indentThree, "Sus found by checker " + checkEntry.getKey()); 87 | 88 | for (final Entry scanEntry : checkEntry.getValue().checkResultsMap.entrySet()) { 89 | writer.printf(indentFour, "Detected " + scanEntry.getValue() + " time(s): " + scanEntry.getKey()); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | @Override 98 | public void print(PrintWriter writer) { 99 | scanResults.forEach((archiveName, archiveResults) -> print(writer, archiveName, archiveResults, "- %s%n", true)); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/WeirdStringConstructionMethodsChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import org.objectweb.asm.Opcodes; 7 | import org.objectweb.asm.tree.AbstractInsnNode; 8 | import org.objectweb.asm.tree.ClassNode; 9 | import org.objectweb.asm.tree.MethodInsnNode; 10 | import org.objectweb.asm.tree.MethodNode; 11 | 12 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 13 | import com.github.NeRdTheNed.jSus.util.Util; 14 | 15 | public class WeirdStringConstructionMethodsChecker implements IChecker { 16 | 17 | @Override 18 | public String getName() { 19 | return "Weird string construction methods checker"; 20 | } 21 | 22 | @Override 23 | public TestResultLevel getPossibleHighestResult() { 24 | return TestResult.TestResultLevel.STRONG_SUS; 25 | } 26 | 27 | // TODO More thorough check, check for any use of fixed Base64 as well 28 | @Override 29 | public List testClass(ClassNode clazz) { 30 | final List res = new ArrayList<>(); 31 | int foundFixedByteArrayConstructions = 0; 32 | 33 | for (final MethodNode methodNode : clazz.methods) { 34 | for (final AbstractInsnNode ins : methodNode.instructions) { 35 | final int opcode = ins.getOpcode(); 36 | 37 | if (opcode == Opcodes.INVOKESPECIAL) { 38 | final MethodInsnNode methodInsNode = (MethodInsnNode) ins; 39 | final String methodName = methodInsNode.name; 40 | final String methodOwner = methodInsNode.owner; 41 | final String methodDesc = methodInsNode.desc; 42 | 43 | if ("java/lang/String".equals(methodOwner) && "([B)V".equals(methodDesc) && "".equals(methodName)) { 44 | // TODO Handle more scenarios 45 | final AbstractInsnNode prev = ins.getPrevious(); 46 | 47 | if (prev != null) { 48 | boolean foundString = false; 49 | final int previousOpcode = prev.getOpcode(); 50 | 51 | if (previousOpcode == Opcodes.BASTORE) { 52 | foundString = true; 53 | final String possibleString = Util.tryComputeConstantString(ins); 54 | 55 | if (possibleString == null) { 56 | foundFixedByteArrayConstructions++; 57 | } else { 58 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing String " + possibleString + " from fixed byte array at class " + clazz.name, 1)); 59 | } 60 | } else if (Util.isOpcodeMethodInvoke(previousOpcode)) { 61 | // TODO Handle previous operations like concat 62 | final MethodInsnNode prevMethodInsNode = (MethodInsnNode) prev; 63 | final String prevMethodName = prevMethodInsNode.name; 64 | final String prevMethodOwner = prevMethodInsNode.owner; 65 | final String prevMethodDesc = prevMethodInsNode.desc; 66 | 67 | if (Util.isCommonBase64DecodeMethod(previousOpcode, prevMethodOwner, prevMethodName, prevMethodDesc)) { 68 | final String possibleString = Util.tryComputeConstantString(prev.getPrevious()); 69 | 70 | if (possibleString != null) { 71 | foundString = true; 72 | 73 | try { 74 | final String decoded = new String(Util.decoder.decode(possibleString)); 75 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing String from fixed Base64 " + possibleString + " (decoded: " + decoded + ") at class " + clazz.name, 1)); 76 | } catch (final IllegalArgumentException e) { 77 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing String from invalid (?) fixed Base64 " + possibleString + " at class " + clazz.name, 1)); 78 | } 79 | } 80 | } else { 81 | final byte[] possibleBytes = Util.tryComputeConstantBytes(prev); 82 | 83 | if (possibleBytes != null) { 84 | final String possibleString = new String(possibleBytes); 85 | foundString = true; 86 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing String " + possibleString + " from fixed byte array through method " + prevMethodOwner + "." + prevMethodName + " at class " + clazz.name, 1)); 87 | } 88 | } 89 | } 90 | 91 | if (!foundString) { 92 | final String possibleString = Util.tryComputeConstantString(ins); 93 | 94 | if (possibleString != null) { 95 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing String " + possibleString + " through unknown means (?) at class " + clazz.name, 1)); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | if (foundFixedByteArrayConstructions > 0) { 105 | res.add(new TestResult(TestResult.TestResultLevel.STRONG_SUS, "Constructing unknown String from fixed byte array at class " + clazz.name, foundFixedByteArrayConstructions)); 106 | } 107 | 108 | return res; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/Scanner.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus; 2 | 3 | import java.io.File; 4 | import java.io.PrintWriter; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.concurrent.CompletionService; 8 | import java.util.concurrent.ExecutorCompletionService; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.Future; 12 | import java.util.jar.JarFile; 13 | 14 | import org.objectweb.asm.tree.ClassNode; 15 | 16 | import com.github.NeRdTheNed.jSus.detector.CheckResult; 17 | import com.github.NeRdTheNed.jSus.detector.CheckerTask; 18 | import com.github.NeRdTheNed.jSus.detector.checker.Checkers; 19 | import com.github.NeRdTheNed.jSus.detector.checker.IChecker; 20 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult; 21 | import com.github.NeRdTheNed.jSus.result.ArchiveScanResults; 22 | import com.github.NeRdTheNed.jSus.result.FileScanResults; 23 | import com.github.NeRdTheNed.jSus.result.printer.HumanReadablePrinter; 24 | import com.github.NeRdTheNed.jSus.result.printer.JSONPrinter; 25 | import com.github.NeRdTheNed.jSus.util.Util; 26 | 27 | // TODO Use LL-zip 28 | public class Scanner { 29 | 30 | public static boolean detectSus(File file, boolean verbose, TestResult.TestResultLevel level, boolean color, boolean json) throws Exception { 31 | final PrintWriter pw = new PrintWriter(System.out); 32 | 33 | if (file.isDirectory()) { 34 | return detectSusFromDirectory(file, verbose, level, color, pw, json); 35 | } 36 | 37 | if (file.toString().toLowerCase().endsWith(".class")) { 38 | return detectSusFromClassfile(file, verbose, level, color, pw, json); 39 | } 40 | 41 | try 42 | (final JarFile jarFile = new JarFile(file)) { 43 | return detectSusFromJar(jarFile, verbose, level, color, pw, json); 44 | } catch (final Exception e) { 45 | System.err.println("Invalid directory or jar file " + file.getAbsolutePath()); 46 | throw e; 47 | } 48 | } 49 | 50 | public static boolean detectSusFromDirectory(File dir, boolean verbose, TestResult.TestResultLevel level, boolean color, PrintWriter pw, boolean json) { 51 | if (!dir.isDirectory()) { 52 | System.err.println("Invalid directory " + dir.getAbsolutePath()); 53 | return false; 54 | } 55 | 56 | boolean foundSus = false; 57 | final File[] files = dir.listFiles(); 58 | 59 | if (files != null) { 60 | for (final File file : files) { 61 | if (file.isDirectory() && detectSusFromDirectory(file, verbose, level, color, pw, json)) { 62 | foundSus = true; 63 | } 64 | 65 | final String fileName = file.toString().toLowerCase(); 66 | 67 | if (fileName.endsWith(".class")) { 68 | try { 69 | if (detectSusFromClassfile(file, verbose, level, color, pw, json)) { 70 | foundSus = true; 71 | } 72 | } catch (final Exception e) { 73 | System.err.println("Invalid class file " + file.getAbsolutePath()); 74 | e.printStackTrace(); 75 | } 76 | 77 | continue; 78 | } 79 | 80 | if (!fileName.endsWith(".jar")) { 81 | continue; 82 | } 83 | 84 | try 85 | (final JarFile jarFile = new JarFile(file)) { 86 | if (detectSusFromJar(jarFile, verbose, level, color, pw, json)) { 87 | foundSus = true; 88 | } 89 | } catch (final Exception e) { 90 | System.err.println("Invalid jar file " + file.getAbsolutePath()); 91 | e.printStackTrace(); 92 | } 93 | } 94 | } 95 | 96 | return foundSus; 97 | } 98 | 99 | public static boolean detectSusFromClassfile(File file, boolean verbose, TestResult.TestResultLevel level, boolean color, PrintWriter pw, boolean json) { 100 | final String name = file.getPath(); 101 | 102 | if (verbose && !json) { 103 | System.out.println("Scanning " + name); 104 | } 105 | 106 | final ClassNode node = Util.classfileFileToClass(file); 107 | 108 | if (node != null) { 109 | return detectSusFromNode(node, name, verbose, level, color, pw, json); 110 | } 111 | 112 | return false; 113 | } 114 | 115 | public static boolean detectSusFromJar(JarFile file, boolean verbose, TestResult.TestResultLevel level, boolean color, PrintWriter pw, boolean json) { 116 | final String name = file.getName(); 117 | 118 | if (verbose && !json) { 119 | System.out.println("Scanning " + name); 120 | } 121 | 122 | final List nodes = Util.gatherClassNodesFromJar(file, verbose, json); 123 | return detectSusFromNodes(nodes, name, verbose, level, color, pw, json); 124 | } 125 | 126 | public static boolean detectSusFromNode(ClassNode node, String name, boolean verbose, TestResult.TestResultLevel level, boolean color, PrintWriter pw, boolean json) { 127 | return detectSusFromNodes(Collections.singletonList(node), name, verbose, level, color, pw, json); 128 | } 129 | 130 | public static boolean detectSusFromNodes(Iterable nodes, String name, boolean verbose, TestResult.TestResultLevel level, boolean color, PrintWriter pw, boolean json) { 131 | final ExecutorService execService = Executors.newCachedThreadPool(); 132 | final CompletionService compService = new ExecutorCompletionService<>(execService); 133 | int tasks = 0; 134 | 135 | for (final IChecker checker : Checkers.checkerList) { 136 | final TestResult.TestResultLevel possibleResult = checker.getPossibleHighestResult(); 137 | 138 | if ((possibleResult == null) || (level.ordinal() >= possibleResult.ordinal())) { 139 | for (final ClassNode node : nodes) { 140 | compService.submit(new CheckerTask(checker, node)); 141 | tasks++; 142 | } 143 | } 144 | } 145 | 146 | execService.shutdown(); 147 | boolean didDetectSus = false; 148 | final ArchiveScanResults archRes = new ArchiveScanResults(); 149 | 150 | for (int i = 0; i < tasks; i++) { 151 | try { 152 | final Future result = compService.take(); 153 | final CheckResult finalRes = result.get(); 154 | 155 | if (!finalRes.checkerResults.isEmpty()) { 156 | didDetectSus = true; 157 | 158 | for (final TestResult testRes : finalRes.checkerResults) { 159 | archRes.add(finalRes.fileName, new FileScanResults().add(testRes.result, finalRes.checkerName, testRes.reason, testRes.amount)); 160 | } 161 | } 162 | } catch (final Exception e) { 163 | // TODO better error handling 164 | System.err.println("Exception thrown while running scan task"); 165 | e.printStackTrace(); 166 | } 167 | } 168 | 169 | if (didDetectSus) { 170 | if (json) { 171 | new JSONPrinter(name, archRes).print(pw); 172 | pw.println(); 173 | } else { 174 | new HumanReadablePrinter(name, archRes, level, color).print(pw); 175 | } 176 | } 177 | 178 | pw.flush(); 179 | 180 | if (verbose && !json) { 181 | System.out.println("Finished scanning " + name); 182 | } 183 | 184 | return didDetectSus; 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/ObfuscatorChecker.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.HashMap; 6 | import java.util.HashSet; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | import org.objectweb.asm.Opcodes; 12 | import org.objectweb.asm.tree.AbstractInsnNode; 13 | import org.objectweb.asm.tree.ClassNode; 14 | import org.objectweb.asm.tree.MethodNode; 15 | 16 | import com.github.NeRdTheNed.jSus.detector.checker.TestResult.TestResultLevel; 17 | import com.github.NeRdTheNed.jSus.util.Util; 18 | 19 | // TODO Doesn't really do much 20 | public class ObfuscatorChecker implements IChecker { 21 | private static final Set commonObfNamesList = getCommonObfNamesList(); 22 | private static final Set commonObfNamesListCaseSensitive = getCommonObfNamesListCaseSensitive(); 23 | 24 | private static Set getCommonObfNamesList() { 25 | final Set set = new HashSet<>(); 26 | set.add("con"); 27 | set.add("prn"); 28 | set.add("aux"); 29 | set.add("nul"); 30 | return set; 31 | } 32 | 33 | private static Set getCommonObfNamesListCaseSensitive() { 34 | final Set set = new HashSet<>(); 35 | final String[] reservedJavaNames = { 36 | "abstract", 37 | "assert", 38 | "boolean", 39 | "break", 40 | "byte", 41 | "case", 42 | "catch", 43 | "char", 44 | "class", 45 | "const", 46 | "continue", 47 | "default", 48 | "do", 49 | "double", 50 | "else", 51 | "enum", 52 | "extends", 53 | "final", 54 | "finally", 55 | "float", 56 | "for", 57 | "goto", 58 | "if", 59 | "implements", 60 | "import", 61 | "instanceof", 62 | "int", 63 | "interface", 64 | "long", 65 | "native", 66 | "new", 67 | "package", 68 | "private", 69 | "protected", 70 | "public", 71 | "return", 72 | "short", 73 | "static", 74 | "strictfp", 75 | "super", 76 | "switch", 77 | "synchronized", 78 | "this", 79 | "throw", 80 | "throws", 81 | "transient", 82 | "try", 83 | "void", 84 | "volatile", 85 | "while", 86 | "_", 87 | "true", 88 | "false", 89 | "null", 90 | }; 91 | Collections.addAll(set, reservedJavaNames); 92 | return set; 93 | } 94 | 95 | private static boolean checkChains(int opcode) { 96 | switch (opcode) { 97 | case Opcodes.NOP: 98 | case Opcodes.DUP: 99 | case Opcodes.POP: 100 | case Opcodes.INEG: 101 | case Opcodes.LNEG: 102 | case Opcodes.FNEG: 103 | case Opcodes.DNEG: 104 | case Opcodes.SWAP: 105 | case Opcodes.I2L: 106 | case Opcodes.I2F: 107 | case Opcodes.I2D: 108 | case Opcodes.L2I: 109 | case Opcodes.L2F: 110 | case Opcodes.L2D: 111 | case Opcodes.F2I: 112 | case Opcodes.F2L: 113 | case Opcodes.F2D: 114 | case Opcodes.D2I: 115 | case Opcodes.D2L: 116 | case Opcodes.D2F: 117 | case Opcodes.I2B: 118 | case Opcodes.I2C: 119 | case Opcodes.I2S: 120 | return true; 121 | 122 | default: 123 | return false; 124 | } 125 | } 126 | 127 | private static int chainSize(int opcode) { 128 | switch (opcode) { 129 | case Opcodes.NOP: 130 | return 3; 131 | 132 | case Opcodes.DUP: 133 | case Opcodes.POP: 134 | return 2; 135 | 136 | default: 137 | return 1; 138 | } 139 | } 140 | 141 | @Override 142 | public String getName() { 143 | return "Obfuscator checker"; 144 | } 145 | 146 | @Override 147 | public TestResultLevel getPossibleHighestResult() { 148 | return TestResult.TestResultLevel.BENIGN; 149 | } 150 | 151 | private static void checkName(String name, boolean isClassName, String className, Map foundBenign, Map foundVeryBenign) { 152 | // TODO Find a better balance 153 | final boolean checkVeryShortNameLength = isClassName; 154 | final boolean checkShortNameLength = isClassName; 155 | final char firstChar; 156 | boolean notValidJavaIdent = name.isEmpty() || (!Character.isJavaIdentifierStart(firstChar = name.charAt(0)) && (firstChar != '-')); 157 | 158 | if (!isClassName && notValidJavaIdent && ("".equals(name) || "".equals(name))) { 159 | return; 160 | } 161 | 162 | final int length = name.length(); 163 | 164 | for (int i = 1; !notValidJavaIdent && (i < length); i++) { 165 | final char currentChar = name.charAt(i); 166 | 167 | if (!Character.isJavaIdentifierPart(currentChar) && (currentChar != '$') && (currentChar != '-')) { 168 | notValidJavaIdent = true; 169 | } 170 | } 171 | 172 | /* 173 | if (isClassName && notValidJavaIdent && ("package-info".equals(name) || "module-info".equals(name))) { 174 | return; 175 | } 176 | */ 177 | 178 | if (notValidJavaIdent) { 179 | final String str = isClassName ? "Found common obfuscated classname technique at class " + className : "Found common obfuscated method name technique for method " + name + " at class " + className; 180 | foundBenign.merge(str, 1, Integer::sum); 181 | } else if ((checkVeryShortNameLength && (length == 1)) || commonObfNamesListCaseSensitive.contains(name) || commonObfNamesList.contains(name.toLowerCase())) { 182 | final String str = isClassName ? "Found common obfuscated classname " + className : "Found common obfuscated method name " + name + " at class " + className; 183 | foundBenign.merge(str, 1, Integer::sum); 184 | } else if (checkShortNameLength && (length == 2)) { 185 | final String str = isClassName ? "Class name may be obfuscated " + className : "Method name " + name + " may be obfuscated at class " + className; 186 | foundVeryBenign.merge(str, 1, Integer::sum); 187 | } 188 | } 189 | 190 | // TODO https://github.com/java-deobfuscator/deobfuscator/tree/master/src/main/java/com/javadeobfuscator/deobfuscator/rules 191 | // TODO Handle cases like foo$illegalname$bar 192 | // TODO Check for sequences of bytecode Javac wouldn't produce 193 | // TODO https://docs.oracle.com/javase/8/docs/api/java/lang/Character.html#isJavaIdentifierStart-int- 194 | @Override 195 | public List testClass(ClassNode clazz) { 196 | final List res = new ArrayList<>(); 197 | final Map foundBenign = new HashMap<>(); 198 | final Map foundVeryBenign = new HashMap<>(); 199 | final String className = clazz.name; 200 | final String processedClassName = className.substring(className.lastIndexOf('/') + 1); 201 | checkName(processedClassName, true, className, foundBenign, foundVeryBenign); 202 | final Map chains = new HashMap<>(); 203 | int allatoriDemoCount = 0; 204 | int branchlockCount = 0; 205 | 206 | for (final MethodNode methodNode : clazz.methods) { 207 | if (methodNode.name.contains("ALLATORIxDEMO")) { 208 | allatoriDemoCount++; 209 | } else if (methodNode.name.contains("branchlock.net")) { 210 | branchlockCount++; 211 | } 212 | 213 | checkName(methodNode.name, false, className, foundBenign, foundVeryBenign); 214 | boolean foundChain = false; 215 | int currentChainSize = 0; 216 | int prevOpcode = -1; 217 | 218 | for (final AbstractInsnNode ins : methodNode.instructions) { 219 | final String possibleString = Util.tryComputeConstantString(ins); 220 | 221 | if ((possibleString != null) && possibleString.toLowerCase().contains("branchlock.net")) { 222 | branchlockCount++; 223 | } 224 | 225 | final int opcode = ins.getOpcode(); 226 | 227 | if (opcode != prevOpcode) { 228 | foundChain = false; 229 | currentChainSize = 0; 230 | } else if (checkChains(opcode) && !foundChain) { 231 | currentChainSize++; 232 | 233 | if (currentChainSize >= chainSize(opcode)) { 234 | foundChain = true; 235 | chains.merge(opcode, 1, Integer::sum); 236 | } 237 | } 238 | 239 | prevOpcode = opcode; 240 | } 241 | } 242 | 243 | if (allatoriDemoCount > 0) { 244 | res.add(new TestResult(TestResult.TestResultLevel.BENIGN, "Obfuscator Allatori demo detected at class " + className, allatoriDemoCount)); 245 | } 246 | 247 | if (branchlockCount > 0) { 248 | res.add(new TestResult(TestResult.TestResultLevel.BENIGN, "Obfuscator Branchlock detected at class " + className, branchlockCount)); 249 | } 250 | 251 | foundBenign.forEach((k, v) -> res.add(new TestResult(TestResult.TestResultLevel.BENIGN, k, v))); 252 | foundVeryBenign.forEach((k, v) -> res.add(new TestResult(TestResult.TestResultLevel.VERY_BENIGN, k, v))); 253 | chains.forEach((k, v) -> res.add(new TestResult(TestResult.TestResultLevel.BENIGN, "Unlikely opcode chain of " + Util.opcodeName(k) + " found at " + className, v))); 254 | return res; 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/util/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.util; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | import java.nio.file.StandardCopyOption; 10 | import java.util.ArrayList; 11 | import java.util.Base64; 12 | import java.util.Enumeration; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.jar.Attributes; 17 | import java.util.jar.JarEntry; 18 | import java.util.jar.JarFile; 19 | import java.util.jar.Manifest; 20 | 21 | import org.objectweb.asm.ClassReader; 22 | import org.objectweb.asm.Opcodes; 23 | import org.objectweb.asm.tree.AbstractInsnNode; 24 | import org.objectweb.asm.tree.ClassNode; 25 | import org.objectweb.asm.tree.IntInsnNode; 26 | import org.objectweb.asm.tree.LdcInsnNode; 27 | import org.objectweb.asm.tree.MethodInsnNode; 28 | import org.objectweb.asm.tree.TypeInsnNode; 29 | import org.objectweb.asm.util.Printer; 30 | 31 | import me.coley.cafedude.classfile.ClassFile; 32 | import me.coley.cafedude.io.ClassFileReader; 33 | import me.coley.cafedude.io.ClassFileWriter; 34 | import me.coley.cafedude.transform.IllegalStrippingTransformer; 35 | 36 | public final class Util { 37 | /** Private constructor to hide the default one */ 38 | private Util() { 39 | // This space left intentionally blank 40 | } 41 | 42 | public static final Base64.Decoder decoder = Base64.getDecoder(); 43 | 44 | public static byte[] convertInputStreamToBytes(InputStream in) throws IOException { 45 | final ByteArrayOutputStream result = new ByteArrayOutputStream(); 46 | final byte[] buffer = new byte[1024]; 47 | int length; 48 | 49 | while ((length = in.read(buffer)) != -1) { 50 | result.write(buffer, 0, length); 51 | } 52 | 53 | return result.toByteArray(); 54 | } 55 | 56 | public static ClassNode streamToClass(InputStream stream, String name) throws IOException { 57 | final byte[] classBytes = convertInputStreamToBytes(stream); 58 | return bytesToClass(classBytes, name); 59 | } 60 | 61 | public static ClassNode bytesToClass(byte[] clazz, String name) { 62 | try { 63 | final ClassReader reader = new ClassReader(clazz); 64 | final ClassNode node = new ClassNode(); 65 | reader.accept(node, ClassReader.SKIP_DEBUG); 66 | return node; 67 | } catch (final Exception e) { 68 | System.err.println("Malformed class " + name + ", trying to read with CAFED00D"); 69 | e.printStackTrace(); 70 | } 71 | 72 | try { 73 | final ClassFileReader classFileReader = new ClassFileReader(); 74 | final ClassFile classFile = classFileReader.read(clazz); 75 | // Try to remove junk data that confuses ASM 76 | new IllegalStrippingTransformer(classFile).transform(); 77 | final byte[] fixedClass = new ClassFileWriter().write(classFile); 78 | final ClassReader reader = new ClassReader(fixedClass); 79 | final ClassNode node = new ClassNode(); 80 | reader.accept(node, ClassReader.SKIP_DEBUG); 81 | return node; 82 | } catch (final Exception e) { 83 | System.err.println("Malformed class " + name + ", could not read with CAFED00D"); 84 | e.printStackTrace(); 85 | } 86 | 87 | return null; 88 | } 89 | 90 | public static ClassNode classfileFileToClass(File clazz) { 91 | return classfilePathToClass(clazz.toPath()); 92 | } 93 | 94 | public static ClassNode classfilePathToClass(Path clazz) { 95 | try 96 | (final InputStream is = Files.newInputStream(clazz)) { 97 | return streamToClass(is, clazz.toString()); 98 | } catch (final IOException e) { 99 | System.err.println("Error reading class file " + clazz); 100 | e.printStackTrace(); 101 | } 102 | 103 | return null; 104 | } 105 | 106 | private static void findAddNodes(JarFile jarFile, List nodes, boolean verbose, boolean noLog) { 107 | String obf = null; 108 | boolean didFindWeirdObf = false; 109 | final Enumeration entries = jarFile.entries(); 110 | 111 | while (entries.hasMoreElements()) { 112 | final JarEntry entry = entries.nextElement(); 113 | final String name = entry.getName(); 114 | final boolean weirdClassObf = name.endsWith(".class/") && ((entry.getCompressedSize() > 0L) || (entry.getSize() > 0L)); 115 | 116 | if (entry.isDirectory() && !weirdClassObf) { 117 | continue; 118 | } 119 | 120 | if (weirdClassObf) { 121 | didFindWeirdObf = true; 122 | } 123 | 124 | if (name.endsWith(".jar")) { 125 | if (verbose && !noLog) { 126 | System.out.println("- Adding JIJ " + name + " to scan"); 127 | } 128 | 129 | Path tempFile = null; 130 | 131 | try 132 | (final InputStream is = jarFile.getInputStream(entry)) { 133 | // TODO Not totally sure if this is correct 134 | tempFile = Files.createTempFile("jSus-temp-", ".jar"); 135 | tempFile.toFile().deleteOnExit(); 136 | Files.copy(is, tempFile, StandardCopyOption.REPLACE_EXISTING); 137 | 138 | try 139 | (final JarFile jij = new JarFile(tempFile.toFile())) { 140 | findAddNodes(jij, nodes, verbose, noLog); 141 | } 142 | } catch (final Exception e) { 143 | System.err.println("Issue extracting JIJ " + name + " from " + jarFile.getName() + " to temporary file"); 144 | e.printStackTrace(); 145 | } finally { 146 | if (tempFile != null) { 147 | try { 148 | Files.deleteIfExists(tempFile); 149 | } catch (final Exception e) { 150 | System.err.println("Issue deleting temporary file " + tempFile); 151 | e.printStackTrace(); 152 | } 153 | } 154 | } 155 | } 156 | 157 | if (weirdClassObf || name.endsWith(".class")) { 158 | try 159 | (final InputStream is = jarFile.getInputStream(entry)) { 160 | final ClassNode node = streamToClass(is, name); 161 | 162 | if (node != null) { 163 | nodes.add(node); 164 | } else { 165 | System.err.println("Class node was null: " + name); 166 | } 167 | } catch (final IOException e) { 168 | System.err.println("Error reading class " + entry.getName() + " from jar " + jarFile.getName()); 169 | e.printStackTrace(); 170 | } 171 | } 172 | } 173 | 174 | if (obf == null) { 175 | try { 176 | final Manifest jarManifest = jarFile.getManifest(); 177 | 178 | if (jarManifest != null) { 179 | final Attributes attrib = jarManifest.getMainAttributes(); 180 | obf = attrib.getValue("Obfuscated-By"); 181 | 182 | if (obf == null) { 183 | obf = attrib.getValue("Protected-By"); 184 | } 185 | } 186 | } catch (final IOException e) { 187 | System.err.println("Could not get manifest for jar " + jarFile.getName()); 188 | e.printStackTrace(); 189 | } 190 | } 191 | 192 | if ((obf != null) && !noLog) { 193 | System.out.println("- Note: jar " + jarFile.getName() + " claims to be obfuscated by " + obf); 194 | } 195 | 196 | if (didFindWeirdObf && !noLog) { 197 | System.out.println("- Note: Common class obfuscation technique was used in jar " + jarFile.getName()); 198 | } 199 | } 200 | 201 | public static List gatherClassNodesFromJar(JarFile jarFile, boolean verbose, boolean noLog) { 202 | final List nodes = new ArrayList<>(); 203 | findAddNodes(jarFile, nodes, verbose, noLog); 204 | return nodes; 205 | } 206 | 207 | public static String opcodeName(int opcode) { 208 | return (opcode >= 0) && (opcode < Printer.OPCODES.length) ? Printer.OPCODES[opcode] : Integer.toString(opcode); 209 | } 210 | 211 | public static boolean isOpcodeMethodInvoke(int opcode) { 212 | return (opcode <= Opcodes.INVOKEINTERFACE) && (opcode >= Opcodes.INVOKEVIRTUAL); 213 | } 214 | 215 | public static boolean isCommonBase64DecodeMethod(int opcode, String owner, String name, String signature) { 216 | if (!"(Ljava/lang/String;)[B".equals(signature)) { 217 | return false; 218 | } 219 | 220 | // TODO handle org.apache.commons.codec.binary.Base16 221 | // TODO handle org.apache.commons.codec.binary.Base32 222 | // TODO handle org.apache.commons.codec.binary.Hex 223 | return ("java/util/Base64$Decoder".equals(owner) && "decode".equals(name)) || 224 | ("org/apache/commons/codec/binary/Base64".equals(owner) && "decodeBase64".equals(name)) || 225 | ("javax/xml/bind/DatatypeConverter".equals(owner) && "parseBase64Binary".equals(name)); 226 | } 227 | 228 | public static boolean isCommonBase64DecodeBytesToBytesMethod(int opcode, String owner, String name, String signature) { 229 | if (!"([B)[B".equals(signature)) { 230 | return false; 231 | } 232 | 233 | // TODO Support more methods 234 | return ("java/util/Base64$Decoder".equals(owner) && "decode".equals(name)) || 235 | ("org/apache/commons/codec/binary/Base64".equals(owner) && "decodeBase64".equals(name)); 236 | } 237 | 238 | private static Number getValueOrNull(AbstractInsnNode load) { 239 | switch (load.getOpcode()) { 240 | case Opcodes.ICONST_M1: 241 | return -1; 242 | 243 | case Opcodes.ICONST_0: 244 | return 0; 245 | 246 | case Opcodes.ICONST_1: 247 | return 1; 248 | 249 | case Opcodes.ICONST_2: 250 | return 2; 251 | 252 | case Opcodes.ICONST_3: 253 | return 3; 254 | 255 | case Opcodes.ICONST_4: 256 | return 4; 257 | 258 | case Opcodes.ICONST_5: 259 | return 5; 260 | 261 | case Opcodes.LCONST_0: 262 | return 0L; 263 | 264 | case Opcodes.LCONST_1: 265 | return 1L; 266 | 267 | case Opcodes.FCONST_0: 268 | return 0.0f; 269 | 270 | case Opcodes.FCONST_1: 271 | return 1.0f; 272 | 273 | case Opcodes.FCONST_2: 274 | return 2.0f; 275 | 276 | case Opcodes.DCONST_0: 277 | return 0.0d; 278 | 279 | case Opcodes.DCONST_1: 280 | return 1.0d; 281 | 282 | case Opcodes.BIPUSH: 283 | case Opcodes.SIPUSH: 284 | return ((IntInsnNode) load).operand; 285 | 286 | case Opcodes.LDC: 287 | final LdcInsnNode ldc = (LdcInsnNode) load; 288 | 289 | if (ldc.cst instanceof Number) { 290 | return (Number) ldc.cst; 291 | } 292 | 293 | // Fall through 294 | 295 | default: 296 | return null; 297 | } 298 | } 299 | 300 | private static Pair getArrayConstructPoint(AbstractInsnNode postInit, Map indexToByte) { 301 | if (postInit != null) { 302 | final AbstractInsnNode newArrayIns = postInit.getPrevious(); 303 | 304 | if ((newArrayIns != null) && (newArrayIns.getOpcode() == Opcodes.NEWARRAY)) { 305 | final IntInsnNode newArray = (IntInsnNode) newArrayIns; 306 | 307 | if (newArray.operand == Opcodes.T_BYTE) { 308 | final AbstractInsnNode newArrayLengthIns = newArrayIns.getPrevious(); 309 | 310 | if (newArrayLengthIns != null) { 311 | final Number value = getValueOrNull(newArrayLengthIns); 312 | 313 | if (value != null) { 314 | final byte[] computedBytes = new byte[value.intValue()]; 315 | indexToByte.forEach((k, v) -> { 316 | if (k < computedBytes.length) { 317 | computedBytes[k] 318 | = v; 319 | } else { 320 | System.err.println("grievous error: index out of bounds when reconstructing fixed byte array, index " + k + " value " + v); 321 | } 322 | }); 323 | return new Pair<>(newArrayLengthIns, computedBytes); 324 | } 325 | } 326 | } 327 | } 328 | } 329 | 330 | return new Pair<>(null, null); 331 | } 332 | 333 | private static Pair tryComputeArray(AbstractInsnNode arrayOnStack) { 334 | if (arrayOnStack == null) { 335 | return new Pair<>(null, null); 336 | } 337 | 338 | final int opcode = arrayOnStack.getOpcode(); 339 | 340 | if (opcode == Opcodes.BASTORE) { 341 | // This matches the pattern that javac uses to construct Strings from code like 342 | // new String(new byte[] { some, bytes, ect }); 343 | // The code is Not Good 344 | final Map indexToByte = new HashMap<>(); 345 | AbstractInsnNode storeNextByte = arrayOnStack; 346 | 347 | while ((storeNextByte != null) && (storeNextByte.getOpcode() == Opcodes.BASTORE)) { 348 | final AbstractInsnNode valueIns = storeNextByte.getPrevious(); 349 | 350 | if (valueIns == null) { 351 | break; 352 | } 353 | 354 | final Number value = getValueOrNull(valueIns); 355 | 356 | if (value == null) { 357 | break; 358 | } 359 | 360 | final AbstractInsnNode indexIns = valueIns.getPrevious(); 361 | 362 | if (indexIns == null) { 363 | break; 364 | } 365 | 366 | final Number index = getValueOrNull(indexIns); 367 | 368 | if (index == null) { 369 | break; 370 | } 371 | 372 | final AbstractInsnNode dup = indexIns.getPrevious(); 373 | 374 | if ((dup == null) || (dup.getOpcode() != Opcodes.DUP)) { 375 | break; 376 | } 377 | 378 | indexToByte.putIfAbsent(index.intValue(), value.byteValue()); 379 | storeNextByte = dup.getPrevious(); 380 | } 381 | 382 | if (storeNextByte != null) { 383 | return getArrayConstructPoint(storeNextByte.getNext(), indexToByte); 384 | } 385 | } else if (isOpcodeMethodInvoke(opcode)) { 386 | final MethodInsnNode methodInsNode = (MethodInsnNode) arrayOnStack; 387 | final String methodOwner = methodInsNode.owner; 388 | final String methodName = methodInsNode.name; 389 | final String methodDesc = methodInsNode.desc; 390 | 391 | if (isCommonBase64DecodeBytesToBytesMethod(opcode, methodOwner, methodName, methodDesc)) { 392 | final Pair computedArray = tryComputeArray(arrayOnStack.getPrevious()); 393 | 394 | if (computedArray.v != null) { 395 | AbstractInsnNode postInit = null; 396 | 397 | if (opcode == Opcodes.INVOKESTATIC) { 398 | postInit = computedArray.k; 399 | } else if (computedArray.k != null) { 400 | final AbstractInsnNode prev = computedArray.k.getPrevious(); 401 | 402 | if (prev != null) { 403 | final int prevOpcode = prev.getOpcode(); 404 | 405 | if (isOpcodeMethodInvoke(prevOpcode)) { 406 | final MethodInsnNode prevMethodInsNode = (MethodInsnNode) prev; 407 | final String prevMethodOwner = prevMethodInsNode.owner; 408 | final String prevMethodName = prevMethodInsNode.name; 409 | final String prevMethodDesc = prevMethodInsNode.desc; 410 | 411 | if ((prevOpcode == Opcodes.INVOKESTATIC) 412 | && "java/util/Base64".equals(prevMethodOwner) 413 | && "getDecoder".equals(prevMethodName) 414 | && "()Ljava/util/Base64$Decoder;".equals(prevMethodDesc)) { 415 | postInit = prev; 416 | } 417 | } else if (prevOpcode == Opcodes.ALOAD) { 418 | postInit = prev; 419 | } 420 | } 421 | } 422 | 423 | try { 424 | final byte[] decoded = decoder.decode(computedArray.v); 425 | return new Pair<>(postInit, decoded); 426 | } catch (final IllegalArgumentException e) { 427 | // Invalid Base64? 428 | } 429 | } 430 | } 431 | } 432 | 433 | return new Pair<>(arrayOnStack, null); 434 | } 435 | 436 | public static byte[] tryComputeConstantBytes(AbstractInsnNode stringOnStack) { 437 | return tryComputeArray(stringOnStack).v; 438 | } 439 | 440 | private static Pair getStringConstructPoint(AbstractInsnNode postInit, String str) { 441 | if (postInit != null) { 442 | final AbstractInsnNode firstDup = postInit.getPrevious(); 443 | 444 | if ((firstDup != null) && (firstDup.getOpcode() == Opcodes.DUP)) { 445 | final AbstractInsnNode firstNewIns = firstDup.getPrevious(); 446 | 447 | if ((firstNewIns != null) && (firstNewIns.getOpcode() == Opcodes.NEW)) { 448 | final TypeInsnNode firstNew = (TypeInsnNode) firstNewIns; 449 | 450 | if ("java/lang/String".equals(firstNew.desc)) { 451 | return new Pair<>(firstNew, str); 452 | } 453 | } 454 | } 455 | } 456 | 457 | return new Pair<>(null, str); 458 | } 459 | 460 | // Returns the computed value of a String constant at the earliest point possible 461 | // (e.g. if two Strings are concatenated, at the start of the concatenation, 462 | // if a String is constructed from a byte array, at the point where NEW java/lang/String is called). 463 | // This is to support figuring out where a previous stack value is (for String concatenations ect). 464 | // If the String is null, it was unable to be computed at the given point, 465 | // either because it can't be determined, or my code doesn't support figuring it out yet. 466 | private static Pair tryComputeString(AbstractInsnNode stringOnStack) { 467 | if (stringOnStack == null) { 468 | return new Pair<>(null, null); 469 | } 470 | 471 | final int opcode = stringOnStack.getOpcode(); 472 | 473 | if (opcode == Opcodes.LDC) { 474 | final LdcInsnNode ldc = (LdcInsnNode) stringOnStack; 475 | 476 | if (ldc.cst instanceof String) { 477 | return new Pair<>(stringOnStack, (String) ldc.cst); 478 | } 479 | } 480 | 481 | // TODO Real analysis 482 | if (isOpcodeMethodInvoke(opcode)) { 483 | final MethodInsnNode methodInsNode = (MethodInsnNode) stringOnStack; 484 | final String methodOwner = methodInsNode.owner; 485 | final String methodName = methodInsNode.name; 486 | final String methodDesc = methodInsNode.desc; 487 | 488 | // TODO Support more methods 489 | if ((opcode == Opcodes.INVOKEVIRTUAL) 490 | && "java/lang/String".equals(methodOwner) 491 | && "concat".equals(methodName) 492 | && "(Ljava/lang/String;)Ljava/lang/String;".equals(methodDesc)) { 493 | final Pair firstPair = tryComputeString(stringOnStack.getPrevious()); 494 | 495 | if ((firstPair.v != null) && (firstPair.k != null)) { 496 | final Pair secondPair = tryComputeString(firstPair.k.getPrevious()); 497 | 498 | if (secondPair.v != null) { 499 | return new Pair<>(secondPair.k, secondPair.v + firstPair.v); 500 | } 501 | } 502 | } 503 | 504 | if ((opcode == Opcodes.INVOKESPECIAL) 505 | && "java/lang/String".equals(methodOwner) 506 | && "".equals(methodName) 507 | && "([B)V".equals(methodDesc)) { 508 | final AbstractInsnNode prev = stringOnStack.getPrevious(); 509 | 510 | if (prev != null) { 511 | final int prevOpcode = prev.getOpcode(); 512 | 513 | if (isOpcodeMethodInvoke(prevOpcode)) { 514 | final MethodInsnNode possibleBase64 = (MethodInsnNode) prev; 515 | final String prevMethodOwner = possibleBase64.owner; 516 | final String prevMethodName = possibleBase64.name; 517 | final String prevMethodDesc = possibleBase64.desc; 518 | 519 | if (isCommonBase64DecodeMethod(prevOpcode, prevMethodOwner, prevMethodName, prevMethodDesc)) { 520 | final Pair passedBase64 = tryComputeString(prev.getPrevious()); 521 | 522 | if (passedBase64.v != null) { 523 | AbstractInsnNode postInit = null; 524 | 525 | if (prevOpcode == Opcodes.INVOKESTATIC) { 526 | postInit = passedBase64.k; 527 | } else if (passedBase64.k != null) { 528 | final AbstractInsnNode preBase64 = passedBase64.k.getPrevious(); 529 | 530 | if (preBase64 != null) { 531 | final int preBase64Opcode = preBase64.getOpcode(); 532 | 533 | if (isOpcodeMethodInvoke(preBase64Opcode)) { 534 | final MethodInsnNode prevMethodInsNode = (MethodInsnNode) preBase64; 535 | final String preBase64MethodOwner = prevMethodInsNode.owner; 536 | final String preBase64MethodName = prevMethodInsNode.name; 537 | final String preBase64MethodDesc = prevMethodInsNode.desc; 538 | 539 | if ((preBase64Opcode == Opcodes.INVOKESTATIC) 540 | && "java/util/Base64".equals(preBase64MethodOwner) 541 | && "getDecoder".equals(preBase64MethodName) 542 | && "()Ljava/util/Base64$Decoder;".equals(preBase64MethodDesc)) { 543 | postInit = preBase64; 544 | } 545 | } else if (preBase64Opcode == Opcodes.ALOAD) { 546 | postInit = preBase64; 547 | } 548 | } 549 | } 550 | 551 | final String possibleString = passedBase64.v; 552 | 553 | try { 554 | final String decoded = new String(decoder.decode(possibleString)); 555 | return getStringConstructPoint(postInit, decoded); 556 | } catch (final IllegalArgumentException e) { 557 | // Invalid Base64? 558 | } 559 | } 560 | } 561 | } 562 | 563 | final Pair computedArray = tryComputeArray(prev); 564 | 565 | if (computedArray.v != null) { 566 | final String str = new String(computedArray.v); 567 | return getStringConstructPoint(computedArray.k, str); 568 | } 569 | } 570 | } 571 | 572 | if ((opcode == Opcodes.INVOKESPECIAL) 573 | && "java/lang/String".equals(methodOwner) 574 | && "".equals(methodName) 575 | && "(Ljava/lang/String;)V".equals(methodDesc)) { 576 | final Pair prevString = tryComputeString(stringOnStack.getPrevious()); 577 | return getStringConstructPoint(prevString.k, prevString.v); 578 | } 579 | } 580 | 581 | return new Pair<>(stringOnStack, null); 582 | } 583 | 584 | public static String tryComputeConstantString(AbstractInsnNode stringOnStack) { 585 | return tryComputeString(stringOnStack).v; 586 | } 587 | } 588 | -------------------------------------------------------------------------------- /src/main/java/com/github/NeRdTheNed/jSus/detector/checker/Checkers.java: -------------------------------------------------------------------------------- 1 | package com.github.NeRdTheNed.jSus.detector.checker; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.regex.Pattern; 7 | 8 | public class Checkers { 9 | 10 | public static final List checkerList = makeCheckerList(); 11 | 12 | private static void addGenericRatChecker(List list) { 13 | final HashMap susMap = new HashMap<>(); 14 | final String[] strongSusStrings = { 15 | "/.metadata/.plugins/org.eclipse.buildship.ui/dialog_settings.xml", 16 | "/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/Local Storage/leveldb/", 17 | "/AppData/Local/Google/Chrome/User Data/Default/Local Storage/leveldb/", 18 | "/AppData/Local/Microsoft/Edge/User Data/Default/Local Storage/leveldb/", 19 | "/AppData/Local/Vivaldi/User Data/Default/Local Storage/leveldb/", 20 | "/AppData/Local/Yandex/YandexBrowser/User Data/Default/Local Storage/leveldb/", 21 | "/AppData/Roaming/BraveSoftware/Brave-Browser/User Data/Default/Local Storage/leveldb/", 22 | "/AppData/Roaming/discord/Local Storage/leveldb/", 23 | "/AppData/Roaming/discordcanary/Local Storage/leveldb/", 24 | "/AppData/Roaming/discordptb/Local Storage/leveldb/", 25 | "/AppData/Roaming/FileZilla/recentservers.xml", 26 | "/AppData/Roaming/JetBrains/", 27 | "/AppData/Roaming/Mozilla/Firefox/Profiles/", 28 | "/AppData/Roaming/Opera Software/Opera Stable/Local Storage/leveldb/", 29 | "/AppData/Roaming/Yandex/YandexBrowser/User Data/Default/Local Storage/leveldb/", 30 | "/eclipse/configuration/.settings/org.eclipse.ui.ide.prefs", 31 | "/Library/Application Support/discord/Local Storage/leveldb/", 32 | "/Library/Application Support/discordcanary/Local Storage/leveldb/", 33 | "/Library/Application Support/discordptb/Local Storage/leveldb/", 34 | "/Library/Application Support/Firefox/Profiles/", 35 | "/Library/Application Support/Google/Chrome/User Data/Default/Local Storage/leveldb/", 36 | "/options/recentProjects.xml", 37 | 38 | "ps -e -o command", 39 | "wmic process get name,executablepath", 40 | 41 | "[\\w\\W]{24}\\.[\\w\\W]{6}\\.[\\w\\W]{27}|mfa\\.[\\w\\W]{84}", 42 | "\\BraveSoftware\\Brave-Browser\\User Data\\Default", 43 | "\\Future\\accounts.txt", 44 | "\\Google\\Chrome\\User Data\\Default", 45 | "\\Google\\Chrome\\User Data\\Default\\Login Data", 46 | "\\LightCord", 47 | "\\Local Storage\\leveldb\\", 48 | "\\Microsoft\\Edge\\User Data\\Default", 49 | "\\Mozilla\\Firefox\\Profiles", 50 | "\\Opera Software\\Opera Stable", 51 | "\\Yandex\\YandexBrowser\\User Data\\Default", 52 | "$USER_HOME$", 53 | 54 | "/.config/BraveSoftware/Brave-Browser/Default/Login Data", 55 | "/.config/BraveSoftware/Brave-Browser/Local State", 56 | "/.config/google-chrome/Default/Login Data", 57 | "/.config/google-chrome/Local State", 58 | "/.config/Microsoft/Edge/Default/Login Data", 59 | "/.config/Microsoft/Edge/Local State", 60 | "/.config/opera/Default/Login Data", 61 | "/.config/opera/Local State", 62 | "/.config/Yandex/YandexBrowser/Default/Login Data", 63 | "/.config/Yandex/YandexBrowser/Local State", 64 | "/BraveSoftware/Brave-Browser/Default/Login Data", 65 | "/BraveSoftware/Brave-Browser/Local State", 66 | "/Google/Chrome/User Data/Default/Login Data", 67 | "/Google/Chrome/User Data/Local State", 68 | "/Library/Application Support/BraveSoftware/Brave-Browser/Default/Login Data", 69 | "/Library/Application Support/BraveSoftware/Brave-Browser/Local State", 70 | "/Library/Application Support/Google/Chrome/User Data/Default/Login Data", 71 | "/Library/Application Support/Google/Chrome/User Data/Local State", 72 | "/Library/Application Support/Microsoft/Edge/Default/Login Data", 73 | "/Library/Application Support/Microsoft/Edge/Local State", 74 | "/Library/Application Support/Opera/Opera/Default/Login Data", 75 | "/Library/Application Support/Opera/Opera/Local State", 76 | "/Library/Application Support/Yandex/YandexBrowser/Default/Login Data", 77 | "/Library/Application Support/Yandex/YandexBrowser/Local State", 78 | "/Microsoft/Edge/User Data/Default/Login Data", 79 | "/Microsoft/Edge/User Data/Local State", 80 | "/Opera Software/Opera Stable/Default/Login Data", 81 | "/Opera Software/Opera Stable/Local State", 82 | "/Yandex/YandexBrowser/User Data/Default/Login Data", 83 | "/Yandex/YandexBrowser/User Data/Local State", 84 | "SELECT `origin_url`,`username_value`,`password_value` from `logins`", 85 | 86 | "/.config/BraveSoftware/Brave-Browser/Default/Cookies", 87 | "/.config/BraveSoftware/Brave-Browser/Default/Network/Cookies", 88 | "/.config/BraveSoftware/Brave-Browser/Default/Web Data", 89 | "/.config/google-chrome/Default/Cookies", 90 | "/.config/google-chrome/Default/Network/Cookies", 91 | "/.config/google-chrome/Default/Web Data", 92 | "/.config/Microsoft/Edge/Default/Cookies", 93 | "/.config/Microsoft/Edge/Default/Network/Cookies", 94 | "/.config/Microsoft/Edge/Default/Web Data", 95 | "/.config/opera/Default/Cookies", 96 | "/.config/opera/Default/Network/Cookies", 97 | "/.config/opera/Default/Web Data", 98 | "/.config/Yandex/YandexBrowser/Default/Cookies", 99 | "/.config/Yandex/YandexBrowser/Default/Network/Cookies", 100 | "/.config/Yandex/YandexBrowser/Default/Web Data", 101 | "/BraveSoftware/Brave-Browser/Default/Cookies", 102 | "/BraveSoftware/Brave-Browser/Default/Network/Cookies", 103 | "/BraveSoftware/Brave-Browser/Default/Web Data", 104 | "/Google/Chrome/User Data", 105 | "/Google/Chrome/User Data/Default/Cookies", 106 | "/Google/Chrome/User Data/Default/Network/Cookies", 107 | "/Google/Chrome/User Data/Default/Web Data", 108 | "/Library/Application Support/BraveSoftware/Brave-Browser/Default/Cookies", 109 | "/Library/Application Support/BraveSoftware/Brave-Browser/Default/Network/Cookies", 110 | "/Library/Application Support/BraveSoftware/Brave-Browser/Default/Web Data", 111 | "/Library/Application Support/Google/Chrome/User Data/Default/Cookies", 112 | "/Library/Application Support/Google/Chrome/User Data/Default/Network/Cookies", 113 | "/Library/Application Support/Google/Chrome/User Data/Default/Web Data", 114 | "/Library/Application Support/Microsoft/Edge/Default/Cookies", 115 | "/Library/Application Support/Microsoft/Edge/Default/Network/Cookies", 116 | "/Library/Application Support/Microsoft/Edge/Default/Web Data", 117 | "/Library/Application Support/Opera/Opera/Default/Cookies", 118 | "/Library/Application Support/Opera/Opera/Default/Network/Cookies", 119 | "/Library/Application Support/Opera/Opera/Default/Web Data", 120 | "/Library/Application Support/Yandex/YandexBrowser/Default/Cookies", 121 | "/Library/Application Support/Yandex/YandexBrowser/Default/Network/Cookies", 122 | "/Library/Application Support/Yandex/YandexBrowser/Default/Web Data", 123 | "/Microsoft/Edge/User Data/Default/Cookies", 124 | "/Microsoft/Edge/User Data/Default/Network/Cookies", 125 | "/Microsoft/Edge/User Data/Default/Web Data", 126 | "/Opera Software/Opera Stable/Default/Cookies", 127 | "/Opera Software/Opera Stable/Default/Network/Cookies", 128 | "/Opera Software/Opera Stable/Default/Web Data", 129 | "/Yandex/YandexBrowser/User Data/Default/Cookies", 130 | "/Yandex/YandexBrowser/User Data/Default/Network/Cookies", 131 | "/Yandex/YandexBrowser/User Data/Default/Web Data", 132 | "\\recentservers.xml", 133 | 134 | "com.liberty.jaxx\\IndexedDB\\file__0.indexeddb.leveldb\\", 135 | 136 | "config\\loginusers.vdf", 137 | 138 | "FileZilla/recentservers.xml", 139 | 140 | "HKCU\\Software\\Bitcoin\\Bitcoin-Qt", 141 | "HKCU\\Software\\Dash\\Dash-Qt", 142 | "HKCU\\Software\\Litecoin\\Litecoin-Qt", 143 | "HKCU\\Software\\monero-project\\monero-core", 144 | "HKLM\\SOFTWARE\\WOW6432Node\\Valve\\Steam", 145 | 146 | "SELECT * from `credit_cards`", 147 | "SELECT `host_key`,`name`,`path`,`encrypted_value`,`expires_utc` from `cookies`", 148 | "SELECT date_created,date_last_used,name,value,count from `autofill` ORDER BY date_created", 149 | "Steam/config/", 150 | "Telegram Desktop\\tdata", 151 | 152 | "tasklist.exe", 153 | "wireshark", 154 | 155 | "/.config/discord/Cache/Local Storage/leveldb/", 156 | "/.config/discordcanary/Cache/Local Storage/leveldb/", 157 | "/.config/discordptb/Cache/Local Storage/leveldb/", 158 | "/AppData/Local/Google/Chrome/User Data/", 159 | "C:\\Users\\", 160 | 161 | "[\\w]{24}\\.[\\w]{6}\\.[\\w]{27}", 162 | "mfa\\.[\\w-]{84}", 163 | 164 | "https://discordapp.com/api/v6/users/@me/billing/payment-sources", 165 | 166 | "aHR0cHM6Ly9hcGkubWluZWNyYWZ0Zm9yY2VvcC5jb20vbmFtZS5waHA/cG9ydD0=", 167 | "aHR0cHM6Ly9hcGkubWluZWNyYWZ0Zm9yY2VvcC5jb20vZG93bmxvYWQucGhwP3BvcnQ9", 168 | }; 169 | 170 | for (final String susString : strongSusStrings) { 171 | susMap.put(susString, TestResult.TestResultLevel.STRONG_SUS); 172 | } 173 | 174 | final String[] susStrings = { 175 | ".wallet", 176 | "[\\w\\.]{24}\\.[\\w\\.]{6}\\.[\\w\\.\\-]{27}|mfa\\.[\\w\\.\\-]{84}", 177 | "[nNmM][\\w\\W]{23}\\.[xX][\\w\\W]{5}\\.[\\w\\W]{27}|mfa\\.[\\w\\W]{84}", 178 | "/Future/accounts.txt", 179 | "/Future/auth_key", 180 | "/Future/waypoints.txt", 181 | "\"76(.*?)\"", 182 | 183 | "\\.minecraft\\Pyro\\server", 184 | "\\.minecraft\\SalHack\\Waypoints\\Waypoints.json", 185 | "\\Documents\\ShareX\\", 186 | "\\Future\\backup", 187 | "\\wallet.dat", 188 | 189 | "Armory\\", 190 | "atomic\\Local Storage\\leveldb\\", 191 | "bytecoin\\", 192 | "Crypto/Armory", 193 | "Crypto/AtomicWallet", 194 | "Crypto/BitcoinCore", 195 | "Crypto/Bytecoin", 196 | "Crypto/DashCore", 197 | "Crypto/Electrum", 198 | "Crypto/Ethereum", 199 | "Crypto/Exodus", 200 | "Crypto/Jaxx", 201 | "Crypto/LitecoinCore", 202 | "Crypto/MoneroCore", 203 | "Crypto/Zcash", 204 | "dQw4w9WgXcQ:[^\"]*", 205 | "Electrum\\wallets\\", 206 | "Ethereum\\keystore\\", 207 | "Exodus\\exodus.wallet\\", 208 | "https://discordapp.com/api/v7/invites/minecraft", 209 | "https://steamcommunity.com/profiles/", 210 | "KAMIBlueWaypoints.json", 211 | "Pyro/alts.json", 212 | "Pyro/launcher.json", 213 | "Pyro/server/", 214 | "Pyro\\alts.json", 215 | "rusherhack\\alts.json", 216 | "rusherhack\\waypoints.json", 217 | "strDataDir", 218 | "wallet_path", 219 | "Zcash\\", 220 | }; 221 | 222 | for (final String susString : susStrings) { 223 | susMap.put(susString, TestResult.TestResultLevel.SUS); 224 | } 225 | 226 | final String[] begignStrings = { 227 | // Netty 228 | "\\AppData\\Local\\Temp", 229 | "http://checkip.amazonaws.com", 230 | "https://wtfismyip.com/text", 231 | //"launcher_profiles.json" 232 | //"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.11 Safari/537.36" 233 | "https://discordapp.com/api/v6/users/@me", 234 | "\\discordcanary", 235 | "\\discordptb", 236 | "webappsstore", 237 | "\\.minecraft\\journeymap", 238 | "dQw4w9WgXcQ:" 239 | }; 240 | 241 | for (final String begignString : begignStrings) { 242 | susMap.put(begignString, TestResult.TestResultLevel.BENIGN); 243 | } 244 | 245 | final HashMap susPatternMap = new HashMap<>(); 246 | susPatternMap.put(Pattern.compile("/users/@me/billing"), TestResult.TestResultLevel.STRONG_SUS); 247 | susPatternMap.put(Pattern.compile("AppData"), TestResult.TestResultLevel.SUS); 248 | susPatternMap.put(Pattern.compile("Application Support"), TestResult.TestResultLevel.SUS); 249 | susPatternMap.put(Pattern.compile("Default[/|\\\\](Login Data|Local Storage|Web Data|Cookies|Network)"), TestResult.TestResultLevel.STRONG_SUS); 250 | susPatternMap.put(Pattern.compile("User Data[/|\\\\](Local State|Default)"), TestResult.TestResultLevel.STRONG_SUS); 251 | susPatternMap.put(Pattern.compile("Local Storage[/|\\\\]leveldb"), TestResult.TestResultLevel.STRONG_SUS); 252 | // Regex from https://github.com/MinnDevelopment/discord-webhooks/blob/bbbd1e0a7ff1bdeef64df3d7a769105e118a60af/src/main/java/club/minnced/discord/webhook/WebhookClientBuilder.java#L46 253 | susPatternMap.put(Pattern.compile("(?:https?://)?(?:\\w+\\.)?discord(?:app)?\\.com/api(?:/v\\d+)?/webhooks/(\\d+)/([\\w-]+)(?:/(?:\\w+)?)?"), TestResult.TestResultLevel.SUS); 254 | susPatternMap.put(Pattern.compile("api\\.minecraftforceop\\.com"), TestResult.TestResultLevel.STRONG_SUS); 255 | susPatternMap.put(Pattern.compile("/bin/sh"), TestResult.TestResultLevel.STRONG_SUS); 256 | susPatternMap.put(Pattern.compile("cmd /c start"), TestResult.TestResultLevel.STRONG_SUS); 257 | susPatternMap.put(Pattern.compile("cmd.exe"), TestResult.TestResultLevel.STRONG_SUS); 258 | susPatternMap.put(Pattern.compile("java -jar"), TestResult.TestResultLevel.STRONG_SUS); 259 | susPatternMap.put(Pattern.compile("Java-DiscordWebhook-BY-Gelox"), TestResult.TestResultLevel.SUS); 260 | final StringChecker susTest = new StringChecker("Possible RAT / stealer", susMap, susPatternMap); 261 | list.add(susTest); 262 | } 263 | 264 | private static void addYoinkRatChecker(List list) { 265 | final HashMap susMap = new HashMap<>(); 266 | final String[] virusStrings = { 267 | // Moneyrat 268 | ".apiloader.APILoader", 269 | "net.minecraftforge:apiloader:1.0.4", 270 | " --tweakClass net.minecraftforge.apiloader.APILoader", 271 | "net.minecraftforge.apiloader.APILoader", 272 | "libraries/net/minecraftforge/apiloader/1.0.4/apiloader-1.0.4.jar", 273 | "APILoader - 28 Dec 2020\n\n", 274 | // v1 275 | "--tweakClass net.minecraftforge.apiloader.APILoader", 276 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvNzk5MDk5MzU4MDg3MzQ4MzE1L1VyTjQtV2psTDBsQUJNckF3dVpiMUdUUDJkUFdJbG1jR0JEbmJKalFYMmhwdE4taF8xXzBfeUxROXlSR0JNeGc4X0dK", 277 | "http://yoink.site/ukraine/rat.jar", 278 | "the rat is running out of the game", 279 | "https://cdn-107.anonfiles.com/xe9fG219y7/1a2d8176-1659361918/build.exe", 280 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvMTAwODQzNTUwNzc5MDg5MzA2Ny9GTjVoYmhjWkJNY183ZnVrR1MyX3JjQnFWWkc3dDhGaURlZlBrVGpIZVRkRFhrU0liWEh6Z3plQmhpcWd1SUVUV0QwRw==", 281 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvODMwNTQ5ODg5NzgzNTYyMjcwL193OW8zQlgyck1zNzl2ZmMtdGNWWm9BZlYtLTVxQXJDdFFhbEJBZ09VMzdHZ3J3SENyLWgzdXVLVmVZeWJnY3g1N20t", 282 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvODMwNTgwNjUzNjA2Njk5MDIwLzROaFBRcWlhY3NKREFOYnZ5YlFxMFZDZTJWOE5OaldwLUl1SXR6cF96QVZzZG54SVRwWUdIbkF5LVhHb1Rqb29vc0Zo", 283 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvODMwNTgwNzczNTM0MzAyMjE5L1ZLd2dlUzh0NnQtTlJ2ZTFGN19HOTdGR29FQUxIdnoxb3VrbmdhU2lVV2dPYThGalZCbE4xS2dnZEVhbFJWYlRVNW9V", 284 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvODMwNTgwODY1NjQxMDg3MDI3Ly1MN21lWFlJZDJTVHVYbVhsYmFoczd1ekFJTVEyQWlBU2NyQUIwbVVQbFI1dHJlOVF0dUdHYUhFUm02bFpVUEU1bjQy", 285 | "aHR0cHM6Ly9kaXNjb3JkLmNvbS9hcGkvd2ViaG9va3MvODMwNTgwOTczMDE5NDYzNzMxL0UteUNKTnF1ODhrT2g4VDZxdVJHbEFMYmR0ZTFLSW5LMU9uZkw2cS1rZklSM2dWSks3WXR0WXZYQ2xfVVFaa3NpLUNS", 286 | }; 287 | 288 | for (final String virusString : virusStrings) { 289 | susMap.put(virusString, TestResult.TestResultLevel.VIRUS); 290 | } 291 | 292 | final String[] strongSusStrings = { 293 | // Moneyrat 294 | "\nAdditional discord data:\n", 295 | //"\nDesktop folder:\n" 296 | "\nDiscord token[s]: \n", 297 | //"\nDownloads folder:\n" 298 | "\nEclipse workspaces:\n", 299 | "\nFileZilla hosts:\n", 300 | "\nFuture accounts: \n", 301 | "\nFuture loader credentials:\n", 302 | "\nFuture waypoints: \n", 303 | "\nIntellij workspaces:\n", 304 | "\nJourneyMap waypoints: \n", 305 | "\nKAMI Blue waypoints: \n", 306 | "\nMinecraft data: \n", 307 | //"\nMinecraft mods:\n" 308 | "\nMultiMC accounts.json: \n", 309 | "\nPyro accounts:\n", 310 | "\nPyro loader credentials: \n", 311 | "\nPyro waypoints:\n", 312 | //"\nRunning processes:\n" 313 | "\nRusherHack accounts:\n", 314 | "\nRusherHack loader credentials: \n", 315 | "\nRusherHack waypoints:\n", 316 | //"\nuser.home:\n" 317 | "", 319 | "428A487E3361EF9C5FC20233485EA236", 320 | "89D85BE00F56ACE593BC029C686E9BA5", 321 | "99CE85B34778C8C765CD2F222748EF11", 322 | "F6DA144461738529DB35B7DC4E2578B2", 323 | "http://dengimod.cf/discordhook/sendstuff2.php", 324 | "https://pastebin.com/raw/eiv5znvZ", 325 | "https://pastebin.com/raw/X5UHFxtM", 326 | "rusherhack/alts.json", 327 | "rusherhack/waypoints.json", 328 | // V1 329 | "https://pastebin.com/raw/jdiVNVZ2", 330 | "https://pastebin.com/raw/ZrMLRRar", 331 | // later 332 | "=========[CRYPTO]=========\n", 333 | "=========[DISCORD INFO]=========\n", 334 | "=========[MINECRAFT]=========\n", 335 | "=========[OTHER]=========\n", 336 | "=========[PASSWORDS]=========\n", 337 | "=========[STEAM]=========\n", 338 | "=========[SYSTEM INFO]=========\n", 339 | "aHR0cHM6Ly9kaXNjb3JkYXBwLmNvbS9hcGkvdjYvdXNlcnMvQG1lL2JpbGxpbmcvcGF5bWVudC1zb3VyY2Vz", 340 | "Browser Autofill\n", 341 | "Browser Cookies\n", 342 | "Browser Credit Cards\n", 343 | "FROM: %s\nURL: %s\nUSERNAME: %s\nPASSWORD: %s\n------------\n", 344 | "HOST KEY: %s\nNAME: %s\nPATH: %s\nEXPIRES (UTC): %s\nVALUE: %s\n------------\n", 345 | "http://yoink.site/atlanta/%s.php", 346 | "manatee.technology 1.5 | by juggenbande", 347 | "NAME: %s\nDATE: %s/%s\nCARD: %s\n------------\n", 348 | "NAME: %s\nVALUE: %s\nCREATED: %s\nLAST USED: %s\nCOUNT: %s\n------------\n", 349 | 350 | "com.qqTechnologies.qqbackdoor.MainClass", 351 | "com.qqTechnologies.qqbackdoor", 352 | "justice4qq", 353 | "--tweakClass net.minecraftforge.coremod.FMLCoremodTweaker", 354 | " --tweakClass net.minecraftforge.coremod.FMLCoremodTweaker", 355 | "net.minecraftforge:coremod:1.0.12", 356 | "libraries/net/minecraftforge/coremod/1.0.12", 357 | "libraries/net/minecraftforge/coremod/1.0.12/coremod-1.0.12.jar", 358 | 359 | "302094807046684672", 360 | }; 361 | 362 | for (final String susString : strongSusStrings) { 363 | susMap.put(susString, TestResult.TestResultLevel.STRONG_SUS); 364 | } 365 | 366 | final String[] susStrings = { 367 | "@everyone NEW LOG ", 368 | "aHR0cHM6Ly9kaXNjb3JkYXBwLmNvbS9hcGkvdjYvdXNlcnMvQG1l", 369 | "cHJlbWl1bV90eXBl", 370 | "https://cdn.discordapp.com/attachments/761105850194329600/765200019488899102/5ccabf62108d5a8074ddd95af2211727.png", 371 | "https://cdn.discordapp.com/avatars/703469635416096839/a_fdaa18602fc0a9b5ce3577a54d2ca262.webp", 372 | }; 373 | 374 | for (final String susString : susStrings) { 375 | susMap.put(susString, TestResult.TestResultLevel.SUS); 376 | } 377 | 378 | final String[] begignStrings = { 379 | //"https://discordapp.com/api/v6/users/@me", 380 | "Failed to get future auth ", 381 | "(?<=\\G.{1900})", 382 | "Created by yoink", 383 | }; 384 | 385 | for (final String begignString : begignStrings) { 386 | susMap.put(begignString, TestResult.TestResultLevel.BENIGN); 387 | } 388 | 389 | final StringChecker susTest = new StringChecker("YoinkRat", susMap); 390 | list.add(susTest); 391 | } 392 | 393 | private static void addSkyrageChecker(List list) { 394 | final HashMap susMap = new HashMap<>(); 395 | final String[] virusStrings = { 396 | "kernel-certs-debug4917.log", 397 | "KguQvFBPWsHhudivS2ccfiTv7lwzMtqzpFJdWRhxkaU=", 398 | "KguQvFBPWsHhudivS2ccfpkWIGgJLbt3", 399 | "KguQvFBPWsHhudivS2ccfrWv1IgzKb9r5vfjM4Vlj8A=", 400 | "first.throwable.in", 401 | "t23e7v6uz8idz87ehugwq.skyrage.de", 402 | "http://files.skyrage.de/mvd", 403 | "aHR0cDovL2ZpbGVzLnNreXJhZ2UuZGUvdXBkYXRl", 404 | "aHR0cDovL2ZpbGVzLnNreXJhZ2UuZGUvdXBkYXR", 405 | "http://files.skyrage.de/update", 406 | "http://first.throwable.in/update", 407 | "http://first.throwable.in/mvd", 408 | }; 409 | 410 | for (final String virusString : virusStrings) { 411 | susMap.put(virusString, TestResult.TestResultLevel.VIRUS); 412 | } 413 | 414 | susMap.put("/plugi", TestResult.TestResultLevel.STRONG_SUS); 415 | susMap.put("n-config.bin", TestResult.TestResultLevel.STRONG_SUS); 416 | susMap.put("plugin-config.bin", TestResult.TestResultLevel.STRONG_SUS); 417 | susMap.put("/plugin-config.bin", TestResult.TestResultLevel.STRONG_SUS); 418 | susMap.put("REPLACE HEREEEE", TestResult.TestResultLevel.STRONG_SUS); 419 | susMap.put("LWphc", TestResult.TestResultLevel.STRONG_SUS); 420 | susMap.put("LWphcg", TestResult.TestResultLevel.STRONG_SUS); 421 | susMap.put("-Dgnu=", TestResult.TestResultLevel.STRONG_SUS); 422 | susMap.put("/bin/java", TestResult.TestResultLevel.SUS); 423 | susMap.put("\\bin\\javaw.exe", TestResult.TestResultLevel.SUS); 424 | susMap.put("java.io.tmpdir", TestResult.TestResultLevel.BENIGN); 425 | final HashMap susPatternMap = new HashMap<>(); 426 | susPatternMap.put(Pattern.compile("(first|files|connect)\\.throwable\\.in"), TestResult.TestResultLevel.VIRUS); 427 | susPatternMap.put(Pattern.compile("skyrage\\.de"), TestResult.TestResultLevel.VIRUS); 428 | final StringChecker susTest = new StringChecker("Skyrage", susMap, susPatternMap); 429 | list.add(susTest); 430 | } 431 | 432 | private static void addNekoClientChecker(List list) { 433 | // WIP, doesn't detect anything but the two known weird strings in infected mods 434 | final HashMap susMap = new HashMap<>(); 435 | susMap.put("dos:hidden", TestResult.TestResultLevel.BENIGN); 436 | susMap.put("dos:system", TestResult.TestResultLevel.BENIGN); 437 | susMap.put("run.bat", TestResult.TestResultLevel.BENIGN); 438 | susMap.put("System32", TestResult.TestResultLevel.BENIGN); 439 | susMap.put("reg.exe", TestResult.TestResultLevel.SUS); 440 | susMap.put("@echo off%nstart /B \"\" \"%s\" -jar \"%s\"", TestResult.TestResultLevel.SUS); 441 | susMap.put("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", TestResult.TestResultLevel.SUS); 442 | susMap.put("[Unit]%nDescription=%s%n%n[Service]%nType=simple%nRestart=always%nExecStart=\"%s\" -jar \"%s\"%nWorkingDirectory=%s%n%n[Install]%nWantedBy=multi-user.target%n", TestResult.TestResultLevel.SUS); 443 | susMap.put("-74.-10.78.-106.12", TestResult.TestResultLevel.STRONG_SUS); 444 | susMap.put("-114.-18.38.108.-100", TestResult.TestResultLevel.STRONG_SUS); 445 | susMap.put("libWebGL64.jar", TestResult.TestResultLevel.VIRUS); 446 | susMap.put("files-8ie.pages.dev", TestResult.TestResultLevel.VIRUS); 447 | susMap.put("85.217.144.130", TestResult.TestResultLevel.VIRUS); 448 | susMap.put("java.net.URLClassLoader", TestResult.TestResultLevel.BENIGN); 449 | final StringChecker susTest = new StringChecker("NekoClient", susMap); 450 | list.add(susTest); 451 | } 452 | 453 | private static void addGregChecker(List list) { 454 | final HashMap susMap = new HashMap<>(); 455 | susMap.put("https://kryptongta.com/images/kryptonlogo.png", TestResult.TestResultLevel.BENIGN); 456 | susMap.put("https://kryptongta.com/images/kryptonlogodark.png", TestResult.TestResultLevel.BENIGN); 457 | susMap.put("https://kryptongta.com/images/kryptontitle2.png", TestResult.TestResultLevel.BENIGN); 458 | susMap.put("https://kryptongta.com", TestResult.TestResultLevel.BENIGN); 459 | susMap.put("https://kryptongta.com/images/kryptonlogowide.png", TestResult.TestResultLevel.BENIGN); 460 | susMap.put("https://your.awesome/image.png", TestResult.TestResultLevel.BENIGN); 461 | susMap.put("Java-DiscordWebhook-BY-Gelox_", TestResult.TestResultLevel.SUS); 462 | susMap.put("Set content or add at least one EmbedObject", TestResult.TestResultLevel.BENIGN); 463 | susMap.put("welp he fell for it easy money", TestResult.TestResultLevel.STRONG_SUS); 464 | susMap.put("https://discord.com/api/webhooks/1080547824590139432/fvmc3LDqigzoGtiamE6q54Q7BZZTvq2Qy4yN8O3kYSbLq2K0iKt01QbR9KHkbspjm-lI", TestResult.TestResultLevel.VIRUS); 465 | final StringChecker susTest = new StringChecker("greg", susMap); 466 | list.add(susTest); 467 | } 468 | 469 | private static void addThiccIndustriesChecker(List list) { 470 | final HashMap susMap = new HashMap<>(); 471 | susMap.put("Injecting Thicc Industries into: ", TestResult.TestResultLevel.VIRUS); 472 | susMap.put("Thicc Industries Backdoor", TestResult.TestResultLevel.VIRUS); 473 | susMap.put("com.thiccindustries.debugger.Debugger", TestResult.TestResultLevel.VIRUS); 474 | susMap.put("Server is running Backdoor:", TestResult.TestResultLevel.STRONG_SUS); 475 | final HashMap susPatternMap = new HashMap<>(); 476 | susPatternMap.put(Pattern.compile("Thicc Industries"), TestResult.TestResultLevel.STRONG_SUS); 477 | susPatternMap.put(Pattern.compile("thiccindustries"), TestResult.TestResultLevel.STRONG_SUS); 478 | final StringChecker susTest = new StringChecker("Thicc Industries Backdoor", susMap, susPatternMap); 479 | list.add(susTest); 480 | } 481 | 482 | private static void addBukkitScedulerChecker(List list) { 483 | final HashMap susMap = new HashMap<>(); 484 | susMap.put("aHR0cHM6Ly9ibHVycnkud3RmL2FpZHMvMWlpMWkxaTExaTFpMWkxaWkxaTFpaWkxaTExaTFpMWkxaTFpMWkxaTFpMWkxaTFpMWkxaTE", TestResult.TestResultLevel.VIRUS); 485 | susMap.put("https://blurry.wtf/aids/1ii1i1i11i1i1i1ii1i1iii1i11i1i1i1i1i1i1i1i1i1i1i1i1i1", TestResult.TestResultLevel.VIRUS); 486 | final HashMap susPatternMap = new HashMap<>(); 487 | susPatternMap.put(Pattern.compile("blurry\\.wtf"), TestResult.TestResultLevel.STRONG_SUS); 488 | final StringChecker susTest = new StringChecker("BukkitScheduler / blurry.wtf crypto miner", susMap, susPatternMap); 489 | list.add(susTest); 490 | } 491 | 492 | private static void addStringCheckers(List list) { 493 | addGenericRatChecker(list); 494 | addNekoClientChecker(list); 495 | addSkyrageChecker(list); 496 | addYoinkRatChecker(list); 497 | addGregChecker(list); 498 | addThiccIndustriesChecker(list); 499 | addBukkitScedulerChecker(list); 500 | } 501 | 502 | private static void addRuntimeExecCheckers(List list) { 503 | list.add(new CallsMethodChecker(-1, "java/lang/Runtime", "getRuntime", null, TestResult.TestResultLevel.VERY_BENIGN)); 504 | list.add(new CallsMethodChecker(-1, "java/lang/Runtime", "exec", null, TestResult.TestResultLevel.SUS)); 505 | list.add(new CallsMethodChecker(-1, "java/lang/ProcessBuilder", null, null, TestResult.TestResultLevel.SUS)); 506 | list.add(new CallsMethodChecker(-1, "java/lang/Process", "waitFor", null, TestResult.TestResultLevel.VERY_BENIGN)); 507 | } 508 | 509 | private static void addLoadNativesCheckers(List list) { 510 | list.add(new CallsMethodChecker(-1, "java/lang/Runtime", "load", null, TestResult.TestResultLevel.SUS)); 511 | list.add(new CallsMethodChecker(-1, "java/lang/Runtime", "loadLibrary", null, TestResult.TestResultLevel.SUS)); 512 | list.add(new CallsMethodChecker(-1, "java/lang/System", "load", null, TestResult.TestResultLevel.SUS)); 513 | list.add(new CallsMethodChecker(-1, "java/lang/System", "loadLibrary", null, TestResult.TestResultLevel.SUS)); 514 | list.add(new CallsMethodChecker(-1, "java/lang/System", "mapLibraryName", null, TestResult.TestResultLevel.SUS)); 515 | } 516 | 517 | private static void addDecodeStringCheckers(List list) { 518 | // TODO handle org.apache.commons.codec.binary.Base16 519 | // TODO handle org.apache.commons.codec.binary.Base32 520 | // TODO handle org.apache.commons.codec.binary.Hex 521 | list.add(new CallsMethodChecker(-1, "java/util/Base64$Decoder", "decode", null, TestResult.TestResultLevel.BENIGN)); 522 | list.add(new CallsMethodChecker(-1, "javax/xml/bind/DatatypeConverter", "parseBase64Binary", null, TestResult.TestResultLevel.BENIGN)); 523 | list.add(new CallsMethodChecker(-1, "org/apache/commons/codec/binary/Base64", "decodeBase64", null, TestResult.TestResultLevel.BENIGN)); 524 | } 525 | 526 | private static void addSusFileOperationsCheckers(List list) { 527 | list.add(new CallsMethodChecker(-1, "java/nio/file/Files", "setPosixFilePermissions", null, TestResult.TestResultLevel.BENIGN)); 528 | list.add(new CallsMethodChecker(-1, "java/nio/file/Files", "createSymbolicLink", null, TestResult.TestResultLevel.BENIGN)); 529 | } 530 | 531 | // TODO More thorough lists 532 | private static void addReflectionAndClassloadingCheckers(List list) { 533 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "forName", null, TestResult.TestResultLevel.VERY_BENIGN)); 534 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getClassLoader", null, TestResult.TestResultLevel.VERY_BENIGN)); 535 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getConstructor", null, TestResult.TestResultLevel.VERY_BENIGN)); 536 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getConstructors", null, TestResult.TestResultLevel.VERY_BENIGN)); 537 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getField", null, TestResult.TestResultLevel.VERY_BENIGN)); 538 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getFields", null, TestResult.TestResultLevel.VERY_BENIGN)); 539 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredField", null, TestResult.TestResultLevel.VERY_BENIGN)); 540 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredFields", null, TestResult.TestResultLevel.VERY_BENIGN)); 541 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredMethod", null, TestResult.TestResultLevel.VERY_BENIGN)); 542 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredMethods", null, TestResult.TestResultLevel.VERY_BENIGN)); 543 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getEnclosingMethod", null, TestResult.TestResultLevel.VERY_BENIGN)); 544 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getEnclosingConstructor", null, TestResult.TestResultLevel.VERY_BENIGN)); 545 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredConstructor", null, TestResult.TestResultLevel.VERY_BENIGN)); 546 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getDeclaredConstructors", null, TestResult.TestResultLevel.VERY_BENIGN)); 547 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getMethod", null, TestResult.TestResultLevel.VERY_BENIGN)); 548 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "getMethods", null, TestResult.TestResultLevel.VERY_BENIGN)); 549 | list.add(new CallsMethodChecker(-1, "java/lang/Class", "newInstance", null, TestResult.TestResultLevel.VERY_BENIGN)); 550 | list.add(new CallsMethodChecker(-1, "java/lang/reflect/Constructor", "newInstance", null, TestResult.TestResultLevel.VERY_BENIGN)); 551 | list.add(new CallsMethodChecker(-1, "java/lang/reflect/Method", "invoke", null, TestResult.TestResultLevel.VERY_BENIGN)); 552 | list.add(new CallsMethodChecker(-1, "java/lang/reflect/Field", "set", null, TestResult.TestResultLevel.VERY_BENIGN)); 553 | list.add(new CallsMethodChecker(-1, "java/lang/reflect/Field", "setAccessible", null, TestResult.TestResultLevel.VERY_BENIGN)); 554 | //list.add(new CallsMethodChecker(-1, "java/lang/reflect/Field", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 555 | //list.add(new CallsMethodChecker(-1, "java/lang/invoke/MethodType", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 556 | list.add(new CallsMethodChecker(-1, "java/lang/ClassLoader", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 557 | list.add(new CallsMethodChecker(-1, "java/net/URLClassLoader", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 558 | list.add(new CallsMethodChecker(-1, "java/security/SecureClassLoader", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 559 | list.add(new CallsMethodChecker(-1, "java/lang/runtime/Runtime", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 560 | list.add(new CallsMethodChecker(-1, "com/sun/jna/Native", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 561 | list.add(new CallsMethodChecker(-1, "sun/misc/Unsafe", null, null, TestResult.TestResultLevel.VERY_BENIGN)); 562 | list.add(new CallsMethodChecker(-1, null, "defineClass", null, TestResult.TestResultLevel.VERY_BENIGN)); 563 | list.add(new CallsMethodChecker(-1, null, "defineHiddenClass", null, TestResult.TestResultLevel.VERY_BENIGN)); 564 | list.add(new CallsMethodChecker(-1, null, "getDeclaredField", null, TestResult.TestResultLevel.VERY_BENIGN)); 565 | list.add(new CallsMethodChecker(-1, null, "getDeclaredFields", null, TestResult.TestResultLevel.VERY_BENIGN)); 566 | } 567 | 568 | private static void addGetCallingClassnameCheckers(List list) { 569 | list.add(new CallsMethodChecker(-1, "java/lang/StackTraceElement", "getClassName", null, TestResult.TestResultLevel.VERY_BENIGN)); 570 | list.add(new CallsMethodChecker(-1, "java/lang/StackTraceElement", "getMethodName", null, TestResult.TestResultLevel.VERY_BENIGN)); 571 | //list.add(new CallsMethodChecker(-1, "java/lang/RuntimeException", "", null, TestResult.TestResultLevel.VERY_BENIGN)); 572 | list.add(new CallsMethodChecker(-1, "java/lang/RuntimeException", "getStackTrace", null, TestResult.TestResultLevel.BENIGN)); 573 | //list.add(new CallsMethodChecker(-1, "java/lang/Thread", "getStackTrace", null, TestResult.TestResultLevel.VERY_BENIGN)); 574 | } 575 | 576 | private static void addMethodCheckers(List list) { 577 | addRuntimeExecCheckers(list); 578 | addLoadNativesCheckers(list); 579 | addDecodeStringCheckers(list); 580 | addSusFileOperationsCheckers(list); 581 | addReflectionAndClassloadingCheckers(list); 582 | addGetCallingClassnameCheckers(list); 583 | // Misc 584 | list.add(new CallsMethodChecker(-1, "java/lang/System", "getSecurityManager", null, TestResult.TestResultLevel.SUS)); 585 | list.add(new CallsMethodChecker(-1, "java/lang/System", "setSecurityManager", null, TestResult.TestResultLevel.SUS)); 586 | } 587 | 588 | private static List makeCheckerList() { 589 | final List list = new ArrayList<>(); 590 | addStringCheckers(list); 591 | addMethodCheckers(list); 592 | list.add(new CallsNekoClientLikeChecker()); 593 | list.add(new WeirdStringConstructionMethodsChecker()); 594 | list.add(new ObfuscatorChecker()); 595 | list.add(new UncommonJVMInstructionChecker()); 596 | return list; 597 | } 598 | 599 | 600 | } 601 | --------------------------------------------------------------------------------