Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Features.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- **Number obfuscation** – Hides `int` constants via math expressions (`123` → `(50*3)-27`, `(a<<n)+b`, etc.); decompilers show expressions, not literals. `long`/`float`/`double` still use XOR. Less prone to constant folding than simple XOR.
- **Array dimension obfuscation** – Hides array sizes (`new int[8]` → `new int[(8 ^ key) ^ key]`)
- **Boolean obfuscation** – Hides `true`/`false` literals using XOR (`ICONST_0`/`ICONST_1` → `(value ^ key) ^ key`)
- **String obfuscation** – Encrypts string literals using XOR; **decryption inlined at each use site** (no central decoder class). **Key per class and per string**: each string uses `key ^ class.hashCode() ^ index`, so no global key and different strings get different keys. Strong against `strings`-tools and casual inspection. **Note:** Determined reversers can still trace decryption logic – obfuscation raises the bar, not unbreakable.
- **String obfuscation** – XOR encryption; inline decrypt at each use site (no central decoder). Key per class and per string/field. **Static final String fields**: ConstantValue removed; initialized in &lt;clinit&gt; with obfuscated code, so `private static final String KEY = "secret"` never appears as readable text. Strong against `strings`-tools and casual inspection.
- **Optional random keys** – `numberKeyRandom`, `arrayKeyRandom`, `booleanKeyRandom`, `stringKeyRandom` for different keys/patterns per build
- **Debug info stripping** – Removes source file names, line number table, and local variable table. Decompilers show `var0`, `var1`, etc., and lose source mappings. Configurable via `debugInfoStrippingEnabled`.

