diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2bb7dc5..723c185 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ asm = "9.5" cafedude = "1.10.2" -lljzip = "1.3.2" +lljzip = "1.3.3" picocli = "4.7.4" jsr305 = "3.0.2" slf4j = "2.0.7" diff --git a/src/main/java/me/cortex/jarscanner/detection/DetectionManager.java b/src/main/java/me/cortex/jarscanner/detection/DetectionManager.java index d0d188b..49abb8d 100644 --- a/src/main/java/me/cortex/jarscanner/detection/DetectionManager.java +++ b/src/main/java/me/cortex/jarscanner/detection/DetectionManager.java @@ -1,8 +1,6 @@ package me.cortex.jarscanner.detection; -import me.cortex.jarscanner.detection.impl.FractureiserStage0a; -import me.cortex.jarscanner.detection.impl.FractureiserStage0b; -import me.cortex.jarscanner.detection.impl.FractureiserStage0c; +import me.cortex.jarscanner.detection.impl.*; import javax.annotation.Nonnull; import java.util.ArrayList; @@ -20,6 +18,8 @@ public final class DetectionManager { instance.addDetection(new FractureiserStage0a()); instance.addDetection(new FractureiserStage0b()); instance.addDetection(new FractureiserStage0c()); + instance.addDetection(new Skyragea()); + instance.addDetection(new Skyrageb()); } private DetectionManager() { diff --git a/src/main/java/me/cortex/jarscanner/detection/impl/Skyragea.java b/src/main/java/me/cortex/jarscanner/detection/impl/Skyragea.java new file mode 100644 index 0000000..883b9d9 --- /dev/null +++ b/src/main/java/me/cortex/jarscanner/detection/impl/Skyragea.java @@ -0,0 +1,62 @@ +package me.cortex.jarscanner.detection.impl; + +import me.cortex.jarscanner.detection.DetectionSink; +import me.cortex.jarscanner.util.InsnUtils; +import org.objectweb.asm.tree.*; + +import javax.annotation.Nonnull; +import java.nio.file.Path; + +import static java.lang.reflect.Modifier.isStatic; + +public class Skyragea extends AbstractDetection { + + // Based on KosmX's Skyrage detector: https://github.com/KosmX/jneedle/blob/main/src/dbGen/kotlin/dev/kosmx/needle/dbGen/db/Skyrage.kt + private static final AbstractInsnNode[] SIG = { + new TypeInsnNode(NEW, "java/lang/String"), + new MethodInsnNode(INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "concat", "(Ljava/lang/String;)Ljava/lang/String;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"), + new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), + new MethodInsnNode(INVOKEVIRTUAL, "java/io/File", "getPath", "()Ljava/lang/String;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Process", "waitFor", "()I"), + new MethodInsnNode(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V"), + new MethodInsnNode(INVOKEVIRTUAL, "java/io/File", "delete", "()Z"), + new MethodInsnNode(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "getBytes", "()[B"), + new MethodInsnNode(INVOKESTATIC, "java/util/Base64", "getDecoder", "()Ljava/util/Base64$Decoder;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/util/Base64$Decoder", "decode", "(Ljava/lang/String;)[B"), + }; + + public Skyragea() { + super("Skyrage Variant a"); + } + + @Override + public void scan(@Nonnull Path jarPath, @Nonnull ClassNode node, @Nonnull DetectionSink sink) { + for (MethodNode method : node.methods) { + AbstractInsnNode[] insns = method.instructions.toArray(); + int sigOffset = 0; + for (int i = 0; i < insns.length && sigOffset < SIG.length; i++) { + AbstractInsnNode insn = insns[i]; + + // Skip labels + if (insn.getOpcode() == -1) + continue; + + // Check if opcode matches sig opcode, and insn operands match + if (insn.getOpcode() == SIG[sigOffset].getOpcode() && + !InsnUtils.same(insn, SIG[sigOffset++])) { + sigOffset = 0; + continue; + } + } + if (sigOffset >= SIG.length) { + sink.addItem(det(jarPath, node, method)); + return; + } + } + } + +} diff --git a/src/main/java/me/cortex/jarscanner/detection/impl/Skyrageb.java b/src/main/java/me/cortex/jarscanner/detection/impl/Skyrageb.java new file mode 100644 index 0000000..0d0e79b --- /dev/null +++ b/src/main/java/me/cortex/jarscanner/detection/impl/Skyrageb.java @@ -0,0 +1,79 @@ +package me.cortex.jarscanner.detection.impl; + +import me.cortex.jarscanner.detection.DetectionSink; +import me.cortex.jarscanner.util.InsnUtils; +import org.objectweb.asm.tree.*; + +import javax.annotation.Nonnull; +import java.nio.file.Path; + +import static java.lang.reflect.Modifier.isStatic; + +public class Skyrageb extends AbstractDetection { + + // Based on KosmX's Skyrage detector: https://github.com/KosmX/jneedle/blob/main/src/dbGen/kotlin/dev/kosmx/needle/dbGen/db/Skyrage.kt + private static final AbstractInsnNode[] SIG = { + new MethodInsnNode(INVOKESTATIC, "java/lang/Runtime", "getRuntime", "()Ljava/lang/Runtime;"), + new TypeInsnNode(ANEWARRAY, "java/lang/String"), + new TypeInsnNode(NEW, "java/lang/StringBuilder"), + new MethodInsnNode(INVOKESPECIAL, "java/lang/StringBuilder", "", "()V"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"), + new MethodInsnNode(INVOKESTATIC, "java/util/Base64", "getEncoder", "()Ljava/util/Base64$Encoder;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/util/Base64$Encoder", "encodeToString", "([B)Ljava/lang/String;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;"), + new TypeInsnNode(NEW, "java/lang/String"), + new MethodInsnNode(INVOKESPECIAL, "java/lang/String", "", "([B)V"), + new MethodInsnNode(INVOKEVIRTUAL, "java/io/File", "getPath", "()Ljava/lang/String;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Runtime", "exec", "([Ljava/lang/String;)Ljava/lang/Process;"), + new MethodInsnNode(INVOKEVIRTUAL, "java/lang/Process", "waitFor", "()I"), + new MethodInsnNode(INVOKEVIRTUAL, "java/io/File", "delete", "()Z"), + }; + + public Skyrageb() { + super("Skyrage Variant b"); + } + + @Override + public void scan(@Nonnull Path jarPath, @Nonnull ClassNode node, @Nonnull DetectionSink sink) { + + // Basically for each class, loop through its methods + // When inside the method, loop through each instruction until we reach the first one + // to match the first instruction in SIG. + + // loop through every method in the class + for (MethodNode method : node.methods) { + + // array of instructions + AbstractInsnNode[] insns = method.instructions.toArray(); + + // track which item in SIG we are currently looking for + int sigOffset = 0; + + // loop through the instructions + for (int i = 0; i < insns.length && sigOffset < SIG.length; i++) { + // bytecode instruction + AbstractInsnNode insn = insns[i]; + + // Skip labels + if (insn.getOpcode() == -1) + continue; + + // Check if opcode matches sig opcode, and insn operands match + // + if (insn.getOpcode() == SIG[sigOffset].getOpcode() && + !InsnUtils.same(insn, SIG[sigOffset++])) { + sigOffset = 0; + continue; + } + } + + // if we've cycled through the entire SIG array, we've got a match + if (sigOffset >= SIG.length) { + sink.addItem(det(jarPath, node, method)); + return; + } + } + } + +} diff --git a/src/main/java/me/cortex/jarscanner/scanner/RootScanner.java b/src/main/java/me/cortex/jarscanner/scanner/RootScanner.java index 80a8479..69b343b 100644 --- a/src/main/java/me/cortex/jarscanner/scanner/RootScanner.java +++ b/src/main/java/me/cortex/jarscanner/scanner/RootScanner.java @@ -9,6 +9,7 @@ import software.coley.llzip.format.ZipPatterns; import javax.annotation.Nonnull; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -90,20 +91,36 @@ public JarFileVisitor(JarScanner scanner, ExecutorService scanPool, List) () -> scanner.runScan(file), this.scanPool) - .whenComplete((jarScan, exception) -> { - if (jarScan != null) { - logger.info("Completed scan for jar: {} - [Detections={}, Problems={}]", - file, jarScan.getItems().size(), jarScan.getProblems().size()); - this.jarScans.add(jarScan); - } else if (exception != null) { - logger.warn("Error scanning jar: {}", file, exception); - } - }); + + // if the file isn't a jar or zip skip it + if (!pathNameInsensitive.endsWith(".jar") || !isZip(file)) { + return FileVisitResult.CONTINUE; } + + // check if the file is too big + try { + if (!Files.isDirectory(file) && Files.size(file) >= 2e+9) { + logger.warn("Jar is too large to scan (>2gb): {}", file); + return FileVisitResult.CONTINUE; + } + } catch (IOException e) { + logger.error("Failed to get file size for jar: {}", file); + return FileVisitResult.CONTINUE; + } + + // Submit all jar files to the thread pool + CompletableFuture.supplyAsync((UncheckedSupplier) () -> scanner.runScan(file), this.scanPool) + .whenComplete((jarScan, exception) -> { + if (jarScan != null) { + logger.info("Completed scan for jar: {} - [Detections={}, Problems={}]", + file, jarScan.getItems().size(), jarScan.getProblems().size()); + this.jarScans.add(jarScan); + } else if (exception != null) { + logger.warn("Error scanning jar: {}", file, exception); + } + }); + return FileVisitResult.CONTINUE; }