Skip to content

Commit b7c4579

Browse files
authored
Superblaubeere (#118)
* SB27 Indy's and isAccess multiple arg helper * Make UniversalStringPool work for sb27 pools without changing logic. * Added AES decryption * Added INDY decryption * Added Composed decryption
1 parent 21f6613 commit b7c4579

File tree

5 files changed

+223
-81
lines changed

5 files changed

+223
-81
lines changed

deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/AsmHelper.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ public static boolean isAccess(int access, int opcode) {
9898
return (access & opcode) != 0;
9999
}
100100

101+
public static boolean isAccess(int opcode, int... access) {
102+
return Arrays.stream(access).allMatch(access1 -> (access1 & opcode) != 0);
103+
}
104+
101105
/**
102106
* Convert constant value to instruction that represents this constant
103107
*

deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedSuperblaubeereTransformer.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import uwu.narumi.deobfuscator.core.other.composed.general.ComposedPeepholeCleanTransformer;
55
import uwu.narumi.deobfuscator.core.other.impl.clean.LocalVariableNamesCleanTransformer;
66
import uwu.narumi.deobfuscator.core.other.impl.pool.InlineLocalVariablesTransformer;
7+
import uwu.narumi.deobfuscator.core.other.impl.sb27.SuperblaubeereInvokeDynamicTransformer;
8+
import uwu.narumi.deobfuscator.core.other.impl.sb27.SuperblaubeereStringTransformer;
79
import uwu.narumi.deobfuscator.core.other.impl.universal.InlinePureFunctionsTransformer;
10+
import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalFlowTransformer;
11+
import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalNumberTransformer;
812
import uwu.narumi.deobfuscator.core.other.impl.universal.pool.UniversalNumberPoolTransformer;
913
import uwu.narumi.deobfuscator.core.other.impl.universal.pool.UniversalStringPoolTransformer;
10-
import uwu.narumi.deobfuscator.core.other.impl.sb27.SuperblaubeereStringTransformer;
11-
import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalFlowTransformer;
1214

1315
/**
1416
* https://github.com/superblaubeere27/obfuscator
@@ -19,14 +21,16 @@ public ComposedSuperblaubeereTransformer() {
1921
// Remove var names as they are obfuscated and names are useless
2022
LocalVariableNamesCleanTransformer::new,
2123

24+
UniversalNumberTransformer::new,
25+
UniversalNumberPoolTransformer::new,
26+
2227
// Fix flow
2328
UniversalFlowTransformer::new,
2429

25-
// Inline number pools
26-
UniversalNumberPoolTransformer::new,
2730
// Decrypt strings
2831
SuperblaubeereStringTransformer::new,
2932
UniversalStringPoolTransformer::new,
33+
SuperblaubeereInvokeDynamicTransformer::new,
3034

3135
InlinePureFunctionsTransformer::new,
3236
InlineLocalVariablesTransformer::new,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package uwu.narumi.deobfuscator.core.other.impl.sb27;
2+
3+
import org.objectweb.asm.Type;
4+
import org.objectweb.asm.tree.*;
5+
import uwu.narumi.deobfuscator.api.asm.MethodContext;
6+
import uwu.narumi.deobfuscator.api.asm.matcher.Match;
7+
import uwu.narumi.deobfuscator.api.asm.matcher.MatchContext;
8+
import uwu.narumi.deobfuscator.api.asm.matcher.group.SequenceMatch;
9+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.*;
10+
import uwu.narumi.deobfuscator.api.transformer.Transformer;
11+
12+
import java.util.*;
13+
import java.util.regex.Pattern;
14+
15+
public class SuperblaubeereInvokeDynamicTransformer extends Transformer {
16+
17+
Match indyMatch = SequenceMatch.of(
18+
FieldMatch.getStatic().capture("type"),
19+
NumberMatch.numInteger().capture("position"),
20+
Match.of(ctx -> ctx.insn().isType()).or(StringMatch.of()).capture("ldc"),
21+
OpcodeMatch.of(AASTORE)
22+
);
23+
24+
private final Pattern INTEGER_PATTERN = Pattern.compile("^-?\\d+$");
25+
26+
@Override
27+
protected void transform() throws Exception {
28+
context().classes().forEach(classWrapper -> {
29+
Map<Integer, String> callInfos = new HashMap<>();
30+
Map<Integer, Type> types = new HashMap<>();
31+
32+
Set<MethodNode> methodsToRemove = new HashSet<>();
33+
Set<FieldInsnNode> fieldsToRemove = new HashSet<>();
34+
35+
classWrapper.methods().forEach(methodNode -> {
36+
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
37+
38+
if (isAccess(methodNode.access, ACC_PRIVATE, ACC_STATIC) && methodNode.desc.equals("()V")) {
39+
indyMatch.findAllMatches(methodContext).forEach(matchContext -> {
40+
FieldInsnNode fieldInsnNode = matchContext.captures().get("type").insn().asFieldInsn();
41+
int position = matchContext.captures().get("position").insn().asInteger();
42+
AbstractInsnNode ldc = matchContext.captures().get("ldc").insn();
43+
if (ldc.isType()) {
44+
types.put(position, ldc.asType());
45+
} else {
46+
callInfos.put(position, ldc.asString());
47+
}
48+
fieldsToRemove.add(fieldInsnNode);
49+
methodsToRemove.add(methodNode);
50+
markChange();
51+
});
52+
}
53+
});
54+
55+
classWrapper.methods().forEach(methodNode -> {
56+
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
57+
Match indyCallMatch = Match.of(ctx -> ctx.insn() instanceof InvokeDynamicInsnNode indy && INTEGER_PATTERN.matcher(indy.name).matches() && callInfos.containsKey(Integer.parseInt(indy.name)));
58+
indyCallMatch.findAllMatches(methodContext).forEach(matchContext -> {
59+
InvokeDynamicInsnNode node = matchContext.insn().asInvokeDynamicInsn();
60+
String[] parts = callInfos.get(Integer.parseInt(node.name)).split(":");
61+
String owner = parts[0].replace('.', '/');
62+
String name = parts[1];
63+
String desc = parts[2];
64+
int type = parts[3].length();
65+
66+
Type fieldType = null;
67+
if (type > 2) {
68+
fieldType = types.get(Integer.parseInt(desc));
69+
if (fieldType == null) return;
70+
}
71+
switch (type) {
72+
case 2:
73+
methodNode.instructions.set(node, new MethodInsnNode(INVOKEVIRTUAL, owner, name, desc, false));
74+
break;
75+
case 3:
76+
methodNode.instructions.set(node, new FieldInsnNode(GETFIELD, owner, name, fieldType.getDescriptor()));
77+
break;
78+
case 4:
79+
methodNode.instructions.set(node, new FieldInsnNode(GETSTATIC, owner, name, fieldType.getDescriptor()));
80+
break;
81+
case 5:
82+
methodNode.instructions.set(node, new FieldInsnNode(PUTFIELD, owner, name, fieldType.getDescriptor()));
83+
break;
84+
default:
85+
if (type <= 2) {
86+
methodNode.instructions.set(node, new MethodInsnNode(INVOKESTATIC, owner, name, desc, false));
87+
} else {
88+
methodNode.instructions.set(node, new FieldInsnNode(PUTSTATIC, owner, name, fieldType.getDescriptor()));
89+
}
90+
break;
91+
}
92+
});
93+
});
94+
findClInit(classWrapper.classNode()).ifPresent(clinit -> {
95+
MethodContext methodContext = MethodContext.of(classWrapper, clinit);
96+
Match.of(ctx -> {
97+
if (ctx.insn() instanceof MethodInsnNode node) {
98+
if (node.getOpcode() == INVOKESTATIC &&
99+
node.owner.equals(classWrapper.name()) &&
100+
methodsToRemove.stream().anyMatch(method -> method.name.equals(node.name) && method.desc.equals(node.desc))) {
101+
return true;
102+
}
103+
}
104+
return false;
105+
}).findAllMatches(methodContext).forEach(MatchContext::removeAll);
106+
});
107+
108+
fieldsToRemove.forEach(fieldInsnNode -> classWrapper.fields().removeIf(fieldNode -> fieldNode.name.equals(fieldInsnNode.name)
109+
&& fieldNode.desc.equals(fieldInsnNode.desc)));
110+
classWrapper.methods().removeAll(methodsToRemove);
111+
classWrapper.methods().removeIf(methodNode -> methodNode.desc.equals("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;"));
112+
});
113+
}
114+
}

deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/sb27/SuperblaubeereStringTransformer.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ public class SuperblaubeereStringTransformer extends Transformer {
7171
MethodMatch.invokeSpecial().owner("javax/crypto/spec/SecretKeySpec").name("<init>").desc("([BLjava/lang/String;)V")
7272
);
7373

74+
private static final Match STRING_DECRYPT_AES_MATCH = SequenceMatch.of(
75+
MethodMatch.invokeVirtual().owner("java/lang/String").name("getBytes").desc("(Ljava/nio/charset/Charset;)[B"),
76+
MethodMatch.invokeVirtual().owner("java/security/MessageDigest").name("digest").desc("([B)[B"),
77+
StringMatch.of("AES"),
78+
MethodMatch.invokeSpecial().owner("javax/crypto/spec/SecretKeySpec").name("<init>").desc("([BLjava/lang/String;)V")
79+
);
80+
7481
/*
7582
new java/lang/String
7683
dup
@@ -108,6 +115,8 @@ protected void transform() throws Exception {
108115
decryptMethods.put(MethodRef.of(classWrapper.classNode(), methodNode), SuperblaubeereStringTransformer::decryptStringBlowfish);
109116
} else if (STRING_DECRYPT_DES_MATCH.findFirstMatch(methodContext) != null) {
110117
decryptMethods.put(MethodRef.of(classWrapper.classNode(), methodNode), SuperblaubeereStringTransformer::decryptStringDES);
118+
} else if (STRING_DECRYPT_AES_MATCH.findFirstMatch(methodContext) != null) {
119+
decryptMethods.put(MethodRef.of(classWrapper.classNode(), methodNode), SuperblaubeereStringTransformer::decryptStringAES);
111120
} else if (STRING_DECRYPT_XOR_MATCH.findFirstMatch(methodContext) != null) {
112121
decryptMethods.put(MethodRef.of(classWrapper.classNode(), methodNode), SuperblaubeereStringTransformer::decryptXOR);
113122
}
@@ -173,6 +182,17 @@ private static String decryptStringDES(String encryptedString, String key) {
173182
}
174183
}
175184

185+
private static String decryptStringAES(String encryptedString, String key) {
186+
try {
187+
SecretKeySpec secretKeySpec = new SecretKeySpec(MessageDigest.getInstance("SHA-256").digest(key.getBytes(StandardCharsets.UTF_8)), "AES");
188+
Cipher cipher = Cipher.getInstance("AES");
189+
cipher.init(2, secretKeySpec);
190+
return new String(cipher.doFinal(Base64.getDecoder().decode(encryptedString.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
191+
} catch (Exception ex) {
192+
throw new RuntimeException(ex);
193+
}
194+
}
195+
176196
private static String decryptXOR(String encryptedString, String key) {
177197
encryptedString = new String(Base64.getDecoder().decode(encryptedString.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
178198
StringBuilder builder = new StringBuilder();
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
11
package uwu.narumi.deobfuscator.core.other.impl.universal.pool;
22

3-
import org.objectweb.asm.tree.AbstractInsnNode;
4-
import org.objectweb.asm.tree.FieldInsnNode;
5-
import org.objectweb.asm.tree.LdcInsnNode;
6-
import org.objectweb.asm.tree.MethodInsnNode;
7-
import org.objectweb.asm.tree.TypeInsnNode;
3+
import org.objectweb.asm.tree.*;
84
import uwu.narumi.deobfuscator.api.asm.MethodContext;
95
import uwu.narumi.deobfuscator.api.asm.matcher.Match;
10-
import uwu.narumi.deobfuscator.api.asm.matcher.MatchContext;
11-
import uwu.narumi.deobfuscator.api.asm.matcher.impl.FieldMatch;
12-
import uwu.narumi.deobfuscator.api.asm.matcher.impl.FrameMatch;
13-
import uwu.narumi.deobfuscator.api.asm.matcher.impl.NumberMatch;
14-
import uwu.narumi.deobfuscator.api.asm.matcher.impl.OpcodeMatch;
15-
import uwu.narumi.deobfuscator.api.asm.matcher.impl.StringMatch;
6+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.*;
167
import uwu.narumi.deobfuscator.api.transformer.Transformer;
178

9+
import java.util.HashSet;
1810
import java.util.Objects;
11+
import java.util.Set;
12+
import java.util.concurrent.atomic.AtomicBoolean;
1913

2014
/**
2115
* Replaces string pool references with actual values.
@@ -33,73 +27,79 @@ public class UniversalStringPoolTransformer extends Transformer {
3327
@Override
3428
protected void transform() throws Exception {
3529
scopedClasses().forEach(classWrapper -> {
36-
MatchContext stringPoolMatchCtx = classWrapper.methods().stream()
30+
Set<MethodNode> toRemoveMn = new HashSet<>();
31+
Set<FieldNode> toRemoveFn = new HashSet<>();
32+
classWrapper.methods().stream()
3733
.map(methodNode -> STRING_POOL_METHOD_MATCH.findFirstMatch(MethodContext.of(classWrapper, methodNode)))
38-
.filter(Objects::nonNull)
39-
.findFirst()
40-
.orElse(null);
41-
42-
// No number pool method found
43-
if (stringPoolMatchCtx == null) return;
44-
45-
//System.out.println("Found string pool method in " + classWrapper.name() + "." + stringPoolMatchCtx.insnContext().methodNode().name);
46-
47-
int stringPoolSize = stringPoolMatchCtx.captures().get("size").insn().asInteger();
48-
FieldInsnNode stringPoolFieldInsn = (FieldInsnNode) stringPoolMatchCtx.captures().get("stringPoolField").insn();
49-
50-
// Get whole number pool
51-
String[] stringPool = new String[stringPoolSize];
52-
STORE_STRING_TO_ARRAY_MATCH.findAllMatches(stringPoolMatchCtx.insnContext().methodContext()).forEach(storeNumberMatchCtx -> {
53-
int index = storeNumberMatchCtx.captures().get("index").insn().asInteger();
54-
String value = storeNumberMatchCtx.captures().get("value").insn().asString();
55-
56-
stringPool[index] = value;
57-
});
58-
59-
for (String string : stringPool) {
60-
if (string == null) {
61-
// String pool is not fully initialized
62-
return;
63-
}
64-
}
65-
66-
Match stringPoolReferenceMatch = OpcodeMatch.of(AALOAD) // AALOAD - Load array reference
67-
// Index
68-
.and(FrameMatch.stack(0, NumberMatch.of().capture("index")
69-
// Load number pool field
70-
.and(FrameMatch.stack(0, FieldMatch.getStatic().owner(stringPoolFieldInsn.owner).name(stringPoolFieldInsn.name).desc(stringPoolFieldInsn.desc)))
71-
));
72-
73-
// Replace number pool references with actual values
74-
classWrapper.methods().forEach(methodNode -> {
75-
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
76-
77-
stringPoolReferenceMatch.findAllMatches(methodContext).forEach(numberPoolReferenceCtx -> {
78-
int index = numberPoolReferenceCtx.captures().get("index").insn().asInteger();
79-
String value = stringPool[index];
80-
81-
// Value
82-
methodNode.instructions.insert(numberPoolReferenceCtx.insn(), new LdcInsnNode(value));
83-
numberPoolReferenceCtx.removeAll();
84-
markChange();
85-
});
86-
});
87-
88-
// Cleanup
89-
classWrapper.methods().remove(stringPoolMatchCtx.insnContext().methodNode());
90-
classWrapper.fields().removeIf(fieldNode -> fieldNode.name.equals(stringPoolFieldInsn.name) && fieldNode.desc.equals(stringPoolFieldInsn.desc));
91-
// Remove string pool initialization from clinit
92-
classWrapper.findClInit().ifPresent(clinit -> {
93-
for (AbstractInsnNode insn : clinit.instructions.toArray()) {
94-
if (insn.getOpcode() == INVOKESTATIC && insn instanceof MethodInsnNode methodInsn &&
95-
methodInsn.name.equals(stringPoolMatchCtx.insnContext().methodNode().name) && methodInsn.desc.equals(stringPoolMatchCtx.insnContext().methodNode().desc) &&
96-
methodInsn.owner.equals(classWrapper.name())
97-
) {
98-
// Remove invocation
99-
clinit.instructions.remove(insn);
100-
}
101-
}
102-
});
34+
.filter(Objects::nonNull).forEach(stringPoolMatchCtx -> {
35+
AtomicBoolean changedForThisContext = new AtomicBoolean(false);
36+
//System.out.println("Found string pool method in " + classWrapper.name() + "." + stringPoolMatchCtx.insnContext().methodNode().name);
37+
38+
int stringPoolSize = stringPoolMatchCtx.captures().get("size").insn().asInteger();
39+
FieldInsnNode stringPoolFieldInsn = (FieldInsnNode) stringPoolMatchCtx.captures().get("stringPoolField").insn();
40+
41+
// Get whole number pool
42+
String[] stringPool = new String[stringPoolSize];
43+
STORE_STRING_TO_ARRAY_MATCH.findAllMatches(stringPoolMatchCtx.insnContext().methodContext()).forEach(storeNumberMatchCtx -> {
44+
int index = storeNumberMatchCtx.captures().get("index").insn().asInteger();
45+
String value = storeNumberMatchCtx.captures().get("value").insn().asString();
46+
//System.out.println(classWrapper.name());
47+
stringPool[index] = value;
48+
});
49+
50+
for (String string : stringPool) {
51+
if (string == null) {
52+
// String pool is not fully initialized
53+
return;
54+
}
55+
}
56+
57+
Match stringPoolReferenceMatch = OpcodeMatch.of(AALOAD) // AALOAD - Load array reference
58+
// Index
59+
.and(FrameMatch.stack(0, NumberMatch.of().capture("index")
60+
// Load number pool field
61+
.and(FrameMatch.stack(0, FieldMatch.getStatic().owner(stringPoolFieldInsn.owner).name(stringPoolFieldInsn.name).desc(stringPoolFieldInsn.desc)))
62+
));
63+
64+
// Replace number pool references with actual values
65+
classWrapper.methods().forEach(methodNode -> {
66+
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
67+
68+
stringPoolReferenceMatch.findAllMatches(methodContext).forEach(numberPoolReferenceCtx -> {
69+
int index = numberPoolReferenceCtx.captures().get("index").insn().asInteger();
70+
String value = stringPool[index];
71+
72+
// Value
73+
methodNode.instructions.insert(numberPoolReferenceCtx.insn(), new LdcInsnNode(value));
74+
numberPoolReferenceCtx.removeAll();
75+
markChange();
76+
changedForThisContext.set(true);
77+
});
78+
});
79+
80+
if (changedForThisContext.get()) {
81+
toRemoveMn.add(stringPoolMatchCtx.insnContext().methodNode());
82+
classWrapper.fields().forEach(fieldNode -> {
83+
if (fieldNode.name.equals(stringPoolFieldInsn.name) && fieldNode.desc.equals(stringPoolFieldInsn.desc)) {
84+
toRemoveFn.add(fieldNode);
85+
}
86+
});
87+
// Remove string pool initialization from clinit
88+
classWrapper.findClInit().ifPresent(clinit -> {
89+
for (AbstractInsnNode insn : clinit.instructions.toArray()) {
90+
if (insn.getOpcode() == INVOKESTATIC && insn instanceof MethodInsnNode methodInsn &&
91+
methodInsn.name.equals(stringPoolMatchCtx.insnContext().methodNode().name) && methodInsn.desc.equals(stringPoolMatchCtx.insnContext().methodNode().desc) &&
92+
methodInsn.owner.equals(classWrapper.name())
93+
) {
94+
// Remove invocation
95+
clinit.instructions.remove(insn);
96+
}
97+
}
98+
});
99+
}
100+
});
101+
classWrapper.methods().removeAll(toRemoveMn);
102+
classWrapper.fields().removeAll(toRemoveFn);
103103
});
104104
}
105105
}

0 commit comments

Comments
 (0)