├── .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 [](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
--------------------------------------------------------------------------------