diff --git a/obfuscator/src/main/java/by/radioegor146/NativeObfuscator.java b/obfuscator/src/main/java/by/radioegor146/NativeObfuscator.java index ebdf191..b78e882 100644 --- a/obfuscator/src/main/java/by/radioegor146/NativeObfuscator.java +++ b/obfuscator/src/main/java/by/radioegor146/NativeObfuscator.java @@ -1,6 +1,8 @@ package by.radioegor146; import by.radioegor146.bytecode.PreprocessorRunner; +import by.radioegor146.javaobf.JvmRenamerConfig; +import by.radioegor146.javaobf.JvmRenamerEntityConfig; import by.radioegor146.source.CMakeFilesBuilder; import by.radioegor146.source.ClassSourceBuilder; import by.radioegor146.source.MainSourceBuilder; @@ -121,7 +123,8 @@ public void process(Path inputJarPath, Path outputDir, List inputLibs, obfuscateStrings, obfuscateConstants, false, "MEDIUM", new ArrayList<>(), new ArrayList<>(), true, true, true, true, false, false, false, FlowExceptionMode.STANDARD, - JavaFlowSettings.createDefault()); + JavaFlowSettings.createDefault(), + JvmRenamerConfig.disabled()); } public void process(Path inputJarPath, Path outputDir, List inputLibs, @@ -136,7 +139,8 @@ public void process(Path inputJarPath, Path outputDir, List inputLibs, boolean skidFlowObfuscation, boolean skidSdkInjection, boolean skidVmHashing, boolean skidInvokeDynamicObfuscation, FlowExceptionMode skidFlowExceptionMode, - JavaFlowSettings javaFlowSettings) throws IOException { + JavaFlowSettings javaFlowSettings, + JvmRenamerConfig jvmRenamerConfig) throws IOException { ProtectionConfig protectionConfig = new ProtectionConfig(enableVirtualization, enableJit, flattenControlFlow, obfuscateStrings, obfuscateConstants); if (Files.exists(outputDir) && Files.isSameFile(inputJarPath.toRealPath().getParent(), outputDir.toRealPath())) { @@ -153,7 +157,7 @@ public void process(Path inputJarPath, Path outputDir, List inputLibs, Files.createDirectories(javaObfOutputDir); Path skidOutputJar = javaObfOutputDir.resolve(inputJarPath.getFileName().toString()); - Path skidConfig = createSkidConfig(javaBlackList, javaWhiteList, javaObfOutputDir, javaFlowSettings); + Path skidConfig = createSkidConfig(javaBlackList, javaWhiteList, javaObfOutputDir, javaFlowSettings, jvmRenamerConfig); File[] skidLibs = inputLibs.stream().map(Path::toFile).toArray(File[]::new); SkidfuscatorSession session = SkidfuscatorSession.builder() @@ -164,7 +168,7 @@ public void process(Path inputJarPath, Path outputDir, List inputLibs, .analytics(false) .phantom(false) .fuckit(false) - .renamer(false) + .renamer(jvmRenamerConfig != null && jvmRenamerConfig.isEnabled() && jvmRenamerConfig.isAnyEntityEnabled()) .debug(logger.isDebugEnabled()) .skidStringObfuscation(skidStringObfuscation) .skidNumberObfuscation(skidNumberObfuscation) @@ -578,7 +582,8 @@ public void process(ObfuscatorConfig config) throws IOException { config.isSkidStringObfuscation(), config.isSkidNumberObfuscation(), config.isSkidFlowObfuscation(), config.isSkidSdkInjection(), config.isSkidVmHashing(), config.isSkidInvokeDynamicObfuscation(), - config.getSkidFlowExceptionMode(), config.getJavaFlowSettings()); + config.getSkidFlowExceptionMode(), config.getJavaFlowSettings(), + config.getJvmRenamerConfig()); } private void processWithAntiDebug(Path inputJarPath, Path outputDir, List inputLibs, @@ -592,7 +597,8 @@ private void processWithAntiDebug(Path inputJarPath, Path outputDir, List boolean skidFlowObfuscation, boolean skidSdkInjection, boolean skidVmHashing, boolean skidInvokeDynamicObfuscation, FlowExceptionMode skidFlowExceptionMode, - JavaFlowSettings javaFlowSettings) throws IOException { + JavaFlowSettings javaFlowSettings, + JvmRenamerConfig jvmRenamerConfig) throws IOException { // Call the existing process method but with extended functionality process(inputJarPath, outputDir, inputLibs, blackList, whiteList, plainLibName, customLibraryDirectory, @@ -602,7 +608,8 @@ private void processWithAntiDebug(Path inputJarPath, Path outputDir, List protectionConfig.isStringObfuscationEnabled(), protectionConfig.isConstantObfuscationEnabled(), enableJavaObfuscation, javaObfuscationStrength, javaBlackList, javaWhiteList, enableNativeObfuscation, skidStringObfuscation, skidNumberObfuscation, skidFlowObfuscation, skidSdkInjection, skidVmHashing, - skidInvokeDynamicObfuscation, skidFlowExceptionMode, javaFlowSettings); + skidInvokeDynamicObfuscation, skidFlowExceptionMode, javaFlowSettings, + jvmRenamerConfig); // Generate anti-debug configuration header if any anti-debug features are enabled if (antiDebugConfig.isAnyEnabled()) { @@ -890,16 +897,18 @@ private String generateAntiDebugInitCode(AntiDebugConfig antiDebugConfig) { private Path createSkidConfig(List javaBlackList, List javaWhiteList, Path outputDir, - JavaFlowSettings flowSettings) throws IOException { + JavaFlowSettings flowSettings, + JvmRenamerConfig renamerConfig) throws IOException { boolean hasBlacklist = javaBlackList != null && !javaBlackList.isEmpty(); boolean hasWhitelist = javaWhiteList != null && !javaWhiteList.isEmpty(); boolean hasCustomFlow = flowSettings != null && !flowSettings.isDefault(); + boolean hasRenamer = renamerConfig != null && renamerConfig.isEnabled() && renamerConfig.isAnyEntityEnabled(); if (hasWhitelist) { logger.warn("Skidfuscator migration does not support java whitelist entries; ignoring provided list."); } - if (!hasBlacklist && !hasCustomFlow) { + if (!hasBlacklist && !hasCustomFlow && !hasRenamer) { return null; } @@ -945,6 +954,15 @@ private Path createSkidConfig(List javaBlackList, builder.append("]\n }\n}\n"); } + if (hasRenamer) { + if (builder.length() > 0 && builder.charAt(builder.length() - 1) != '\n') { + builder.append('\n'); + } + appendRenamerSection(builder, "classRenamer", renamerConfig.getClassConfig(), true); + appendRenamerSection(builder, "methodRenamer", renamerConfig.getMethodConfig(), false); + appendRenamerSection(builder, "fieldRenamer", renamerConfig.getFieldConfig(), false); + } + if (builder.length() == 0) { return null; } @@ -954,6 +972,47 @@ private Path createSkidConfig(List javaBlackList, return configPath; } + private void appendRenamerSection(StringBuilder builder, + String sectionName, + JvmRenamerEntityConfig entity, + boolean includeClassSettings) { + if (entity == null) { + return; + } + + builder.append(sectionName).append(" {\n"); + builder.append(" enabled = ").append(entity.isEnabled()).append('\n'); + builder.append(" type = \"").append(entity.getMode().name()).append("\"\n"); + builder.append(" depth = ").append(entity.getDepth()).append('\n'); + + if (includeClassSettings) { + if (entity.getPrefix() != null) { + builder.append(" prefix = \"").append(escapeForHocon(entity.getPrefix())).append("\"\n"); + } + if (entity.getDirectoryDepth() != null) { + builder.append(" directoryDepth = ").append(entity.getDirectoryDepth()).append('\n'); + } + if (entity.getSegmentLength() != null) { + builder.append(" segmentLength = ").append(entity.getSegmentLength()).append('\n'); + } + } + + if (!entity.getAlphabet().isEmpty()) { + builder.append(" chars = [\n"); + for (int i = 0; i < entity.getAlphabet().size(); i++) { + String token = entity.getAlphabet().get(i); + builder.append(" \"").append(escapeForHocon(token)).append("\""); + if (i + 1 < entity.getAlphabet().size()) { + builder.append(','); + } + builder.append('\n'); + } + builder.append(" ]\n"); + } + + builder.append("}\n\n"); + } + private String convertToSkidPattern(String entry) { if (entry == null) { return null; diff --git a/obfuscator/src/main/java/by/radioegor146/ObfuscatorConfig.java b/obfuscator/src/main/java/by/radioegor146/ObfuscatorConfig.java index 3d981ee..27e9a50 100644 --- a/obfuscator/src/main/java/by/radioegor146/ObfuscatorConfig.java +++ b/obfuscator/src/main/java/by/radioegor146/ObfuscatorConfig.java @@ -1,5 +1,6 @@ package by.radioegor146; +import by.radioegor146.javaobf.JvmRenamerConfig; import dev.skidfuscator.obfuscator.FlowExceptionMode; import java.nio.file.Path; @@ -44,6 +45,7 @@ public class ObfuscatorConfig { private final boolean skidInvokeDynamicObfuscation; private final FlowExceptionMode skidFlowExceptionMode; private final JavaFlowSettings javaFlowSettings; + private final JvmRenamerConfig jvmRenamerConfig; public ObfuscatorConfig(Path inputJarPath, Path outputDir, List inputLibs, List blackList, List whiteList, @@ -57,7 +59,8 @@ public ObfuscatorConfig(Path inputJarPath, Path outputDir, List inputLibs, boolean skidFlowObfuscation, boolean skidSdkInjection, boolean skidVmHashing, boolean skidInvokeDynamicObfuscation, FlowExceptionMode skidFlowExceptionMode, - JavaFlowSettings javaFlowSettings) { + JavaFlowSettings javaFlowSettings, + JvmRenamerConfig jvmRenamerConfig) { this.inputJarPath = inputJarPath; this.outputDir = outputDir; this.inputLibs = inputLibs; @@ -83,6 +86,7 @@ public ObfuscatorConfig(Path inputJarPath, Path outputDir, List inputLibs, this.skidInvokeDynamicObfuscation = skidInvokeDynamicObfuscation; this.skidFlowExceptionMode = skidFlowExceptionMode; this.javaFlowSettings = javaFlowSettings == null ? JavaFlowSettings.createDefault() : javaFlowSettings; + this.jvmRenamerConfig = jvmRenamerConfig == null ? JvmRenamerConfig.disabled() : jvmRenamerConfig; } // Getters for basic settings @@ -116,6 +120,8 @@ public ObfuscatorConfig(Path inputJarPath, Path outputDir, List inputLibs, public FlowExceptionMode getSkidFlowExceptionMode() { return skidFlowExceptionMode; } public JavaFlowSettings getJavaFlowSettings() { return javaFlowSettings; } + public JvmRenamerConfig getJvmRenamerConfig() { return jvmRenamerConfig; } + // Convenience methods for accessing nested configuration properties public boolean isVirtualizationEnabled() { return protectionConfig.isVirtualizationEnabled(); } public boolean isJitEnabled() { return protectionConfig.isJitEnabled(); } @@ -161,6 +167,7 @@ public String toString() { " skidInvokeDynamicObfuscation=%s,\n" + " skidFlowExceptionMode=%s,\n" + " javaFlowSettings=%s,\n" + + " jvmRenamerConfigEnabled=%s,\n" + " protectionConfig=%s,\n" + " antiDebugConfig=%s\n" + "}", @@ -170,6 +177,7 @@ public String toString() { skidInvokeDynamicObfuscation, skidFlowExceptionMode, javaFlowSettings, + jvmRenamerConfig != null && jvmRenamerConfig.isEnabled(), protectionConfig, antiDebugConfig); } @@ -202,6 +210,7 @@ public static class Builder { private boolean skidInvokeDynamicObfuscation = false; private FlowExceptionMode skidFlowExceptionMode = FlowExceptionMode.STANDARD; private JavaFlowSettings javaFlowSettings = JavaFlowSettings.createDefault(); + private JvmRenamerConfig jvmRenamerConfig = JvmRenamerConfig.disabled(); public Builder setInputJarPath(Path inputJarPath) { this.inputJarPath = inputJarPath; @@ -330,6 +339,13 @@ public Builder setJavaFlowSettings(JavaFlowSettings javaFlowSettings) { return this; } + public Builder setJvmRenamerConfig(JvmRenamerConfig jvmRenamerConfig) { + if (jvmRenamerConfig != null) { + this.jvmRenamerConfig = jvmRenamerConfig; + } + return this; + } + public ObfuscatorConfig build() { if (inputJarPath == null || outputDir == null) { throw new IllegalArgumentException("Input JAR path and output directory are required"); @@ -340,7 +356,8 @@ public ObfuscatorConfig build() { javaBlackList, javaWhiteList, enableNativeObfuscation, skidStringObfuscation, skidNumberObfuscation, skidFlowObfuscation, skidSdkInjection, skidVmHashing, - skidInvokeDynamicObfuscation, skidFlowExceptionMode, javaFlowSettings); + skidInvokeDynamicObfuscation, skidFlowExceptionMode, javaFlowSettings, + jvmRenamerConfig); } } } diff --git a/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerConfig.java b/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerConfig.java new file mode 100644 index 0000000..166a89b --- /dev/null +++ b/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerConfig.java @@ -0,0 +1,84 @@ +package by.radioegor146.javaobf; + +import java.util.Objects; + +/** + * Aggregated JVM renamer configuration for UI/CLI flows. + */ +public final class JvmRenamerConfig { + private final boolean enabled; + private final JvmRenamerEntityConfig classConfig; + private final JvmRenamerEntityConfig methodConfig; + private final JvmRenamerEntityConfig fieldConfig; + + private JvmRenamerConfig(Builder builder) { + this.enabled = builder.enabled; + this.classConfig = builder.classConfig; + this.methodConfig = builder.methodConfig; + this.fieldConfig = builder.fieldConfig; + } + + public static Builder builder() { + return new Builder(); + } + + public static JvmRenamerConfig disabled() { + return builder().enabled(false).build(); + } + + public boolean isEnabled() { + return enabled; + } + + public JvmRenamerEntityConfig getClassConfig() { + return classConfig; + } + + public JvmRenamerEntityConfig getMethodConfig() { + return methodConfig; + } + + public JvmRenamerEntityConfig getFieldConfig() { + return fieldConfig; + } + + public boolean isAnyEntityEnabled() { + return (classConfig != null && classConfig.isEnabled()) + || (methodConfig != null && methodConfig.isEnabled()) + || (fieldConfig != null && fieldConfig.isEnabled()); + } + + public static final class Builder { + private boolean enabled = false; + private JvmRenamerEntityConfig classConfig = JvmRenamerEntityConfig.disabled(); + private JvmRenamerEntityConfig methodConfig = JvmRenamerEntityConfig.disabled(); + private JvmRenamerEntityConfig fieldConfig = JvmRenamerEntityConfig.disabled(); + + private Builder() { + } + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder classConfig(JvmRenamerEntityConfig classConfig) { + this.classConfig = Objects.requireNonNull(classConfig, "classConfig"); + return this; + } + + public Builder methodConfig(JvmRenamerEntityConfig methodConfig) { + this.methodConfig = Objects.requireNonNull(methodConfig, "methodConfig"); + return this; + } + + public Builder fieldConfig(JvmRenamerEntityConfig fieldConfig) { + this.fieldConfig = Objects.requireNonNull(fieldConfig, "fieldConfig"); + return this; + } + + public JvmRenamerConfig build() { + return new JvmRenamerConfig(this); + } + } +} diff --git a/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerEntityConfig.java b/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerEntityConfig.java new file mode 100644 index 0000000..7207f46 --- /dev/null +++ b/obfuscator/src/main/java/by/radioegor146/javaobf/JvmRenamerEntityConfig.java @@ -0,0 +1,128 @@ +package by.radioegor146.javaobf; + +import dev.skidfuscator.obfuscator.renamer.config.RenamerMode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * UI-facing configuration describing how a single JVM entity type should be renamed. + */ +public final class JvmRenamerEntityConfig { + private final boolean enabled; + private final RenamerMode mode; + private final List alphabet; + private final int depth; + private final String prefix; + private final Integer directoryDepth; + private final Integer segmentLength; + + private JvmRenamerEntityConfig(Builder builder) { + this.enabled = builder.enabled; + this.mode = builder.mode; + this.alphabet = Collections.unmodifiableList(new ArrayList<>(builder.alphabet)); + this.depth = builder.depth; + this.prefix = builder.prefix; + this.directoryDepth = builder.directoryDepth; + this.segmentLength = builder.segmentLength; + } + + public static Builder builder() { + return new Builder(); + } + + public static JvmRenamerEntityConfig disabled() { + return builder().enabled(false).build(); + } + + public boolean isEnabled() { + return enabled; + } + + public RenamerMode getMode() { + return mode; + } + + public List getAlphabet() { + return alphabet; + } + + public int getDepth() { + return depth; + } + + public String getPrefix() { + return prefix; + } + + public Integer getDirectoryDepth() { + return directoryDepth; + } + + public Integer getSegmentLength() { + return segmentLength; + } + + public static final class Builder { + private boolean enabled = false; + private RenamerMode mode = RenamerMode.ALPHABETICAL; + private List alphabet = new ArrayList<>(); + private int depth = 3; + private String prefix = null; + private Integer directoryDepth = null; + private Integer segmentLength = null; + + private Builder() { + } + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder mode(RenamerMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + return this; + } + + public Builder alphabet(List alphabet) { + this.alphabet = new ArrayList<>(alphabet == null ? List.of() : alphabet); + return this; + } + + public Builder depth(int depth) { + this.depth = Math.max(1, depth); + return this; + } + + public Builder prefix(String prefix) { + String trimmed = prefix == null ? null : prefix.trim(); + this.prefix = (trimmed == null || trimmed.isEmpty()) ? null : trimmed; + return this; + } + + public Builder directoryDepth(Integer directoryDepth) { + if (directoryDepth == null) { + this.directoryDepth = null; + } else { + this.directoryDepth = Math.max(1, directoryDepth); + } + return this; + } + + public Builder segmentLength(Integer segmentLength) { + if (segmentLength == null) { + this.segmentLength = null; + } else { + this.segmentLength = Math.max(1, segmentLength); + } + return this; + } + + public JvmRenamerEntityConfig build() { + return new JvmRenamerEntityConfig(this); + } + } +} diff --git a/obfuscator/src/main/java/by/radioegor146/ui/ObfuscatorFrame.java b/obfuscator/src/main/java/by/radioegor146/ui/ObfuscatorFrame.java index be0d6d8..0267deb 100644 --- a/obfuscator/src/main/java/by/radioegor146/ui/ObfuscatorFrame.java +++ b/obfuscator/src/main/java/by/radioegor146/ui/ObfuscatorFrame.java @@ -7,9 +7,12 @@ import by.radioegor146.Platform; import by.radioegor146.ProtectionConfig; import by.radioegor146.javaobf.JavaObfuscationConfig; +import by.radioegor146.javaobf.JvmRenamerConfig; +import by.radioegor146.javaobf.JvmRenamerEntityConfig; import dev.skidfuscator.obfuscator.FlowExceptionMode; import dev.skidfuscator.obfuscator.transform.impl.flow.FlowObfuscationMode; import dev.skidfuscator.obfuscator.transform.impl.flow.FlowObfuscationProfile; +import dev.skidfuscator.obfuscator.renamer.config.RenamerMode; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -28,6 +31,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.prefs.Preferences; @@ -106,6 +110,22 @@ public class ObfuscatorFrame extends JFrame { private final JCheckBox javaVmHashingBox = new JCheckBox("VM hashing (experimental)", false); private final JTextField javaBlacklistField = new JTextField(); private final JTextField javaWhitelistField = new JTextField(); + private final JCheckBox enableJvmRenamerBox = new JCheckBox("Enable JVM identifier renamer"); + private final JCheckBox classRenamerBox = new JCheckBox("Rename classes", true); + private final JComboBox classRenamerModeCombo = new JComboBox<>(RenamerMode.values()); + private final JSpinner classRenamerDepthSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 64, 1)); + private final JSpinner classRenamerDirDepthSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 8, 1)); + private final JSpinner classRenamerSegmentSpinner = new JSpinner(new SpinnerNumberModel(1, 1, 16, 1)); + private final JTextField classRenamerPrefixField = new JTextField(); + private final JTextField classRenamerCharsField = new JTextField(); + private final JCheckBox methodRenamerBox = new JCheckBox("Rename methods", true); + private final JComboBox methodRenamerModeCombo = new JComboBox<>(RenamerMode.values()); + private final JSpinner methodRenamerDepthSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 32, 1)); + private final JTextField methodRenamerCharsField = new JTextField(); + private final JCheckBox fieldRenamerBox = new JCheckBox("Rename fields", true); + private final JComboBox fieldRenamerModeCombo = new JComboBox<>(RenamerMode.values()); + private final JSpinner fieldRenamerDepthSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 32, 1)); + private final JTextField fieldRenamerCharsField = new JTextField(); private final JButton runButton = new JButton("▶ Run Obfuscation"); private final JTextArea logArea = new JTextArea(); private final JProgressBar progressBar = new JProgressBar(); @@ -585,6 +605,10 @@ public Component getListCellRendererComponent(JList list, Object value, int i form.add(featurePanel); form.add(Box.createRigidArea(new Dimension(0, 12))); + JPanel renamerPanel = buildJvmRenamerPanel(); + form.add(renamerPanel); + form.add(Box.createRigidArea(new Dimension(0, 12))); + // Java Filter Files JPanel filterPanel = new JPanel(); filterPanel.setLayout(new BoxLayout(filterPanel, BoxLayout.Y_AXIS)); @@ -614,6 +638,8 @@ public Component getListCellRendererComponent(JList list, Object value, int i javaSdkInjectionBox.setEnabled(enabled); javaVmHashingBox.setEnabled(enabled); updateFlowControls(); + enableJvmRenamerBox.setEnabled(enabled); + updateRenamerControls(); }); javaFlowObfBox.addActionListener(e -> updateFlowControls()); @@ -636,6 +662,8 @@ public Component getListCellRendererComponent(JList list, Object value, int i javaSdkInjectionBox.setEnabled(false); javaVmHashingBox.setEnabled(false); updateFlowControls(); + enableJvmRenamerBox.setEnabled(false); + updateRenamerControls(); JScrollPane sc = new JScrollPane(form); sc.setBorder(null); @@ -765,6 +793,94 @@ private JPanel createFlowRow(String labelText, JComponent field, String hintText return row; } + private JPanel buildJvmRenamerPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(new TitledBorder("🔤 JVM Renamer")); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + + panel.add(checkWithHint(enableJvmRenamerBox, + "Rename classes, methods, and fields after bytecode transforms")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(indent(buildClassRenamerPanel(), 16)); + panel.add(Box.createRigidArea(new Dimension(0, 8))); + panel.add(indent(buildMemberRenamerPanel("Method names", + "Rename declared methods and constructors", + methodRenamerBox, methodRenamerModeCombo, + methodRenamerDepthSpinner, methodRenamerCharsField), 16)); + panel.add(Box.createRigidArea(new Dimension(0, 8))); + panel.add(indent(buildMemberRenamerPanel("Field names", + "Rename declared fields", + fieldRenamerBox, fieldRenamerModeCombo, + fieldRenamerDepthSpinner, fieldRenamerCharsField), 16)); + + enableJvmRenamerBox.addActionListener(e -> updateRenamerControls()); + classRenamerBox.addActionListener(e -> updateRenamerControls()); + methodRenamerBox.addActionListener(e -> updateRenamerControls()); + fieldRenamerBox.addActionListener(e -> updateRenamerControls()); + + classRenamerModeCombo.setSelectedItem(RenamerMode.ALPHABETICAL); + methodRenamerModeCombo.setSelectedItem(RenamerMode.ALPHABETICAL); + fieldRenamerModeCombo.setSelectedItem(RenamerMode.ALPHABETICAL); + + return panel; + } + + private JPanel buildClassRenamerPanel() { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(new TitledBorder("Class names")); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + + panel.add(checkWithHint(classRenamerBox, + "Rename application classes with optional package prefixing")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Mode", classRenamerModeCombo, + "Alphabetical, numeric, or custom token order")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Depth", classRenamerDepthSpinner, + "Minimum length for generated class names")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Directory depth", classRenamerDirDepthSpinner, + "Number of nested package segments to generate")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Segment length", classRenamerSegmentSpinner, + "Characters per generated package segment")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFieldRowPanel("Prefix path", classRenamerPrefixField, + "Optional package prefix (use '/' or '.' separators)")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFieldRowPanel("Character tokens", classRenamerCharsField, + "Comma/space separated tokens; leave blank for defaults")); + + return panel; + } + + private JPanel buildMemberRenamerPanel(String title, + String hint, + JCheckBox toggle, + JComboBox modeCombo, + JSpinner depthSpinner, + JTextField charsField) { + JPanel panel = new JPanel(); + panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); + panel.setBorder(new TitledBorder(title)); + panel.setAlignmentX(Component.LEFT_ALIGNMENT); + + panel.add(checkWithHint(toggle, hint)); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Mode", modeCombo, + "Alphabetical, numeric, or custom token order")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFlowRow("Depth", depthSpinner, + "Minimum length for generated identifiers")); + panel.add(Box.createRigidArea(new Dimension(0, 6))); + panel.add(createFieldRowPanel("Character tokens", charsField, + "Comma/space separated tokens; leave blank for defaults")); + + return panel; + } + private JPanel buildFlowProfilePanel() { JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); @@ -876,6 +992,24 @@ private String describeFlowOps() { return String.join("/", ops); } + private List parseAlphabet(String value) { + if (value == null) { + return List.of(); + } + String trimmed = value.trim(); + if (trimmed.isEmpty()) { + return List.of(); + } + LinkedHashSet tokens = new LinkedHashSet<>(); + for (String token : trimmed.split("[,\\s]+")) { + String t = token.trim(); + if (!t.isEmpty()) { + tokens.add(t); + } + } + return new ArrayList<>(tokens); + } + private JavaFlowSettings collectJavaFlowSettings() { if (!enableJavaObfuscationBox.isSelected() || !javaFlowObfBox.isSelected()) { return JavaFlowSettings.createDefault(); @@ -901,6 +1035,56 @@ private JavaFlowSettings collectJavaFlowSettings() { .build(); } + private JvmRenamerConfig collectJvmRenamerConfig() { + boolean javaEnabled = enableJavaObfuscationBox.isSelected(); + boolean renamerMaster = javaEnabled && enableJvmRenamerBox.isSelected(); + + JvmRenamerEntityConfig classConfig = JvmRenamerEntityConfig.builder() + .enabled(renamerMaster && classRenamerBox.isSelected()) + .mode((RenamerMode) classRenamerModeCombo.getSelectedItem()) + .depth((Integer) classRenamerDepthSpinner.getValue()) + .prefix(classRenamerPrefixField.getText()) + .directoryDepth((Integer) classRenamerDirDepthSpinner.getValue()) + .segmentLength((Integer) classRenamerSegmentSpinner.getValue()) + .alphabet(parseAlphabet(classRenamerCharsField.getText())) + .build(); + + JvmRenamerEntityConfig methodConfig = JvmRenamerEntityConfig.builder() + .enabled(renamerMaster && methodRenamerBox.isSelected()) + .mode((RenamerMode) methodRenamerModeCombo.getSelectedItem()) + .depth((Integer) methodRenamerDepthSpinner.getValue()) + .alphabet(parseAlphabet(methodRenamerCharsField.getText())) + .build(); + + JvmRenamerEntityConfig fieldConfig = JvmRenamerEntityConfig.builder() + .enabled(renamerMaster && fieldRenamerBox.isSelected()) + .mode((RenamerMode) fieldRenamerModeCombo.getSelectedItem()) + .depth((Integer) fieldRenamerDepthSpinner.getValue()) + .alphabet(parseAlphabet(fieldRenamerCharsField.getText())) + .build(); + + return JvmRenamerConfig.builder() + .enabled(renamerMaster) + .classConfig(classConfig) + .methodConfig(methodConfig) + .fieldConfig(fieldConfig) + .build(); + } + + private String summarizeRenamerEntity(String label, JvmRenamerEntityConfig entity) { + if (entity == null || !entity.isEnabled()) { + return label + ": Off"; + } + StringBuilder sb = new StringBuilder(); + sb.append(label).append(':').append(' ') + .append(entity.getMode()) + .append(" (depth=").append(entity.getDepth()).append(')'); + if (entity.getPrefix() != null && !entity.getPrefix().isEmpty()) { + sb.append(", prefix=").append(entity.getPrefix()); + } + return sb.toString(); + } + private int measureHintWidth(JLabel lbl) { @@ -1166,6 +1350,9 @@ private void runObfuscation(ActionEvent e) { .map(String::trim).filter(s -> !s.isEmpty()).collect(Collectors.toList()); } + JavaFlowSettings flowSettings = collectJavaFlowSettings(); + JvmRenamerConfig renamerConfig = collectJvmRenamerConfig(); + publish("🛡️ Protection settings:"); publish(" 🔧 Native Obfuscation: " + (enableNativeObfuscation ? "✅ Enabled" : "❌ Disabled")); if (enableNativeObfuscation) { @@ -1193,6 +1380,13 @@ private void runObfuscation(ActionEvent e) { publish(" • InvokeDynamic references: " + (javaInvokeDynamicBox.isSelected() ? "✅ On" : "❌ Off")); publish(" • SDK injection: " + (javaSdkInjectionBox.isSelected() ? "✅ On" : "❌ Off")); publish(" • VM hashing: " + (javaVmHashingBox.isSelected() ? "✅ On" : "❌ Off")); + boolean renamerActive = renamerConfig.isEnabled() && renamerConfig.isAnyEntityEnabled(); + publish(" • JVM renamer: " + (renamerActive ? "✅ On" : "❌ Off")); + if (renamerActive) { + publish(" ↳ " + summarizeRenamerEntity("Classes", renamerConfig.getClassConfig())); + publish(" ↳ " + summarizeRenamerEntity("Methods", renamerConfig.getMethodConfig())); + publish(" ↳ " + summarizeRenamerEntity("Fields", renamerConfig.getFieldConfig())); + } } publish(" 🛡️ Anti-Debug Protection: " + (enableAntiDebug ? "✅ Enabled" : "❌ Disabled")); if (enableAntiDebug) { @@ -1264,7 +1458,8 @@ private void runObfuscation(ActionEvent e) { .setSkidFlowExceptionMode((FlowExceptionMode) javaFlowExceptionModeCombo.getSelectedItem()) .setSkidSdkInjection(javaSdkInjectionBox.isSelected()) .setSkidVmHashing(javaVmHashingBox.isSelected()) - .setJavaFlowSettings(collectJavaFlowSettings()) + .setJavaFlowSettings(flowSettings) + .setJvmRenamerConfig(renamerConfig) .build(); // Validate configuration @@ -1350,6 +1545,8 @@ private void setFormEnabled(boolean enabled) { javaSdkInjectionBox.setEnabled(enabled && enableJavaObfuscationBox.isSelected()); javaVmHashingBox.setEnabled(enabled && enableJavaObfuscationBox.isSelected()); updateFlowControls(); + enableJvmRenamerBox.setEnabled(enabled && enableJavaObfuscationBox.isSelected()); + updateRenamerControls(); // Anti-debug controls enableAntiDebugBox.setEnabled(enabled); @@ -1371,6 +1568,38 @@ private void updateFlowControls() { flowOpXorCheck.setEnabled(flowControlsEnabled); flowOpAddCheck.setEnabled(flowControlsEnabled); } + + private void updateRenamerControls() { + boolean javaEnabled = enableJavaObfuscationBox.isEnabled() && enableJavaObfuscationBox.isSelected(); + enableJvmRenamerBox.setEnabled(javaEnabled); + + boolean masterEnabled = javaEnabled && enableJvmRenamerBox.isSelected(); + + setRenamerControlsEnabled(classRenamerBox, masterEnabled, + classRenamerModeCombo, classRenamerDepthSpinner, + classRenamerDirDepthSpinner, classRenamerSegmentSpinner, + classRenamerPrefixField, classRenamerCharsField); + + setRenamerControlsEnabled(methodRenamerBox, masterEnabled, + methodRenamerModeCombo, methodRenamerDepthSpinner, + methodRenamerCharsField); + + setRenamerControlsEnabled(fieldRenamerBox, masterEnabled, + fieldRenamerModeCombo, fieldRenamerDepthSpinner, + fieldRenamerCharsField); + } + + private void setRenamerControlsEnabled(JCheckBox toggle, + boolean masterEnabled, + JComponent... components) { + toggle.setEnabled(masterEnabled); + boolean entityEnabled = masterEnabled && toggle.isSelected(); + for (JComponent component : components) { + if (component != null) { + component.setEnabled(entityEnabled); + } + } + } private void updateAntiDebugControls(boolean formEnabled) { boolean antiDebugSelected = enableAntiDebugBox.isSelected(); boolean antiDebugEnabled = formEnabled && antiDebugSelected; diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java index c684729..f09ed00 100644 --- a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/Skidfuscator.java @@ -40,7 +40,9 @@ import dev.skidfuscator.obfuscator.protection.ProtectionProvider; import dev.skidfuscator.obfuscator.protection.TokenLoggerProtectionProvider; import dev.skidfuscator.obfuscator.protection.MinecraftStealerProtectionProvider; +import dev.skidfuscator.obfuscator.renamer.JvmRenamer; import dev.skidfuscator.obfuscator.renamer.SkidRemapper; +import dev.skidfuscator.obfuscator.renamer.config.RenamerSettings; import dev.skidfuscator.obfuscator.resolver.SkidInvocationResolver; import dev.skidfuscator.obfuscator.skidasm.SkidClassNode; import dev.skidfuscator.obfuscator.skidasm.SkidGroup; @@ -133,6 +135,7 @@ public class Skidfuscator { private ExemptManager exemptAnalysis; private Config tsConfig; private DefaultSkidConfig config; + private RenamerSettings renamerSettings; private PredicateAnalysis predicateAnalysis; private final SkidRemapper classRemapper = new SkidRemapper(new HashMap<>()); @@ -380,6 +383,8 @@ public void run() { LOGGER.log("Finished dumping classes..."); EventBus.end(); + runJvmRenamer(); + _cleanup(); _dump(); @@ -480,6 +485,7 @@ public void _importConfig() { ? ConfigFactory.parseString("").resolve() : ConfigFactory.parseFile(session.getConfig()).resolve(); this.config = new DefaultSkidConfig(tsConfig, ""); + this.renamerSettings = RenamerSettings.from(tsConfig, session); progressBar.tick(); } @@ -986,6 +992,26 @@ protected void _cleanup() { System.gc(); } + private void runJvmRenamer() { + if (renamerSettings == null || !renamerSettings.isAnyEnabled()) { + return; + } + + LOGGER.post("Running JVM renamer..."); + JvmRenamer.RenamerResult result = new JvmRenamer(this, renamerSettings).execute(); + + if (result.hasChanges()) { + LOGGER.log(String.format( + "Renamed %d classes, %d methods, %d fields.", + result.getClassesRenamed(), + result.getMethodsRenamed(), + result.getFieldsRenamed() + )); + } else { + LOGGER.log("No JVM renaming changes were required."); + } + } + protected void _dump() { LOGGER.post("Dumping jar..."); try { diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/SkidfuscatorSession.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/SkidfuscatorSession.java index e0e2550..8423b0f 100644 --- a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/SkidfuscatorSession.java +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/SkidfuscatorSession.java @@ -136,6 +136,10 @@ public boolean isDebug() { return debug; } + public boolean isRenamer() { + return renamer; + } + public boolean isSkidStringObfuscationEnabled() { return skidStringObfuscation; } diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/JvmRenamer.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/JvmRenamer.java new file mode 100644 index 0000000..20bd604 --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/JvmRenamer.java @@ -0,0 +1,731 @@ +package dev.skidfuscator.obfuscator.renamer; + +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.exempt.ExemptManager; +import dev.skidfuscator.obfuscator.renamer.config.EntityRenamerSettings; +import dev.skidfuscator.obfuscator.renamer.config.RenamerSettings; +import dev.skidfuscator.obfuscator.renamer.generator.NameGenerator; +import dev.skidfuscator.obfuscator.renamer.generator.RadixNameGenerator; +import dev.skidfuscator.obfuscator.skidasm.SkidClassNode; +import dev.skidfuscator.obfuscator.skidasm.SkidFieldNode; +import dev.skidfuscator.obfuscator.skidasm.SkidMethodNode; +import org.mapleir.asm.ClassNode; +import org.mapleir.asm.FieldNode; +import org.mapleir.asm.MethodNode; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.SourceInterpreter; +import org.objectweb.asm.tree.analysis.SourceValue; +import org.topdank.byteengineer.commons.data.JarClassData; +import org.topdank.byteengineer.commons.data.JarResource; + +import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public final class JvmRenamer { + private final Skidfuscator skidfuscator; + private final RenamerSettings settings; + private static final String MAIN_METHOD_DESCRIPTOR = "([Ljava/lang/String;)V"; + + public JvmRenamer(Skidfuscator skidfuscator, RenamerSettings settings) { + this.skidfuscator = skidfuscator; + this.settings = settings; + } + + public RenamerResult execute() { + if (settings == null || !settings.isAnyEnabled()) { + return RenamerResult.empty(); + } + + List classes = collectClasses(); + if (classes.isEmpty()) { + return RenamerResult.empty(); + } + + Map classIndex = new HashMap<>(); + for (SkidClassNode classNode : classes) { + classIndex.put(classNode.getName(), classNode); + } + + EntryPoint entryPoint = readManifestEntryPoint(classIndex); + + Map classMappings = new LinkedHashMap<>(); + Map methodMappings = new LinkedHashMap<>(); + Map fieldMappings = new LinkedHashMap<>(); + + renameClasses(classes, classMappings); + renameMethods(classes, methodMappings); + renameFields(classes, fieldMappings); + + if (classMappings.isEmpty() && methodMappings.isEmpty() && fieldMappings.isEmpty()) { + return RenamerResult.empty(); + } + + rewriteReflectionSites(classes, classMappings, methodMappings, fieldMappings); + updateManifest(classMappings, methodMappings, entryPoint); + + return new RenamerResult(classMappings.size(), methodMappings.size(), fieldMappings.size()); + } + + private EntryPoint readManifestEntryPoint(Map classIndex) { + JarResource manifestResource = skidfuscator.getJarContents() + .getResourceContents() + .namedMap() + .get(JarFile.MANIFEST_NAME); + + if (manifestResource == null) { + return null; + } + + try (ByteArrayInputStream input = new ByteArrayInputStream(manifestResource.getData())) { + Manifest manifest = new Manifest(input); + Attributes attributes = manifest.getMainAttributes(); + String rawMainClass = attributes.getValue(Attributes.Name.MAIN_CLASS); + if (rawMainClass == null || rawMainClass.isEmpty()) { + return null; + } + + String owner = rawMainClass; + String method = "main"; + + String internalOwner = owner.replace('.', '/'); + if (!classIndex.containsKey(internalOwner)) { + int split = rawMainClass.lastIndexOf('.'); + if (split > 0) { + owner = rawMainClass.substring(0, split); + method = rawMainClass.substring(split + 1); + internalOwner = owner.replace('.', '/'); + } + } + + SkidClassNode classNode = classIndex.get(internalOwner); + String descriptor = null; + if (classNode != null) { + for (MethodNode methodNode : classNode.getMethods()) { + if (methodNode.getName().equals(method)) { + descriptor = methodNode.getDesc(); + break; + } + } + } + + return new EntryPoint(internalOwner, method, descriptor != null ? descriptor : MAIN_METHOD_DESCRIPTOR); + } catch (IOException exception) { + throw new IllegalStateException("Failed to inspect manifest before renaming", exception); + } + } + + private List collectClasses() { + List result = new ArrayList<>(); + for (ClassNode node : skidfuscator.getClassSource().iterate()) { + if (node instanceof SkidClassNode) { + result.add((SkidClassNode) node); + } + } + return result; + } + + private void renameClasses(List classes, Map classMappings) { + EntityRenamerSettings classSettings = settings.getClassSettings(); + if (!classSettings.isEnabled()) { + return; + } + + Set usedNames = new HashSet<>(); + for (JarClassData classData : skidfuscator.getJarContents().getClassContents()) { + String name = classData.getName(); + if (name.endsWith(".class")) { + name = name.substring(0, name.length() - 6); + } + usedNames.add(name.replace('.', '/')); + } + for (SkidClassNode classNode : classes) { + usedNames.add(classNode.getName()); + } + NameGenerator generator = new RadixNameGenerator(classSettings.getAlphabet(), classSettings.getMinLength()); + ExemptManager exemptManager = skidfuscator.getExemptAnalysis(); + + for (SkidClassNode classNode : classes) { + if (!shouldRenameClass(classNode, exemptManager)) { + continue; + } + + String newName = nextUniqueClassName(generator, classSettings, usedNames); + classMappings.put(classNode.getName(), newName); + skidfuscator.getClassRemapper().add(classNode.getName(), newName); + } + } + + private void renameMethods(List classes, Map methodMappings) { + EntityRenamerSettings methodSettings = settings.getMethodSettings(); + if (!methodSettings.isEnabled()) { + return; + } + + ExemptManager exemptManager = skidfuscator.getExemptAnalysis(); + + for (SkidClassNode classNode : classes) { + if (skidfuscator.getClassSource().isLibraryClass(classNode.getName()) || exemptManager.isExempt(classNode)) { + continue; + } + + NameGenerator generator = new RadixNameGenerator(methodSettings.getAlphabet(), methodSettings.getMinLength()); + Set usedNames = new HashSet<>(); + + for (MethodNode method : classNode.getMethods()) { + SkidMethodNode skidMethod = method instanceof SkidMethodNode ? (SkidMethodNode) method : new SkidMethodNode(method.node, classNode, skidfuscator); + if (!shouldRenameMethod(skidMethod, exemptManager)) { + continue; + } + + String newName = nextUniqueName(generator, usedNames); + methodMappings.put(new MethodSignature(classNode.getName(), skidMethod.getName(), skidMethod.node.desc), newName); + skidfuscator.getClassRemapper().add(classNode.getName() + '.' + skidMethod.getName() + skidMethod.node.desc, newName); + } + } + } + + private void renameFields(List classes, Map fieldMappings) { + EntityRenamerSettings fieldSettings = settings.getFieldSettings(); + if (!fieldSettings.isEnabled()) { + return; + } + + ExemptManager exemptManager = skidfuscator.getExemptAnalysis(); + + for (SkidClassNode classNode : classes) { + if (skidfuscator.getClassSource().isLibraryClass(classNode.getName()) || exemptManager.isExempt(classNode)) { + continue; + } + + NameGenerator generator = new RadixNameGenerator(fieldSettings.getAlphabet(), fieldSettings.getMinLength()); + Set usedNames = new HashSet<>(); + + for (FieldNode field : classNode.getFields()) { + SkidFieldNode skidField = field instanceof SkidFieldNode ? (SkidFieldNode) field : new SkidFieldNode(field.node, classNode, skidfuscator); + String newName = nextUniqueName(generator, usedNames); + fieldMappings.put(new FieldSignature(classNode.getName(), skidField.getName(), skidField.node.desc), newName); + skidfuscator.getClassRemapper().add(classNode.getName() + '.' + skidField.getName(), newName); + } + } + } + + private boolean shouldRenameClass(SkidClassNode classNode, ExemptManager exemptManager) { + if (skidfuscator.getClassSource().isLibraryClass(classNode.getName())) { + return false; + } + if (exemptManager.isExempt(classNode)) { + return false; + } + return true; + } + + private boolean shouldRenameMethod(SkidMethodNode methodNode, ExemptManager exemptManager) { + if (methodNode.getName().equals("") || methodNode.getName().equals("")) { + return false; + } + if (exemptManager.isExempt(methodNode.owner) || exemptManager.isExempt(methodNode)) { + return false; + } + return true; + } + + private String nextUniqueClassName(NameGenerator generator, EntityRenamerSettings settings, Set usedNames) { + while (true) { + String candidate = formatClassName(generator.next(), settings); + if (usedNames.add(candidate)) { + return candidate; + } + } + } + + private String formatClassName(String base, EntityRenamerSettings settings) { + String prefix = settings.getPrefix(); + int depth = settings.getPackageDepth(); + int segmentLength = settings.getSegmentLength(); + String fallback = settings.getFallbackToken(); + + StringBuilder working = new StringBuilder(base); + int required = segmentLength * Math.max(0, depth - 1) + 1; + while (working.length() < required) { + working.append(fallback); + } + + List segments = new ArrayList<>(); + int index = 0; + for (int i = 0; i < depth - 1; i++) { + segments.add(working.substring(index, index + segmentLength)); + index += segmentLength; + } + segments.add(working.substring(index)); + + StringBuilder builder = new StringBuilder(); + builder.append(prefix); + for (int i = 0; i < segments.size(); i++) { + if (segments.get(i).isEmpty()) { + builder.append(fallback); + } else { + builder.append(segments.get(i)); + } + if (i + 1 < segments.size()) { + builder.append('/'); + } + } + return builder.toString(); + } + + private String nextUniqueName(NameGenerator generator, Set usedNames) { + while (true) { + String candidate = generator.next(); + if (usedNames.add(candidate)) { + return candidate; + } + } + } + + private void rewriteReflectionSites(List classes, + Map classMappings, + Map methodMappings, + Map fieldMappings) { + if (classMappings.isEmpty() && methodMappings.isEmpty() && fieldMappings.isEmpty()) { + return; + } + + Map>> methodsByOwner = new HashMap<>(); + for (Map.Entry entry : methodMappings.entrySet()) { + methodsByOwner.computeIfAbsent(entry.getKey().owner, key -> new ArrayList<>()).add(entry); + } + + Map>> fieldsByOwner = new HashMap<>(); + for (Map.Entry entry : fieldMappings.entrySet()) { + fieldsByOwner.computeIfAbsent(entry.getKey().owner, key -> new ArrayList<>()).add(entry); + } + + IdentityHashMap classLoadOrigins = new IdentityHashMap<>(); + + for (SkidClassNode classNode : classes) { + for (MethodNode method : classNode.getMethods()) { + org.objectweb.asm.tree.MethodNode asmMethod = method.node; + if (asmMethod.instructions == null || asmMethod.instructions.size() == 0) { + continue; + } + + Analyzer analyzer = new Analyzer<>(new SourceInterpreter()); + Frame[] frames; + try { + frames = analyzer.analyze(classNode.getName(), asmMethod); + } catch (AnalyzerException exception) { + continue; + } + + for (int index = 0; index < asmMethod.instructions.size(); index++) { + AbstractInsnNode insn = asmMethod.instructions.get(index); + if (!(insn instanceof MethodInsnNode)) { + continue; + } + + MethodInsnNode call = (MethodInsnNode) insn; + Frame frame = frames[index]; + if (frame == null) { + continue; + } + + if (isClassForName(call)) { + handleClassForName(call, frame, classMappings, classLoadOrigins); + continue; + } + + if (isClassLoaderLoadClass(call)) { + handleClassLoaderLoadClass(call, frame, classMappings, classLoadOrigins); + continue; + } + + if (isMethodLookup(call)) { + handleMethodLookup(call, frame, methodsByOwner, classLoadOrigins); + continue; + } + + if (isFieldLookup(call)) { + handleFieldLookup(call, frame, fieldsByOwner, classLoadOrigins); + } + } + } + } + } + + private void updateManifest(Map classMappings, + Map methodMappings, + EntryPoint entryPoint) { + if (entryPoint == null) { + return; + } + + JarResource manifestResource = skidfuscator.getJarContents() + .getResourceContents() + .namedMap() + .get(JarFile.MANIFEST_NAME); + + if (manifestResource == null) { + return; + } + + try (ByteArrayInputStream input = new ByteArrayInputStream(manifestResource.getData())) { + Manifest manifest = new Manifest(input); + Attributes attributes = manifest.getMainAttributes(); + String mainClass = attributes.getValue(Attributes.Name.MAIN_CLASS); + if (mainClass == null) { + return; + } + + String owner = entryPoint.owner; + String method = entryPoint.method; + String descriptor = entryPoint.descriptor; + + String mappedOwner = classMappings.getOrDefault(owner, owner); + String mappedMethod = methodMappings.getOrDefault(new MethodSignature(owner, method, descriptor), method); + + String manifestValue = Type.getObjectType(mappedOwner).getClassName() + '.' + mappedMethod; + attributes.putValue(Attributes.Name.MAIN_CLASS.toString(), manifestValue); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + manifest.write(output); + manifestResource.setData(output.toByteArray()); + } catch (IOException exception) { + throw new IllegalStateException("Failed to update manifest after renaming", exception); + } + } + + private boolean isClassForName(MethodInsnNode call) { + return call.getOpcode() == Opcodes.INVOKESTATIC + && "java/lang/Class".equals(call.owner) + && "forName".equals(call.name); + } + + private boolean isClassLoaderLoadClass(MethodInsnNode call) { + return call.getOpcode() == Opcodes.INVOKEVIRTUAL + && "java/lang/ClassLoader".equals(call.owner) + && "loadClass".equals(call.name); + } + + private boolean isMethodLookup(MethodInsnNode call) { + if (!"java/lang/Class".equals(call.owner)) { + return false; + } + return "getDeclaredMethod".equals(call.name) || "getMethod".equals(call.name); + } + + private boolean isFieldLookup(MethodInsnNode call) { + if (!"java/lang/Class".equals(call.owner)) { + return false; + } + return "getDeclaredField".equals(call.name) || "getField".equals(call.name); + } + + private void handleClassForName(MethodInsnNode call, + Frame frame, + Map classMappings, + IdentityHashMap classLoadOrigins) { + Type[] args = Type.getArgumentTypes(call.desc); + int stackIndex = frame.getStackSize() - args.length; + SourceValue stringValue = frame.getStack(stackIndex); + String original = extractStringLiteral(stringValue); + if (original == null) { + return; + } + + String internal = original.replace('.', '/'); + String mapped = classMappings.get(internal); + if (mapped == null) { + return; + } + + updateStringLiteral(stringValue, Type.getObjectType(mapped).getClassName()); + classLoadOrigins.put(call, internal); + } + + private void handleClassLoaderLoadClass(MethodInsnNode call, + Frame frame, + Map classMappings, + IdentityHashMap classLoadOrigins) { + Type[] args = Type.getArgumentTypes(call.desc); + int stackIndex = frame.getStackSize() - args.length; + SourceValue stringValue = frame.getStack(stackIndex); + String original = extractStringLiteral(stringValue); + if (original == null) { + return; + } + + String internal = original.replace('.', '/'); + String mapped = classMappings.get(internal); + if (mapped == null) { + return; + } + + updateStringLiteral(stringValue, Type.getObjectType(mapped).getClassName()); + classLoadOrigins.put(call, internal); + } + + private void handleMethodLookup(MethodInsnNode call, + Frame frame, + Map>> methodsByOwner, + IdentityHashMap classLoadOrigins) { + Type[] args = Type.getArgumentTypes(call.desc); + int argCount = args.length; + int firstArgIndex = frame.getStackSize() - argCount; + if (call.getOpcode() != Opcodes.INVOKESTATIC) { + firstArgIndex -= 1; + } + + if (firstArgIndex < 0) { + return; + } + + SourceValue nameValue = frame.getStack(firstArgIndex); + String originalName = extractStringLiteral(nameValue); + if (originalName == null) { + return; + } + + String owner = resolveOwnerInternal(frame, firstArgIndex, classLoadOrigins); + if (owner == null) { + return; + } + + List> candidates = methodsByOwner.get(owner); + if (candidates == null || candidates.isEmpty()) { + return; + } + + List> matches = new ArrayList<>(); + for (Map.Entry entry : candidates) { + if (entry.getKey().name.equals(originalName)) { + matches.add(entry); + } + } + + if (matches.isEmpty()) { + return; + } + + if (matches.size() != 1) { + return; + } + + updateStringLiteral(nameValue, matches.get(0).getValue()); + } + + private void handleFieldLookup(MethodInsnNode call, + Frame frame, + Map>> fieldsByOwner, + IdentityHashMap classLoadOrigins) { + Type[] args = Type.getArgumentTypes(call.desc); + int argCount = args.length; + int firstArgIndex = frame.getStackSize() - argCount; + if (call.getOpcode() != Opcodes.INVOKESTATIC) { + firstArgIndex -= 1; + } + + if (firstArgIndex < 0) { + return; + } + + SourceValue nameValue = frame.getStack(firstArgIndex); + String originalName = extractStringLiteral(nameValue); + if (originalName == null) { + return; + } + + String owner = resolveOwnerInternal(frame, firstArgIndex, classLoadOrigins); + if (owner == null) { + return; + } + + List> candidates = fieldsByOwner.get(owner); + if (candidates == null || candidates.isEmpty()) { + return; + } + + List> matches = new ArrayList<>(); + for (Map.Entry entry : candidates) { + if (entry.getKey().name.equals(originalName)) { + matches.add(entry); + } + } + + if (matches.size() != 1) { + return; + } + + updateStringLiteral(nameValue, matches.get(0).getValue()); + } + + private String resolveOwnerInternal(Frame frame, + int firstArgIndex, + IdentityHashMap classLoadOrigins) { + int ownerIndex = firstArgIndex - 1; + if (ownerIndex < 0) { + return null; + } + + SourceValue ownerValue = frame.getStack(ownerIndex); + if (ownerValue == null) { + return null; + } + + for (AbstractInsnNode source : ownerValue.insns) { + if (source instanceof LdcInsnNode) { + Object cst = ((LdcInsnNode) source).cst; + if (cst instanceof Type) { + return ((Type) cst).getInternalName(); + } + } + if (source instanceof MethodInsnNode) { + String mapped = classLoadOrigins.get(source); + if (mapped != null) { + return mapped; + } + } + } + + return null; + } + + private String extractStringLiteral(SourceValue value) { + if (value == null) { + return null; + } + for (AbstractInsnNode source : value.insns) { + if (source instanceof LdcInsnNode) { + Object cst = ((LdcInsnNode) source).cst; + if (cst instanceof String) { + return (String) cst; + } + } + } + return null; + } + + private void updateStringLiteral(SourceValue value, String updated) { + if (value == null) { + return; + } + for (AbstractInsnNode source : value.insns) { + if (source instanceof LdcInsnNode) { + LdcInsnNode ldc = (LdcInsnNode) source; + if (ldc.cst instanceof String) { + ldc.cst = updated; + return; + } + } + } + } + + public static final class RenamerResult { + private final int classesRenamed; + private final int methodsRenamed; + private final int fieldsRenamed; + + private RenamerResult(int classesRenamed, int methodsRenamed, int fieldsRenamed) { + this.classesRenamed = classesRenamed; + this.methodsRenamed = methodsRenamed; + this.fieldsRenamed = fieldsRenamed; + } + + public static RenamerResult empty() { + return new RenamerResult(0, 0, 0); + } + + public int getClassesRenamed() { + return classesRenamed; + } + + public int getMethodsRenamed() { + return methodsRenamed; + } + + public int getFieldsRenamed() { + return fieldsRenamed; + } + + public boolean hasChanges() { + return classesRenamed > 0 || methodsRenamed > 0 || fieldsRenamed > 0; + } + } + + private static final class EntryPoint { + private final String owner; + private final String method; + private final String descriptor; + + private EntryPoint(String owner, String method, String descriptor) { + this.owner = owner; + this.method = method; + this.descriptor = descriptor; + } + } + + private static final class MethodSignature { + private final String owner; + private final String name; + private final String desc; + + private MethodSignature(String owner, String name, String desc) { + this.owner = owner; + this.name = name; + this.desc = desc; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MethodSignature that = (MethodSignature) o; + return Objects.equals(owner, that.owner) && + Objects.equals(name, that.name) && + Objects.equals(desc, that.desc); + } + + @Override + public int hashCode() { + return Objects.hash(owner, name, desc); + } + } + + private static final class FieldSignature { + private final String owner; + private final String name; + private final String desc; + + private FieldSignature(String owner, String name, String desc) { + this.owner = owner; + this.name = name; + this.desc = desc; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FieldSignature that = (FieldSignature) o; + return Objects.equals(owner, that.owner) && + Objects.equals(name, that.name) && + Objects.equals(desc, that.desc); + } + + @Override + public int hashCode() { + return Objects.hash(owner, name, desc); + } + } +} diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/SkidRemapper.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/SkidRemapper.java index ecef9a7..4663708 100644 --- a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/SkidRemapper.java +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/SkidRemapper.java @@ -81,6 +81,10 @@ public void add(final String mapping, final String remapped) { this.mapping.put(mapping, remapped); } + public Map getMappings() { + return Collections.unmodifiableMap(mapping); + } + @Override public String toString() { return "SkidRemapper{\n" + diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/EntityRenamerSettings.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/EntityRenamerSettings.java new file mode 100644 index 0000000..4b50c1c --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/EntityRenamerSettings.java @@ -0,0 +1,125 @@ +package dev.skidfuscator.obfuscator.renamer.config; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public final class EntityRenamerSettings { + private final boolean enabled; + private final RenamerMode mode; + private final List alphabet; + private final int minLength; + private final String prefix; + private final int packageDepth; + private final int segmentLength; + private final String fallbackToken; + + private EntityRenamerSettings(Builder builder) { + this.enabled = builder.enabled; + this.mode = builder.mode; + this.alphabet = Collections.unmodifiableList(new ArrayList<>(builder.alphabet)); + this.minLength = builder.minLength; + this.prefix = builder.prefix; + this.packageDepth = builder.packageDepth; + this.segmentLength = builder.segmentLength; + this.fallbackToken = builder.fallbackToken; + } + + public boolean isEnabled() { + return enabled; + } + + public RenamerMode getMode() { + return mode; + } + + public List getAlphabet() { + return alphabet; + } + + public int getMinLength() { + return minLength; + } + + public String getPrefix() { + return prefix; + } + + public int getPackageDepth() { + return packageDepth; + } + + public int getSegmentLength() { + return segmentLength; + } + + public String getFallbackToken() { + return fallbackToken; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private boolean enabled; + private RenamerMode mode = RenamerMode.ALPHABETICAL; + private List alphabet = new ArrayList<>(); + private int minLength = 3; + private String prefix = ""; + private int packageDepth = 1; + private int segmentLength = 1; + private String fallbackToken = "a"; + + private Builder() { + } + + public Builder enabled(boolean enabled) { + this.enabled = enabled; + return this; + } + + public Builder mode(RenamerMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + return this; + } + + public Builder alphabet(List alphabet) { + this.alphabet = new ArrayList<>(Objects.requireNonNull(alphabet, "alphabet")); + return this; + } + + public Builder minLength(int minLength) { + this.minLength = Math.max(1, minLength); + return this; + } + + public Builder prefix(String prefix) { + this.prefix = prefix == null ? "" : prefix; + return this; + } + + public Builder packageDepth(int packageDepth) { + this.packageDepth = Math.max(1, packageDepth); + return this; + } + + public Builder segmentLength(int segmentLength) { + this.segmentLength = Math.max(1, segmentLength); + return this; + } + + public Builder fallbackToken(String fallbackToken) { + this.fallbackToken = fallbackToken == null || fallbackToken.isEmpty() ? "a" : fallbackToken; + return this; + } + + public EntityRenamerSettings build() { + if (alphabet.isEmpty()) { + throw new IllegalStateException("Alphabet must contain at least one entry"); + } + return new EntityRenamerSettings(this); + } + } +} diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerMode.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerMode.java new file mode 100644 index 0000000..4c77478 --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerMode.java @@ -0,0 +1,19 @@ +package dev.skidfuscator.obfuscator.renamer.config; + +public enum RenamerMode { + ALPHABETICAL, + NUMERIC, + CUSTOM; + + public static RenamerMode fromString(String value) { + if (value == null) { + return ALPHABETICAL; + } + for (RenamerMode mode : values()) { + if (mode.name().equalsIgnoreCase(value)) { + return mode; + } + } + return ALPHABETICAL; + } +} diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerSettings.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerSettings.java new file mode 100644 index 0000000..5415354 --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/config/RenamerSettings.java @@ -0,0 +1,172 @@ +package dev.skidfuscator.obfuscator.renamer.config; + +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import dev.skidfuscator.obfuscator.SkidfuscatorSession; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public final class RenamerSettings { + private static final String CLASS_PATH = "classRenamer"; + private static final String METHOD_PATH = "methodRenamer"; + private static final String FIELD_PATH = "fieldRenamer"; + + private final EntityRenamerSettings classSettings; + private final EntityRenamerSettings methodSettings; + private final EntityRenamerSettings fieldSettings; + + private RenamerSettings(EntityRenamerSettings classSettings, + EntityRenamerSettings methodSettings, + EntityRenamerSettings fieldSettings) { + this.classSettings = classSettings; + this.methodSettings = methodSettings; + this.fieldSettings = fieldSettings; + } + + public EntityRenamerSettings getClassSettings() { + return classSettings; + } + + public EntityRenamerSettings getMethodSettings() { + return methodSettings; + } + + public EntityRenamerSettings getFieldSettings() { + return fieldSettings; + } + + public boolean isAnyEnabled() { + return classSettings.isEnabled() || methodSettings.isEnabled() || fieldSettings.isEnabled(); + } + + public static RenamerSettings from(Config root, SkidfuscatorSession session) { + Config config = root == null ? ConfigFactory.empty() : root; + boolean sessionDefault = session != null && session.isRenamer(); + + EntityRenamerSettings classSettings = buildClassSettings(config, sessionDefault); + EntityRenamerSettings methodSettings = buildMemberSettings(config, METHOD_PATH, sessionDefault, 2); + EntityRenamerSettings fieldSettings = buildMemberSettings(config, FIELD_PATH, sessionDefault, 2); + + return new RenamerSettings(classSettings, methodSettings, fieldSettings); + } + + private static EntityRenamerSettings buildClassSettings(Config config, boolean sessionDefault) { + String path = CLASS_PATH; + Config scoped = config.hasPath(path) ? config.getConfig(path) : ConfigFactory.empty(); + + boolean enabled = scoped.hasPath("enabled") ? scoped.getBoolean("enabled") : sessionDefault; + RenamerMode mode = RenamerMode.fromString(scoped.hasPath("type") ? scoped.getString("type") : null); + List alphabet = resolveAlphabet(scoped, mode, defaultAlphabet()); + + int minLength = scoped.hasPath("depth") ? Math.max(1, scoped.getInt("depth")) : 3; + int packageDepth = scoped.hasPath("directoryDepth") ? Math.max(1, scoped.getInt("directoryDepth")) : 1; + int segmentLength = scoped.hasPath("segmentLength") ? Math.max(1, scoped.getInt("segmentLength")) : Math.max(1, minLength / packageDepth); + int requiredLength = segmentLength * Math.max(0, packageDepth - 1) + 1; + if (minLength < requiredLength) { + minLength = requiredLength; + } + + String prefix = scoped.hasPath("prefix") ? normalizePrefix(scoped.getString("prefix")) : ""; + + String fallback = alphabet.get(0); + + return EntityRenamerSettings.builder() + .enabled(enabled) + .mode(mode) + .alphabet(alphabet) + .minLength(minLength) + .prefix(prefix) + .packageDepth(packageDepth) + .segmentLength(segmentLength) + .fallbackToken(fallback) + .build(); + } + + private static EntityRenamerSettings buildMemberSettings(Config config, String path, boolean sessionDefault, int defaultDepth) { + Config scoped = config.hasPath(path) ? config.getConfig(path) : ConfigFactory.empty(); + + boolean enabled = scoped.hasPath("enabled") ? scoped.getBoolean("enabled") : sessionDefault; + RenamerMode mode = RenamerMode.fromString(scoped.hasPath("type") ? scoped.getString("type") : null); + List alphabet = resolveAlphabet(scoped, mode, defaultAlphabet()); + int minLength = scoped.hasPath("depth") ? Math.max(1, scoped.getInt("depth")) : defaultDepth; + String fallback = alphabet.get(0); + + return EntityRenamerSettings.builder() + .enabled(enabled) + .mode(mode) + .alphabet(alphabet) + .minLength(minLength) + .prefix("") + .packageDepth(1) + .segmentLength(minLength) + .fallbackToken(fallback) + .build(); + } + + private static List resolveAlphabet(Config scoped, RenamerMode mode, List fallback) { + if (mode == RenamerMode.NUMERIC) { + return digitsAlphabet(); + } + + if (mode == RenamerMode.ALPHABETICAL && !scoped.hasPath("chars")) { + return fallback; + } + + if (scoped.hasPath("chars")) { + List supplied = scoped.getStringList("chars"); + Set tokens = new LinkedHashSet<>(); + for (String entry : supplied) { + if (entry == null) { + continue; + } + for (int i = 0; i < entry.length(); i++) { + tokens.add(String.valueOf(entry.charAt(i))); + } + } + if (!tokens.isEmpty()) { + return new ArrayList<>(tokens); + } + } + + if (mode == RenamerMode.CUSTOM) { + return fallback; + } + + return fallback; + } + + private static List defaultAlphabet() { + return IntStream.rangeClosed('a', 'z') + .mapToObj(c -> String.valueOf((char) c)) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private static List digitsAlphabet() { + return IntStream.rangeClosed('0', '9') + .mapToObj(c -> String.valueOf((char) c)) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private static String normalizePrefix(String prefix) { + if (prefix == null) { + return ""; + } + String trimmed = prefix.trim(); + if (trimmed.isEmpty()) { + return ""; + } + String normalized = trimmed.replace('.', '/'); + if (!normalized.endsWith("/")) { + normalized = normalized + "/"; + } + if (normalized.startsWith("/")) { + normalized = normalized.substring(1); + } + return normalized; + } +} diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/NameGenerator.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/NameGenerator.java new file mode 100644 index 0000000..a6cdd9f --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/NameGenerator.java @@ -0,0 +1,5 @@ +package dev.skidfuscator.obfuscator.renamer.generator; + +public interface NameGenerator { + String next(); +} diff --git a/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/RadixNameGenerator.java b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/RadixNameGenerator.java new file mode 100644 index 0000000..a3a4e0c --- /dev/null +++ b/obfuscator/src/main/java/dev/skidfuscator/obfuscator/renamer/generator/RadixNameGenerator.java @@ -0,0 +1,49 @@ +package dev.skidfuscator.obfuscator.renamer.generator; + +import java.util.ArrayList; +import java.util.List; + +public final class RadixNameGenerator implements NameGenerator { + private final List alphabet; + private final List state; + + public RadixNameGenerator(List alphabet, int minLength) { + if (alphabet == null || alphabet.isEmpty()) { + throw new IllegalArgumentException("Alphabet must not be empty"); + } + this.alphabet = new ArrayList<>(alphabet); + int length = Math.max(1, minLength); + this.state = new ArrayList<>(length); + for (int i = 0; i < length; i++) { + this.state.add(0); + } + } + + @Override + public synchronized String next() { + StringBuilder builder = new StringBuilder(); + for (int index : state) { + builder.append(alphabet.get(index)); + } + String result = builder.toString(); + increment(); + return result; + } + + private void increment() { + for (int position = state.size() - 1; position >= 0; position--) { + int value = state.get(position) + 1; + if (value < alphabet.size()) { + state.set(position, value); + for (int i = position + 1; i < state.size(); i++) { + state.set(i, 0); + } + return; + } + } + state.add(0, 0); + for (int i = 1; i < state.size(); i++) { + state.set(i, 0); + } + } +} diff --git a/obfuscator/src/main/resources/defaultConfig.hocon b/obfuscator/src/main/resources/defaultConfig.hocon index 50169c4..7d620c8 100644 --- a/obfuscator/src/main/resources/defaultConfig.hocon +++ b/obfuscator/src/main/resources/defaultConfig.hocon @@ -99,6 +99,8 @@ classRenamer { "o0" ] depth: 3 + directoryDepth: 1 + segmentLength: 3 } methodRenamer { diff --git a/obfuscator/src/test/java/by/radioegor146/JvmRenamerIntegrationTest.java b/obfuscator/src/test/java/by/radioegor146/JvmRenamerIntegrationTest.java new file mode 100644 index 0000000..c3d9f33 --- /dev/null +++ b/obfuscator/src/test/java/by/radioegor146/JvmRenamerIntegrationTest.java @@ -0,0 +1,254 @@ +package by.radioegor146; + +import dev.skidfuscator.obfuscator.FlowExceptionMode; +import dev.skidfuscator.obfuscator.Skidfuscator; +import dev.skidfuscator.obfuscator.SkidfuscatorSession; +import org.junit.jupiter.api.Test; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +class JvmRenamerIntegrationTest { + private static final String JAVA_SOURCE = "package test;" + + "public class Sample {" + + " private String message = \"Hello\";" + + " public Sample() {}" + + " public String greet(String name) {" + + " java.util.function.Supplier supplier = () -> message + \" \" + name;" + + " return supplier.get();" + + " }" + + " public static void main(String[] args) throws Exception {" + + " Class clazz = Class.forName(\"test.Sample\");" + + " java.lang.reflect.Method method = clazz.getDeclaredMethod(\"greet\", String.class);" + + " method.setAccessible(true);" + + " java.lang.reflect.Field field = clazz.getDeclaredField(\"message\");" + + " field.setAccessible(true);" + + " Object instance = clazz.getDeclaredConstructor().newInstance();" + + " field.set(instance, \"Hola\");" + + " String target = args.length == 0 ? \"world\" : args[0];" + + " System.out.println(method.invoke(instance, target));" + + " }" + + "}"; + + private static final String CONFIG = String.join("\n", + "stringEncryption { enabled = false }", + "numberEncryption { enabled = false }", + "flowCondition { enabled = false }", + "flowException { enabled = false }", + "flowRange { enabled = false }", + "flowFactoryMaker { enabled = false }", + "flowSwitch { enabled = false }", + "outliner { enabled = false }", + "ahegao { enabled = false }", + "native { enabled = false }", + "reference { enabled = false }", + "driver { enabled = false }", + "classRenamer {", + " enabled = true", + " prefix = \"zz/\"", + " depth = 3", + " directoryDepth = 2", + " segmentLength = 2", + "}", + "methodRenamer {", + " enabled = true", + " depth = 3", + "}", + "fieldRenamer {", + " enabled = true", + " depth = 3", + "}"); + + @Test + void testJvmRenamerObfuscation() throws Exception { + Path tempDir = Files.createTempDirectory("jvm-renamer-test"); + try { + Path sourceDir = tempDir.resolve("src/test"); + Files.createDirectories(sourceDir); + Path sourceFile = sourceDir.resolve("Sample.java"); + Files.writeString(sourceFile, JAVA_SOURCE, StandardCharsets.UTF_8); + + Path classesDir = tempDir.resolve("classes"); + Files.createDirectories(classesDir); + + compileSource(sourceFile, classesDir); + + Path inputJar = tempDir.resolve("input.jar"); + createJar(classesDir, inputJar); + + Path configFile = tempDir.resolve("config.hocon"); + Files.writeString(configFile, CONFIG, StandardCharsets.UTF_8); + + Path outputJar = tempDir.resolve("output.jar"); + SkidfuscatorSession session = SkidfuscatorSession.builder() + .input(inputJar.toFile()) + .output(outputJar.toFile()) + .libs(new java.io.File[0]) + .config(configFile.toFile()) + .analytics(false) + .phantom(false) + .fuckit(true) + .renamer(true) + .debug(false) + .skidStringObfuscation(false) + .skidNumberObfuscation(false) + .skidFlowObfuscation(false) + .skidSdkInjection(false) + .skidVmHashing(false) + .skidInvokeDynamicObfuscation(false) + .flowExceptionMode(FlowExceptionMode.STANDARD) + .build(); + + new Skidfuscator(session).run(); + + assertTrue(Files.exists(outputJar), "Obfuscated jar was not created"); + + verifyJarContents(outputJar); + verifyRuntimeBehavior(outputJar); + } finally { + deleteRecursive(tempDir); + } + } + + private static void compileSource(Path sourceFile, Path classesDir) throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler, "Java compiler is required for the test"); + try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) { + Iterable units = + fileManager.getJavaFileObjectsFromFiles(List.of(sourceFile.toFile())); + List options = List.of("-d", classesDir.toString()); + Boolean result = compiler.getTask(null, fileManager, null, options, null, units).call(); + assertTrue(Boolean.TRUE.equals(result), "Compilation failed"); + } + } + + private static void createJar(Path classesDir, Path jarPath) throws IOException { + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "test.Sample"); + + try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarPath), manifest)) { + List files = Files.walk(classesDir) + .filter(Files::isRegularFile) + .collect(Collectors.toList()); + for (Path file : files) { + String entryName = classesDir.relativize(file).toString().replace(java.io.File.separatorChar, '/'); + JarEntry entry = new JarEntry(entryName); + jar.putNextEntry(entry); + jar.write(Files.readAllBytes(file)); + jar.closeEntry(); + } + } + } + + private static void verifyJarContents(Path outputJar) throws IOException { + try (JarFile jar = new JarFile(outputJar.toFile())) { + assertNull(jar.getEntry("test/Sample.class"), "Original class name should be absent"); + assertNull(jar.getEntry("dev/skidfuscator/runtime/ReflectionMappings.class"), "Reflection metadata should not be emitted"); + assertNull(jar.getEntry("dev/skidfuscator/runtime/ReflectionSupport.class"), "Reflection helper should not be emitted"); + + Manifest manifest = jar.getManifest(); + assertNotNull(manifest, "Manifest missing from jar"); + String mainClass = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + assertNotNull(mainClass, "Main-Class attribute missing"); + assertNotEquals("test.Sample", mainClass, "Manifest main class was not updated"); + + int methodSeparator = mainClass.lastIndexOf('.'); + assertTrue(methodSeparator > 0, "Manifest main class must include method name"); + + String obfuscatedClass = mainClass.substring(0, methodSeparator); + String obfuscatedMethod = mainClass.substring(methodSeparator + 1); + assertFalse(obfuscatedMethod.isEmpty(), "Manifest method segment should not be empty"); + + String mainEntry = obfuscatedClass.replace('.', '/') + ".class"; + assertNotNull(jar.getEntry(mainEntry), "Obfuscated main class missing from jar"); + + List classEntries = new ArrayList<>(); + var entries = jar.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + if (entry.getName().endsWith(".class") && !entry.getName().startsWith("META-INF/")) { + classEntries.add(entry.getName()); + } + } + + boolean foundObfuscated = classEntries.stream() + .anyMatch(name -> !name.startsWith("dev/skidfuscator/runtime/") && !name.equals("module-info.class")); + assertTrue(foundObfuscated, "Obfuscated class entry was not found"); + } + } + + private static void verifyRuntimeBehavior(Path outputJar) throws Exception { + try (JarFile jar = new JarFile(outputJar.toFile())) { + Manifest manifest = jar.getManifest(); + assertNotNull(manifest, "Manifest missing"); + String mainClass = manifest.getMainAttributes().getValue(Attributes.Name.MAIN_CLASS); + assertNotNull(mainClass, "Main-Class attribute missing"); + + int methodSeparator = mainClass.lastIndexOf('.'); + assertTrue(methodSeparator > 0, "Main-Class should encode method segment"); + + String className = mainClass.substring(0, methodSeparator); + String methodName = mainClass.substring(methodSeparator + 1); + assertFalse(methodName.isEmpty(), "Method segment should not be empty"); + + try (URLClassLoader loader = new URLClassLoader(new URL[]{outputJar.toUri().toURL()}, + ClassLoader.getPlatformClassLoader())) { + Class entry = Class.forName(className, true, loader); + Method main = entry.getMethod(methodName, String[].class); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + PrintStream previous = System.out; + PrintStream capture = new PrintStream(buffer, true, StandardCharsets.UTF_8.name()); + try { + System.setOut(capture); + main.invoke(null, new Object[]{new String[]{"Agent"}}); + } finally { + capture.close(); + System.setOut(previous); + } + + String output = buffer.toString(StandardCharsets.UTF_8.name()); + assertTrue(output.contains("Hola Agent"), "Reflection-based main execution failed: " + output); + } + } + } + + private static void deleteRecursive(Path root) { + if (root == null) { + return; + } + try { + Files.walk(root) + .sorted(Comparator.reverseOrder()) + .forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + } + }); + } catch (IOException ignored) { + } + } +}