Skip to content

Commit a39ab21

Browse files
authored
Merge pull request #39 from Statsly-org/fix/object-methods-renaming
fix: exclude Object methods (toString, equals, hashCode) from renaming
2 parents 32b51d9 + a0d294c commit a39ab21

1 file changed

Lines changed: 157 additions & 40 deletions

File tree

src/main/java/st3ix/obfuscator/transform/StringObfuscator.java

Lines changed: 157 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,25 @@
33
import org.objectweb.asm.ClassReader;
44
import org.objectweb.asm.ClassVisitor;
55
import org.objectweb.asm.ClassWriter;
6+
import org.objectweb.asm.Label;
67
import org.objectweb.asm.MethodVisitor;
78
import org.objectweb.asm.Opcodes;
89
import org.objectweb.asm.Type;
910

1011
import java.nio.charset.StandardCharsets;
12+
import java.util.ArrayList;
13+
import java.util.List;
1114
import java.util.Random;
1215

1316
/**
14-
* Obfuscates string literals in bytecode using XOR encryption.
15-
* Replaces LDC "string" with inline decryption bytecode at each call site.
16-
* No central decoder class – no single point to hook and dump all strings.
17-
* Key per class (owner.hashCode) and per string (index in method) for stronger obfuscation.
17+
* Obfuscates string literals using XOR encryption.
18+
* Replaces LDC "string" with inline decryption at each use site (no central decoder).
19+
* Also handles static final String fields: removes ConstantValue and initializes via
20+
* obfuscated code in <clinit>, so API keys and secrets are never readable.
21+
* Key per class and per string/field for stronger obfuscation.
1822
*/
1923
public final class StringObfuscator {
2024

21-
/** Local variable indices for inline decrypt (high to avoid clashes with method params). */
2225
private static final int LOCAL_ENC = 100;
2326
private static final int LOCAL_KEY = 101;
2427
private static final int LOCAL_OUT = 102;
@@ -29,16 +32,10 @@ public final class StringObfuscator {
2932

3033
private final int key;
3134

32-
/**
33-
* Creates an obfuscator with default (fixed) key.
34-
*/
3535
public StringObfuscator() {
3636
this.key = DEFAULT_KEY;
3737
}
3838

39-
/**
40-
* Creates an obfuscator with random key (different per instance).
41-
*/
4239
public static StringObfuscator withRandomKey() {
4340
return new StringObfuscator(new Random().nextInt());
4441
}
@@ -47,11 +44,11 @@ private StringObfuscator(int key) {
4744
this.key = key;
4845
}
4946

50-
/**
51-
* Encrypts a string to a byte array using XOR.
52-
*/
53-
public byte[] encrypt(String s) {
54-
return encrypt(s, key);
47+
public byte[] transform(byte[] classBytes) {
48+
ClassReader reader = new ClassReader(classBytes);
49+
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
50+
reader.accept(new StringObfuscatorClassVisitor(writer, key), ClassReader.EXPAND_FRAMES);
51+
return writer.toByteArray();
5552
}
5653

5754
private static byte[] encrypt(String s, int key) {
@@ -63,20 +60,12 @@ private static byte[] encrypt(String s, int key) {
6360
return out;
6461
}
6562

66-
/**
67-
* Transforms the class bytes, obfuscating string literals.
68-
*/
69-
public byte[] transform(byte[] classBytes) {
70-
ClassReader reader = new ClassReader(classBytes);
71-
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES);
72-
reader.accept(new StringObfuscatorClassVisitor(writer, key), ClassReader.EXPAND_FRAMES);
73-
return writer.toByteArray();
74-
}
75-
7663
private static final class StringObfuscatorClassVisitor extends ClassVisitor {
7764

7865
private final int key;
7966
private String owner;
67+
private final List<StaticStringField> staticStringFields = new ArrayList<>();
68+
private boolean hasClinit;
8069

8170
StringObfuscatorClassVisitor(ClassVisitor cv, int key) {
8271
super(Opcodes.ASM9, cv);
@@ -89,12 +78,80 @@ public void visit(int version, int access, String name, String signature, String
8978
super.visit(version, access, name, signature, superName, interfaces);
9079
}
9180

81+
@Override
82+
public org.objectweb.asm.FieldVisitor visitField(int access, String name, String descriptor,
83+
String signature, Object value) {
84+
if ("Ljava/lang/String;".equals(descriptor)
85+
&& (access & Opcodes.ACC_STATIC) != 0
86+
&& (access & Opcodes.ACC_FINAL) != 0
87+
&& value instanceof String s) {
88+
staticStringFields.add(new StaticStringField(name, s));
89+
return super.visitField(access, name, descriptor, signature, null);
90+
}
91+
return super.visitField(access, name, descriptor, signature, value);
92+
}
93+
9294
@Override
9395
public MethodVisitor visitMethod(int access, String name, String descriptor,
9496
String signature, String[] exceptions) {
9597
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
98+
if ("<clinit>".equals(name) && !staticStringFields.isEmpty()) {
99+
hasClinit = true;
100+
return new ClinitPrepender(mv, key, owner, staticStringFields);
101+
}
96102
return new StringObfuscatorMethodVisitor(mv, key, owner);
97103
}
104+
105+
@Override
106+
public void visitEnd() {
107+
if (!staticStringFields.isEmpty() && !hasClinit) {
108+
MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
109+
if (mv != null) {
110+
mv.visitCode();
111+
emitStaticFieldInits(mv, key, owner, staticStringFields);
112+
mv.visitInsn(Opcodes.RETURN);
113+
mv.visitMaxs(0, 0);
114+
mv.visitEnd();
115+
}
116+
}
117+
super.visitEnd();
118+
}
119+
120+
private record StaticStringField(String name, String value) {}
121+
}
122+
123+
private static final class ClinitPrepender extends MethodVisitor {
124+
125+
private final int key;
126+
private final String owner;
127+
private final List<StringObfuscatorClassVisitor.StaticStringField> fields;
128+
129+
ClinitPrepender(MethodVisitor mv, int key, String owner,
130+
List<StringObfuscatorClassVisitor.StaticStringField> fields) {
131+
super(Opcodes.ASM9, mv);
132+
this.key = key;
133+
this.owner = owner;
134+
this.fields = fields;
135+
}
136+
137+
@Override
138+
public void visitCode() {
139+
emitStaticFieldInits(this, key, owner, fields);
140+
super.visitCode();
141+
}
142+
}
143+
144+
private static void emitStaticFieldInits(MethodVisitor mv, int key, String owner,
145+
List<StringObfuscatorClassVisitor.StaticStringField> fields) {
146+
for (int i = 0; i < fields.size(); i++) {
147+
var f = fields.get(i);
148+
int actualKey = key ^ owner.hashCode() ^ i;
149+
byte[] enc = encrypt(f.value(), actualKey);
150+
emitRuntimeKey(mv, owner, key ^ i);
151+
emitByteArray(mv, enc);
152+
emitInlineDecrypt(mv);
153+
mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, f.name(), "Ljava/lang/String;");
154+
}
98155
}
99156

100157
private static final class StringObfuscatorMethodVisitor extends MethodVisitor {
@@ -112,25 +169,21 @@ private static final class StringObfuscatorMethodVisitor extends MethodVisitor {
112169
@Override
113170
public void visitLdcInsn(Object value) {
114171
if (value instanceof String s) {
115-
emitEncryptedString(s);
172+
int idx = stringIndex++;
173+
int actualKey = key ^ owner.hashCode() ^ idx;
174+
byte[] encrypted = encrypt(s, actualKey);
175+
emitRuntimeKey(idx);
176+
emitByteArray(encrypted);
177+
emitInlineDecrypt();
116178
} else {
117179
super.visitLdcInsn(value);
118180
}
119181
}
120182

121-
private void emitEncryptedString(String s) {
122-
int idx = stringIndex++;
123-
int actualKey = key ^ owner.hashCode() ^ idx;
124-
byte[] encrypted = encrypt(s, actualKey);
125-
emitByteArray(encrypted);
126-
emitRuntimeKey(idx);
127-
emitInlineDecrypt();
128-
}
129-
130-
/** Inline XOR decrypt: stack [byte[] encrypted, int key] -> stack [String]. No central decoder. */
131183
private void emitInlineDecrypt() {
132-
org.objectweb.asm.Label loopStart = new org.objectweb.asm.Label();
133-
org.objectweb.asm.Label loopEnd = new org.objectweb.asm.Label();
184+
Label loopStart = new Label();
185+
Label loopEnd = new Label();
186+
// Stack: [key, byte[]] - top is byte[], store it first (ASTORE), then key (ISTORE)
134187
super.visitVarInsn(Opcodes.ASTORE, LOCAL_ENC);
135188
super.visitVarInsn(Opcodes.ISTORE, LOCAL_KEY);
136189
super.visitVarInsn(Opcodes.ALOAD, LOCAL_ENC);
@@ -169,7 +222,6 @@ private void emitInlineDecrypt() {
169222
super.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/String", "<init>", "([BLjava/nio/charset/Charset;)V", false);
170223
}
171224

172-
/** Emits: (key ^ stringIndex) ^ thisClass.getName().hashCode() = actualKey per class and per string */
173225
private void emitRuntimeKey(int stringIndex) {
174226
super.visitLdcInsn(Type.getObjectType(owner));
175227
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
@@ -194,4 +246,69 @@ private void emitByteArray(byte[] bytes) {
194246
}
195247
}
196248
}
249+
250+
private static void emitByteArray(MethodVisitor mv, byte[] bytes) {
251+
mv.visitLdcInsn(bytes.length);
252+
mv.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BYTE);
253+
for (int i = 0; i < bytes.length; i++) {
254+
mv.visitInsn(Opcodes.DUP);
255+
mv.visitLdcInsn(i);
256+
int b = bytes[i] & 0xFF;
257+
if (b <= 127) {
258+
mv.visitIntInsn(Opcodes.BIPUSH, b);
259+
} else {
260+
mv.visitIntInsn(Opcodes.SIPUSH, b);
261+
}
262+
mv.visitInsn(Opcodes.BASTORE);
263+
}
264+
}
265+
266+
private static void emitRuntimeKey(MethodVisitor mv, String owner, int actualKey) {
267+
mv.visitLdcInsn(Type.getObjectType(owner));
268+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
269+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/String", "hashCode", "()I", false);
270+
mv.visitLdcInsn(actualKey);
271+
mv.visitInsn(Opcodes.IXOR);
272+
}
273+
274+
private static void emitInlineDecrypt(MethodVisitor mv) {
275+
Label loopStart = new Label();
276+
Label loopEnd = new Label();
277+
mv.visitVarInsn(Opcodes.ASTORE, LOCAL_ENC);
278+
mv.visitVarInsn(Opcodes.ISTORE, LOCAL_KEY);
279+
mv.visitVarInsn(Opcodes.ALOAD, LOCAL_ENC);
280+
mv.visitInsn(Opcodes.ARRAYLENGTH);
281+
mv.visitIntInsn(Opcodes.NEWARRAY, Opcodes.T_BYTE);
282+
mv.visitVarInsn(Opcodes.ASTORE, LOCAL_OUT);
283+
mv.visitInsn(Opcodes.ICONST_0);
284+
mv.visitVarInsn(Opcodes.ISTORE, LOCAL_I);
285+
mv.visitLabel(loopStart);
286+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_I);
287+
mv.visitVarInsn(Opcodes.ALOAD, LOCAL_ENC);
288+
mv.visitInsn(Opcodes.ARRAYLENGTH);
289+
mv.visitJumpInsn(Opcodes.IF_ICMPGE, loopEnd);
290+
mv.visitVarInsn(Opcodes.ALOAD, LOCAL_ENC);
291+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_I);
292+
mv.visitInsn(Opcodes.BALOAD);
293+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_KEY);
294+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_I);
295+
mv.visitInsn(Opcodes.IADD);
296+
mv.visitIntInsn(Opcodes.SIPUSH, 255);
297+
mv.visitInsn(Opcodes.IAND);
298+
mv.visitInsn(Opcodes.IXOR);
299+
mv.visitInsn(Opcodes.I2B);
300+
mv.visitVarInsn(Opcodes.ISTORE, LOCAL_BYTE);
301+
mv.visitVarInsn(Opcodes.ALOAD, LOCAL_OUT);
302+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_I);
303+
mv.visitVarInsn(Opcodes.ILOAD, LOCAL_BYTE);
304+
mv.visitInsn(Opcodes.BASTORE);
305+
mv.visitIincInsn(LOCAL_I, 1);
306+
mv.visitJumpInsn(Opcodes.GOTO, loopStart);
307+
mv.visitLabel(loopEnd);
308+
mv.visitTypeInsn(Opcodes.NEW, "java/lang/String");
309+
mv.visitInsn(Opcodes.DUP);
310+
mv.visitVarInsn(Opcodes.ALOAD, LOCAL_OUT);
311+
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/nio/charset/StandardCharsets", "UTF_8", "Ljava/nio/charset/Charset;");
312+
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/String", "<init>", "([BLjava/nio/charset/Charset;)V", false);
313+
}
197314
}

0 commit comments

Comments
 (0)