├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src └── main ├── java └── tk │ └── scuti │ └── core │ └── lite │ └── Scuti.java └── resources └── simplelogger.properties /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | *.ctxt 4 | target/ 5 | dependency-reduced-pom.xml 6 | .setup/ 7 | bin/ 8 | .classpath 9 | .project 10 | *.iml 11 | .idea/ 12 | .settings/ 13 | out/ 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 netindev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scuti-lite [![Build Status](https://travis-ci.org/netindev/scuti-lite.svg?branch=master)](https://travis-ci.org/netindev/scuti-lite) 2 | Java obfuscator in one class, written using [ASM](https://asm.ow2.io/). 3 | 4 | ## Download 5 | * Binary releases: https://github.com/netindev/scuti-lite/releases 6 | * Git tree: https://github.com/netindev/scuti-lite.git 7 | 8 | ## Usage 9 | Usage: ```java -jar scuti-lite.jar -in "input.jar" -out "output.jar"``` 10 | 11 | ## Build 12 | Java: 13 | * Install [Maven](https://maven.apache.org/download.html) 14 | * Go to: `..\scuti-lite` and execute `mvn clean install` 15 | 16 | ## Contacts 17 | * [email](mailto:contact@netindev.tk) 18 | * [twitter](https://twitter.com/netindev) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | tk.scuti.core.lite 6 | scuti-lite 7 | 0.0.3 8 | 9 | 1.8 10 | 1.8 11 | 12 | 13 | 14 | commons-cli 15 | commons-cli 16 | 1.4 17 | 18 | 19 | org.ow2.asm 20 | asm 21 | 7.0 22 | 23 | 24 | org.ow2.asm 25 | asm-tree 26 | 7.0 27 | 28 | 29 | org.slf4j 30 | slf4j-simple 31 | 1.7.21 32 | 33 | 34 | org.slf4j 35 | slf4j-api 36 | 1.7.5 37 | 38 | 39 | 40 | target/classes 41 | 42 | 43 | org.apache.maven.plugins 44 | maven-jar-plugin 45 | 46 | 47 | 48 | true 49 | tk.scuti.core.lite.Scuti 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-shade-plugin 57 | 3.2.0 58 | 59 | 60 | package 61 | 62 | shade 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/java/tk/scuti/core/lite/Scuti.java: -------------------------------------------------------------------------------- 1 | package tk.scuti.core.lite; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.FileOutputStream; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.lang.reflect.Modifier; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.jar.JarEntry; 13 | import java.util.jar.JarFile; 14 | import java.util.jar.JarOutputStream; 15 | import java.util.zip.ZipEntry; 16 | 17 | import org.apache.commons.cli.CommandLine; 18 | import org.apache.commons.cli.DefaultParser; 19 | import org.apache.commons.cli.Option; 20 | import org.apache.commons.cli.Options; 21 | import org.apache.commons.cli.ParseException; 22 | import org.objectweb.asm.ClassReader; 23 | import org.objectweb.asm.ClassWriter; 24 | import org.objectweb.asm.Opcodes; 25 | import org.objectweb.asm.tree.ClassNode; 26 | import org.objectweb.asm.tree.MethodNode; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | /* 31 | * The MIT License 32 | * 33 | * Copyright 2019 netindev. 34 | * 35 | * Permission is hereby granted, free of charge, to any person obtaining a copy 36 | * of this software and associated documentation files (the "Software"), to deal 37 | * in the Software without restriction, including without limitation the rights 38 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | * copies of the Software, and to permit persons to whom the Software is 40 | * furnished to do so, subject to the following conditions: 41 | * 42 | * The above copyright notice and this permission notice shall be included in 43 | * all copies or substantial portions of the Software. 44 | * 45 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 51 | * THE SOFTWARE. 52 | */ 53 | public class Scuti { 54 | 55 | private JarOutputStream outputStream; 56 | 57 | private final File inputFile, outputFile; 58 | private final Map classes; 59 | 60 | private static final double PACKAGE_VERSION = 0.03D; 61 | 62 | private static final Logger logger = LoggerFactory 63 | .getLogger(Scuti.class.getName()); 64 | 65 | public Scuti(File inputFile, File outputFile) { 66 | this.inputFile = inputFile; 67 | this.outputFile = outputFile; 68 | this.classes = new HashMap<>(); 69 | } 70 | 71 | public static void main(String[] args) { 72 | System.out 73 | .println("Scuti-lite Java obfuscator written by netindev, version " 74 | + PACKAGE_VERSION); 75 | if (args.length == 0) { 76 | logger.error( 77 | "Invalid arguments, please add to the arguments your input file and output file, e.g: \"java -jar scuti-lite.jar -in \"your input file.jar\" -out \"your output file.jar\"."); 78 | return; 79 | } 80 | try { 81 | parseArgs(args); 82 | } catch (final Throwable e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | 87 | private static void parseArgs(String[] args) { 88 | final Options options = new Options(); 89 | options.addOption( 90 | Option.builder("in").hasArg().required().argName("jar").build()); 91 | options.addOption( 92 | Option.builder("out").hasArg().required().argName("jar").build()); 93 | try { 94 | final CommandLine parse = new DefaultParser().parse(options, args); 95 | final File inputFile = new File(parse.getOptionValue("in")); 96 | final File outputFile = new File(parse.getOptionValue("out")); 97 | if (!inputFile.exists() || !inputFile.canRead()) { 98 | logger.error("Input file can't be read or doesn't exists"); 99 | return; 100 | } 101 | new Scuti(inputFile, outputFile).run(); 102 | } catch (final ParseException e) { 103 | logger.error(e.getMessage()); 104 | } catch (final Throwable e) { 105 | logger.error(e.getMessage()); 106 | } 107 | } 108 | 109 | private void run() throws Throwable { 110 | this.outputStream = new JarOutputStream( 111 | new FileOutputStream(this.outputFile)); 112 | logger.info("Parsing input \"" + this.inputFile.getName() + "\""); 113 | this.parseInput(); 114 | logger.info("Transforming classes"); 115 | this.classes.values().forEach(classNode -> { 116 | // remove unnecessary insn 117 | this.removeNop(classNode); 118 | if (!Modifier.isInterface(classNode.access)) { 119 | this.transientAccess(classNode); 120 | } 121 | this.deprecatedAccess(classNode); 122 | // bad sources 123 | this.changeSource(classNode); 124 | // bad signatures 125 | this.changeSignature(classNode); 126 | // synthetic access (most decompilers doesn't show synthetic members) 127 | this.syntheticAccess(classNode); 128 | classNode.methods.forEach(methodNode -> { 129 | // bridge access (almost the same than synthetic) 130 | this.bridgeAccess(methodNode); 131 | // varargs access (crashes CFR when last parameter isn't array) 132 | this.varargsAccess(methodNode); 133 | }); 134 | }); 135 | logger.info("Dumping output \"" + this.outputFile.getName() + "\""); 136 | this.dumpClasses(); 137 | logger.info("Obfuscation finished"); 138 | } 139 | 140 | private void varargsAccess(MethodNode methodNode) { 141 | if ((methodNode.access & Opcodes.ACC_SYNTHETIC) == 0 142 | && (methodNode.access & Opcodes.ACC_BRIDGE) == 0) { 143 | methodNode.access |= Opcodes.ACC_VARARGS; 144 | } 145 | } 146 | 147 | private void bridgeAccess(MethodNode methodNode) { 148 | if (!methodNode.name.contains("<") 149 | && !Modifier.isAbstract(methodNode.access)) { 150 | methodNode.access |= Opcodes.ACC_BRIDGE; 151 | } 152 | } 153 | 154 | private void syntheticAccess(ClassNode classNode) { 155 | classNode.access |= Opcodes.ACC_SYNTHETIC; 156 | classNode.fields 157 | .forEach(fieldNode -> fieldNode.access |= Opcodes.ACC_SYNTHETIC); 158 | classNode.methods 159 | .forEach(methodNode -> methodNode.access |= Opcodes.ACC_SYNTHETIC); 160 | } 161 | 162 | private void changeSource(ClassNode classNode) { 163 | classNode.sourceFile = this.getMassiveString(); 164 | classNode.sourceDebug = this.getMassiveString(); 165 | } 166 | 167 | private void changeSignature(ClassNode classNode) { 168 | classNode.signature = this.getMassiveString(); 169 | classNode.fields.forEach( 170 | fieldNode -> fieldNode.signature = this.getMassiveString()); 171 | classNode.methods.forEach( 172 | methodNode -> methodNode.signature = this.getMassiveString()); 173 | } 174 | 175 | private void deprecatedAccess(ClassNode classNode) { 176 | classNode.access |= Opcodes.ACC_DEPRECATED; 177 | classNode.methods 178 | .forEach(methodNode -> methodNode.access |= Opcodes.ACC_DEPRECATED); 179 | classNode.fields 180 | .forEach(fieldNode -> fieldNode.access |= Opcodes.ACC_DEPRECATED); 181 | } 182 | 183 | private void transientAccess(ClassNode classNode) { 184 | classNode.fields 185 | .forEach(fieldNode -> fieldNode.access |= Opcodes.ACC_TRANSIENT); 186 | } 187 | 188 | private void removeNop(ClassNode classNode) { 189 | classNode.methods.parallelStream().forEach( 190 | methodNode -> Arrays.stream(methodNode.instructions.toArray()) 191 | .filter(insnNode -> insnNode.getOpcode() == Opcodes.NOP) 192 | .forEach(insnNode -> { 193 | methodNode.instructions.remove(insnNode); 194 | })); 195 | } 196 | 197 | private void parseInput() throws IOException { 198 | final JarFile jarFile = new JarFile(this.inputFile); 199 | jarFile.stream().forEach(entry -> { 200 | try { 201 | if (entry.getName().endsWith(".class")) { 202 | final ClassReader classReader = new ClassReader( 203 | jarFile.getInputStream(entry)); 204 | final ClassNode classNode = new ClassNode(); 205 | classReader.accept(classNode, ClassReader.SKIP_DEBUG); 206 | this.classes.put(classNode.name, classNode); 207 | } else if (!entry.isDirectory()) { 208 | this.outputStream.putNextEntry(new ZipEntry(entry.getName())); 209 | this.outputStream 210 | .write(this.toByteArray(jarFile.getInputStream(entry))); 211 | this.outputStream.closeEntry(); 212 | } 213 | } catch (final Exception e) { 214 | e.printStackTrace(); 215 | } 216 | }); 217 | jarFile.close(); 218 | } 219 | 220 | private void dumpClasses() throws IOException { 221 | this.classes.values().forEach(classNode -> { 222 | ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 223 | try { 224 | classNode.accept(classWriter); 225 | final JarEntry jarEntry = new JarEntry( 226 | classNode.name.concat(".class")); 227 | this.outputStream.putNextEntry(jarEntry); 228 | this.outputStream.write(classWriter.toByteArray()); 229 | } catch (final Exception e) { 230 | logger.error("Error while writing " + classNode.name, e); 231 | } 232 | }); 233 | this.outputStream.close(); 234 | } 235 | 236 | private String getMassiveString() { 237 | final StringBuilder builder = new StringBuilder(); 238 | for (int i = 0; i < Short.MAX_VALUE; i++) { 239 | builder.append(" "); 240 | } 241 | return builder.toString(); 242 | } 243 | 244 | private byte[] toByteArray(InputStream inputStream) throws IOException { 245 | final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 246 | final byte[] buffer = new byte[0xFFFF]; 247 | int length; 248 | while ((length = inputStream.read(buffer)) != -1) { 249 | outputStream.write(buffer, 0, length); 250 | } 251 | outputStream.flush(); 252 | return outputStream.toByteArray(); 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.logFile=System.out 2 | org.slf4j.simpleLogger.showDateTime=true 3 | org.slf4j.simpleLogger.dateTimeFormat=[HH:mm:ss] 4 | org.slf4j.simpleLogger.showThreadName=false 5 | org.slf4j.simpleLogger.showLogName=false 6 | org.slf4j.simpleLogger.levelInBrackets=true --------------------------------------------------------------------------------