Expand Down
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Bei jedem Release gibt es ein ZIP-Archiv mit allem Nötigen: JAR, Batch-Datei zu
- **Number obfuscation** – Hides `int` via math expressions (`123` → `(50*3)-27`); `long`/`float`/`double` via XOR
- **Array obfuscation** – Hides array dimensions
- **Boolean obfuscation** – Hides `true`/`false` literals
- **String obfuscation** – Encrypts string literals (XOR); inline decrypt at each use; key per class and per string (no central decoder, no dump point)
- **String obfuscation** – XOR encryption; inline decrypt at each use; key per class and per string; static final String fields initialized in &lt;clinit&gt; (no readable API keys/secrets)
- **Debug info stripping** – Removes source names, line numbers, local variable names
- **Local variable renaming** *(planned)* – Obfuscate local variable names when debug kept
- **Random options** – Optional random keys and class/method/field names per build
Expand Down Expand Up @@ -145,21 +145,19 @@ public final class LicenseValidator {
**After obfuscation** (class, method, number, string, debug stripping, homoglyph):

```java

// String constant obfuscation: built from char codes, no readable text
public final class b {
public static void main(String[] args) {
byte[] enc = new byte[]{...};
byte[] out = new byte[enc.length];
for (int i = 0; i < enc.length; i++)
out[i] = (byte)(enc[i] ^ ((key + i) & 0xFF));
System.out.println(new String(out, StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
sb.append((char)(50+59)); sb.append((char)(120+1)); sb.append((char)45); // ...
System.out.println(sb.toString()); // "License..." – unreadable in source
ь var0 = new ь();
var0.a();
}
}

public final class ь {
private static final String a = /* inline decrypt */;
private static final String a = /* built from (char)(expr) per character */;
private int b;

public void a() {
Expand All @@ -181,14 +179,12 @@ public final class ь {
| Invisible chars | Zero-width chars in names – appear normal but differ |
| Number obfuscation | `443` → `431+12`, `3` → `2*2-1` (math expressions) |
| Boolean obfuscation | `true` → `(value ^ key) ^ key` |
| String obfuscation | Inline XOR decrypt at each use; key per class and per string; no central decoder → no dump point |
| String obfuscation | Constant obfuscation: strings built from char codes via math expressions; no readable text in decompiler |
| Debug stripping | Local vars become `var0`, `var1`; line numbers removed |

### Strength

Obfuscation raises the bar for casual and automated reverse engineering. Numbers are hidden as math expressions at compile time; decompilers show the expression, not the literal. Strings use **runtime-computed keys** (`key ^ class.hashCode() ^ index` per string), decrypted inline at each use site—no central decoder, no single breakpoint to dump all strings.

We chose XOR over AES for string encryption: AES would be overkill for typical JAR obfuscation and adds complexity (IV handling, block size). XOR with per-class/per-string keys is lightweight and effective against `strings`-tools and casual inspection. **Issues and PRs are open**—if you want AES or stronger encryption, contributions are welcome.
Obfuscation raises the bar for casual and automated reverse engineering. Numbers are hidden as math expressions at compile time; decompilers show the expression, not the literal. Strings use **constant obfuscation**: each character is built from math expressions (e.g. `(char)(50+59)` for 'm'), so API keys, secrets, and literals never appear as readable text. No string in the constant pool; nothing for `strings`-tools to extract. **Issues and PRs are open**—if you want AES/XOR encryption as an optional mode, contributions are welcome.

Realistic caveats: determined reversers can still trace decryption logic; obfuscation is a deterrent, not unbreakable protection.

Expand Down
179 changes: 179 additions & 0 deletions src/main/java/st3ix/obfuscator/transform/IntExpressionEmitter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package st3ix.obfuscator.transform;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.util.Random;

/**
* Emits bytecode for an int value as a math expression (e.g. (50+59) for 109)
* so decompilers show expressions instead of literals.
* Used by NumberObfuscator and StringObfuscator (char-code construction).
*/
public final class IntExpressionEmitter {

private final MethodVisitor mv;
private final Random rng;

public IntExpressionEmitter(MethodVisitor mv, Random rng) {
this.mv = mv;
this.rng = rng;
}

/**
* Emits bytecode that leaves the given int value on the stack.
* Uses math expressions where possible, falls back to XOR for edge cases.
*/
public void emit(int value) {
if (tryEmitExpression(value)) {
return;
}
emitXor(value);
}

private boolean tryEmitExpression(int value) {
int strategy = rng != null ? rng.nextInt(5) : (Math.abs(value) % 5);
return switch (strategy) {
case 0 -> emitAdd(value);
case 1 -> emitMulAdd(value);
case 2 -> emitSub(value);
case 3 -> emitShiftAdd(value);
case 4 -> emitMulSub(value);
default -> emitAdd(value);
};
}

private static final int XOR_KEY = 0x5A5A5A5A;

private void emitXor(int value) {
int obfuscated = value ^ XOR_KEY;
mv.visitLdcInsn(obfuscated);
mv.visitLdcInsn(XOR_KEY);
mv.visitInsn(Opcodes.IXOR);
}

private void emitLdcInt(int v) {
if (v >= -1 && v <= 5) {
int opcode = switch (v) {
case -1 -> Opcodes.ICONST_M1;
case 0 -> Opcodes.ICONST_0;
case 1 -> Opcodes.ICONST_1;
case 2 -> Opcodes.ICONST_2;
case 3 -> Opcodes.ICONST_3;
case 4 -> Opcodes.ICONST_4;
case 5 -> Opcodes.ICONST_5;
default -> -1;
};
if (opcode != -1) {
mv.visitInsn(opcode);
return;
}
}
if (v >= Byte.MIN_VALUE && v <= Byte.MAX_VALUE) {
mv.visitIntInsn(Opcodes.BIPUSH, v);
} else if (v >= Short.MIN_VALUE && v <= Short.MAX_VALUE) {
mv.visitIntInsn(Opcodes.SIPUSH, v);
} else {
mv.visitLdcInsn(v);
}
}

private boolean emitAdd(int value) {
int a;
if (rng != null) {
a = rng.nextInt(2001) - 1000;
if (value >= 0 && a > value) a = value;
if (value < 0 && a < value) a = value;
} else {
a = (Math.abs(value) % 500) + 1;
if (value < 0) a = -a;
if ((long) a + (long) (value - a) != value) a = value / 2;
}
long b = (long) value - (long) a;
if (b < Integer.MIN_VALUE || b > Integer.MAX_VALUE) return false;
emitLdcInt(a);
emitLdcInt((int) b);
mv.visitInsn(Opcodes.IADD);
return true;
}

private boolean emitMulAdd(int value) {
if (value == 0) {
emitLdcInt(1);
emitLdcInt(0);
mv.visitInsn(Opcodes.IMUL);
return true;
}
int a = (rng != null ? rng.nextInt(49) + 2 : 17) & 0xFF;
if (a <= 0) a = 2;
int b = value / a;
int c = value - a * b;
long prod = (long) a * (long) b;
if (prod != (int) prod) return false;
if ((long) a * b + c != value) return false;
emitLdcInt(a);
emitLdcInt(b);
mv.visitInsn(Opcodes.IMUL);
if (c != 0) {
emitLdcInt(c);
mv.visitInsn(Opcodes.IADD);
}
return true;
}

private boolean emitSub(int value) {
int k = rng != null ? rng.nextInt(500) + 1 : Math.abs(value % 100) + 1;
long a = (long) value + (long) k;
if (a < Integer.MIN_VALUE || a > Integer.MAX_VALUE) return false;
emitLdcInt((int) a);
emitLdcInt(k);
mv.visitInsn(Opcodes.ISUB);
return true;
}

private boolean emitShiftAdd(int value) {
if (value < 0) return false;
int n = rng != null ? rng.nextInt(4) + 1 : (value % 4) + 1;
int shift = 1 << n;
int a = value / shift;
int b = value % shift;
if (value == Integer.MAX_VALUE && n == 1) return false;
emitLdcInt(a);
emitLdcInt(n);
mv.visitInsn(Opcodes.ISHL);
if (b != 0) {
emitLdcInt(b);
mv.visitInsn(Opcodes.IADD);
}
return true;
}

private boolean emitMulSub(int value) {
if (value == Integer.MIN_VALUE) {
emitLdcInt(1);
emitLdcInt(31);
mv.visitInsn(Opcodes.ISHL);
return true;
}
if (value == Integer.MAX_VALUE) {
emitLdcInt(1);
emitLdcInt(31);
mv.visitInsn(Opcodes.ISHL);
emitLdcInt(1);
mv.visitInsn(Opcodes.ISUB);
return true;
}
int a = (rng != null ? rng.nextInt(30) + 2 : 7) & 0xFF;
int b = (rng != null ? rng.nextInt(30) + 2 : 11) & 0xFF;
long prod = (long) a * (long) b;
if (prod < Integer.MIN_VALUE || prod > Integer.MAX_VALUE) return false;
int c = (int) prod - value;
if (c < 0) return false;
emitLdcInt(a);
emitLdcInt(b);
mv.visitInsn(Opcodes.IMUL);
emitLdcInt(c);
mv.visitInsn(Opcodes.ISUB);
return true;
}
}
4 changes: 4 additions & 0 deletions src/main/java/st3ix/obfuscator/transform/MethodMapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ private boolean canRename(String name, String desc, int access) {
if ((access & Opcodes.ACC_NATIVE) != 0) return false;
if ("valueOf".equals(name) && desc.startsWith("(Ljava/lang/String;)")) return false;
if ("values".equals(name) && desc.startsWith("()[L")) return false; // enum values()
// Object methods invoked by JDK (String.valueOf, PrintStream, HashMap, etc.) – keep original names
if ("toString".equals(name) && "()Ljava/lang/String;".equals(desc)) return false;
if ("equals".equals(name) && "(Ljava/lang/Object;)Z".equals(desc)) return false;
if ("hashCode".equals(name) && "()I".equals(desc)) return false;
return true;
}

Expand Down
Loading