Skip to content

Commit 3df3d74

Browse files
committed
make transformers for qprotect AES string encryption and fix invokedynamic transformer
1 parent 8524532 commit 3df3d74

File tree

13 files changed

+522
-92
lines changed

13 files changed

+522
-92
lines changed

deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/ClassWrapper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ public Optional<MethodNode> findMethod(String name, String desc) {
4040
.findFirst();
4141
}
4242

43+
public Optional<MethodNode> findMethod(MethodRef methodRef) {
44+
if (!methodRef.owner().equals(classNode.name)) {
45+
return Optional.empty();
46+
}
47+
return classNode.methods.stream()
48+
.filter(methodNode -> methodNode.name.equals(methodRef.name()) && methodNode.desc.equals(methodRef.desc()))
49+
.findFirst();
50+
}
51+
4352
public Optional<MethodNode> findMethod(String name, Class<?> returnType, Class<?>... parameters) {
4453
String descriptor = MethodType.methodType(returnType, parameters).toMethodDescriptorString();
4554
return classNode.methods.stream()

deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/MethodRef.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package uwu.narumi.deobfuscator.api.asm;
22

3+
import org.objectweb.asm.Handle;
34
import org.objectweb.asm.tree.ClassNode;
45
import org.objectweb.asm.tree.MethodInsnNode;
56
import org.objectweb.asm.tree.MethodNode;
@@ -18,6 +19,10 @@ public static MethodRef of(MethodInsnNode methodInsn) {
1819
return new MethodRef(methodInsn.owner, methodInsn.name, methodInsn.desc);
1920
}
2021

22+
public static MethodRef of(Handle handle) {
23+
return new MethodRef(handle.getOwner(), handle.getName(), handle.getDesc());
24+
}
25+
2126
@Override
2227
public String toString() {
2328
return owner + "." + name + desc;

deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/matcher/impl/FieldMatch.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.objectweb.asm.Opcodes;
44
import org.objectweb.asm.tree.FieldInsnNode;
5+
import uwu.narumi.deobfuscator.api.asm.FieldRef;
56
import uwu.narumi.deobfuscator.api.asm.matcher.Match;
67
import uwu.narumi.deobfuscator.api.asm.matcher.MatchContext;
78

@@ -63,4 +64,11 @@ public FieldMatch desc(String desc) {
6364
this.desc = desc;
6465
return this;
6566
}
67+
68+
public FieldMatch fieldRef(FieldRef fieldRef) {
69+
this.owner = fieldRef.owner();
70+
this.name = fieldRef.name();
71+
this.desc = fieldRef.desc();
72+
return this;
73+
}
6674
}

deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/Context.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.objectweb.asm.tree.ClassNode;
1212
import software.coley.cafedude.InvalidClassException;
1313
import uwu.narumi.deobfuscator.api.asm.ClassWrapper;
14+
import uwu.narumi.deobfuscator.api.asm.FieldRef;
15+
import uwu.narumi.deobfuscator.api.asm.MethodRef;
1416
import uwu.narumi.deobfuscator.api.classpath.ClassProvider;
1517
import uwu.narumi.deobfuscator.api.classpath.ClassInfoStorage;
1618
import uwu.narumi.deobfuscator.api.classpath.CombinedClassProvider;
@@ -79,6 +81,16 @@ public Collection<ClassWrapper> classes() {
7981
return classesMap.values();
8082
}
8183

84+
public void removeMethod(MethodRef methodRef) {
85+
ClassWrapper classWrapper = this.getClassesMap().get(methodRef.owner());
86+
classWrapper.methods().removeIf(methodNode -> methodNode.name.equals(methodRef.name()) && methodNode.desc.equals(methodRef.desc()));
87+
}
88+
89+
public void removeField(FieldRef fieldRef) {
90+
ClassWrapper classWrapper = this.getClassesMap().get(fieldRef.owner());
91+
classWrapper.fields().removeIf(fieldNode -> fieldNode.name.equals(fieldRef.name()) && fieldNode.desc.equals(fieldRef.desc()));
92+
}
93+
8294
@UnmodifiableView
8395
public List<ClassWrapper> scopedClasses(ClassWrapper scope) {
8496
return classesMap.values().stream()

deobfuscator-impl/src/test/java/uwu/narumi/deobfuscator/TestDeobfuscation.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ protected void registerAll() {
167167
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_CLASS, "qprotect/sample1")
168168
.register();
169169

170+
test("qProtect Sample 2")
171+
.transformers(Composed_qProtectTransformer::new, RemapperTransformer::new)
172+
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_CLASS, "qprotect/sample2")
173+
.register();
174+
170175
// Superblaubeere
171176
test("Superblaubeere Sample 1")
172177
.transformers(ComposedSuperblaubeereTransformer::new)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
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.InlineStaticFieldTransformer;
7+
import uwu.narumi.deobfuscator.core.other.impl.qprotect.qProtectAESStringEncryptionTransformer;
78
import uwu.narumi.deobfuscator.core.other.impl.qprotect.qProtectFieldFlowTransformer;
89
import uwu.narumi.deobfuscator.core.other.impl.universal.pool.UniversalStringPoolTransformer;
910
import uwu.narumi.deobfuscator.core.other.impl.qprotect.qProtectStringTransformer;
@@ -39,6 +40,7 @@ public Composed_qProtectTransformer() {
3940

4041
// Decrypt strings
4142
qProtectStringTransformer::new,
43+
qProtectAESStringEncryptionTransformer::new,
4244
// Inline string pools
4345
UniversalStringPoolTransformer::new,
4446

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package uwu.narumi.deobfuscator.core.other.impl.qprotect;
2+
3+
import org.jetbrains.annotations.Nullable;
4+
import org.objectweb.asm.tree.AbstractInsnNode;
5+
import org.objectweb.asm.tree.FieldInsnNode;
6+
import org.objectweb.asm.tree.IntInsnNode;
7+
import org.objectweb.asm.tree.LdcInsnNode;
8+
import org.objectweb.asm.tree.MethodInsnNode;
9+
import org.objectweb.asm.tree.MethodNode;
10+
import uwu.narumi.deobfuscator.api.asm.ClassWrapper;
11+
import uwu.narumi.deobfuscator.api.asm.FieldRef;
12+
import uwu.narumi.deobfuscator.api.asm.MethodContext;
13+
import uwu.narumi.deobfuscator.api.asm.MethodRef;
14+
import uwu.narumi.deobfuscator.api.asm.matcher.Match;
15+
import uwu.narumi.deobfuscator.api.asm.matcher.MatchContext;
16+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.FieldMatch;
17+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.FrameMatch;
18+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.MethodMatch;
19+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.NumberMatch;
20+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.OpcodeMatch;
21+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.StringMatch;
22+
import uwu.narumi.deobfuscator.api.transformer.Transformer;
23+
import uwu.narumi.deobfuscator.core.other.impl.universal.pool.UniversalNumberPoolTransformer;
24+
25+
import javax.crypto.Cipher;
26+
import javax.crypto.SecretKeyFactory;
27+
import javax.crypto.spec.IvParameterSpec;
28+
import javax.crypto.spec.PBEKeySpec;
29+
import javax.crypto.spec.SecretKeySpec;
30+
import java.nio.charset.StandardCharsets;
31+
import java.util.Base64;
32+
import java.util.HashMap;
33+
import java.util.HashSet;
34+
import java.util.Map;
35+
import java.util.Set;
36+
37+
/**
38+
* Transforms AES encrypted strings in qProtect obfuscated code. Example here: {@link qprotect.AESStringEncryption}
39+
*/
40+
public class qProtectAESStringEncryptionTransformer extends Transformer {
41+
private static final Match DECRYPT_STRING_MATCH = MethodMatch.invokeStatic().desc("(Ljava/lang/String;Ljava/lang/String;[B)Ljava/lang/String;").capture("decrypt-method")
42+
.and(FrameMatch.stack(0, FieldMatch.getStatic().capture("iv-array")))
43+
.and(FrameMatch.stack(1, StringMatch.of().capture("password")))
44+
.and(FrameMatch.stack(2, StringMatch.of().capture("encrypted-data")));
45+
46+
private final Set<MethodRef> initIVArrayMethods = new HashSet<>();
47+
48+
@Override
49+
protected void transform() throws Exception {
50+
Map<FieldRef, byte[]> ivArrays = new HashMap<>();
51+
Map<MethodRef, Integer> iterationCounts = new HashMap<>();
52+
53+
scopedClasses().forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> {
54+
DECRYPT_STRING_MATCH.findAllMatches(MethodContext.of(classWrapper, methodNode)).forEach(matchCtx -> {
55+
// Decrypt method
56+
MethodInsnNode decryptMethodInsn = (MethodInsnNode) matchCtx.captures().get("decrypt-method").insn();
57+
MethodRef decryptMethodRef = MethodRef.of(decryptMethodInsn);
58+
// IV array field
59+
FieldInsnNode ivArrayFieldInsn = (FieldInsnNode) matchCtx.captures().get("iv-array").insn();
60+
FieldRef ivArrayFieldRef = FieldRef.of(ivArrayFieldInsn);
61+
String password = matchCtx.captures().get("password").insn().asString();
62+
String encryptedData = matchCtx.captures().get("encrypted-data").insn().asString();
63+
64+
// Get the IV array from the field
65+
byte[] ivArray = ivArrays.computeIfAbsent(ivArrayFieldRef, (k) -> {
66+
return extractIvArray(classWrapper, ivArrayFieldRef);
67+
});
68+
69+
// Get iteration count
70+
int iterationCount = iterationCounts.computeIfAbsent(MethodRef.of(decryptMethodInsn), (k) -> {
71+
// Find the decrypt method
72+
MethodNode decryptMethod = classWrapper.findMethod(decryptMethodRef).orElseThrow();
73+
return extractIterationCount(MethodContext.of(classWrapper, decryptMethod));
74+
});
75+
76+
// Decrypt string
77+
String decryptedString = decryptString(encryptedData, password, ivArray, iterationCount);
78+
methodNode.instructions.insert(matchCtx.insn(), new LdcInsnNode(decryptedString));
79+
matchCtx.removeAll();
80+
81+
markChange();
82+
});
83+
}));
84+
85+
// Cleanup
86+
ivArrays.keySet().forEach(fieldRef -> context().removeField(fieldRef));
87+
iterationCounts.keySet().forEach(methodRef -> context().removeMethod(methodRef));
88+
initIVArrayMethods.forEach(methodRef -> {
89+
context().removeMethod(methodRef);
90+
// Remove invocation from <clinit>
91+
ClassWrapper classWrapper = context().getClassesMap().get(methodRef.owner());
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(methodRef.name()) && methodInsn.desc.equals(methodRef.desc()) &&
96+
methodInsn.owner.equals(classWrapper.name())
97+
) {
98+
// Remove invocation
99+
clinit.instructions.remove(insn);
100+
}
101+
}
102+
});
103+
});
104+
}
105+
106+
private int extractIterationCount(MethodContext decryptMethod) {
107+
/*
108+
sipush 1838 // iteration count
109+
sipush 256
110+
invokespecial javax/crypto/spec/PBEKeySpec.<init> ([C[BII)V
111+
*/
112+
Match iterationCountMatch = MethodMatch.invokeSpecial().owner("javax/crypto/spec/PBEKeySpec").name("<init>").desc("([C[BII)V")
113+
.and(FrameMatch.stack(0, NumberMatch.of()))
114+
.and(FrameMatch.stack(1, NumberMatch.of().capture("iteration-count")));
115+
116+
MatchContext matchCtx = iterationCountMatch.findFirstMatch(decryptMethod);
117+
if (matchCtx == null) {
118+
throw new IllegalStateException("Could not find iteration count");
119+
}
120+
121+
// Get the iteration count from the match context
122+
return matchCtx.captures().get("iteration-count").insn().asInteger();
123+
}
124+
125+
private byte @Nullable [] extractIvArray(ClassWrapper classWrapper, FieldRef ivArrayFieldRef) {
126+
Match ivArrayMethodMatch = FieldMatch.putStatic().fieldRef(ivArrayFieldRef)
127+
.and(FrameMatch.stack(0,
128+
OpcodeMatch.of(NEWARRAY).and(Match.of(ctx -> ((IntInsnNode) ctx.insn()).operand == T_BYTE))
129+
.and(FrameMatch.stack(0, NumberMatch.of().capture("size")))));
130+
131+
for (MethodNode methodNode : classWrapper.methods()) {
132+
// Find match
133+
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
134+
MatchContext ivArrayMatchCtx = ivArrayMethodMatch.findFirstMatch(methodContext);
135+
136+
if (ivArrayMatchCtx == null) continue;
137+
138+
int size = ivArrayMatchCtx.captures().get("size").insn().asInteger();
139+
140+
Number[] ivArrayObj = UniversalNumberPoolTransformer.getNumberPool(methodContext, size, ivArrayFieldRef);
141+
byte[] ivArray = new byte[size];
142+
for (int i = 0; i < size; i++) {
143+
// Convert Number to byte
144+
ivArray[i] = ivArrayObj[i].byteValue();
145+
}
146+
147+
initIVArrayMethods.add(MethodRef.of(classWrapper.classNode(), methodNode));
148+
149+
return ivArray;
150+
}
151+
152+
// Not found
153+
return null;
154+
}
155+
156+
/**
157+
* Decrypts a Base64 encoded string using AES encryption with a password-based key derivation function (PBKDF2).
158+
*/
159+
private String decryptString(String base64EncryptedData, String password, byte[] ivArray, int iterationCount) {
160+
try {
161+
// Decode the Base64 encoded input string
162+
byte[] decodedData = Base64.getDecoder().decode(base64EncryptedData);
163+
164+
// Initialize salt array. This will be overwritten by the first 16 bytes of the decoded data.
165+
// The initial values here seem to be placeholders or defaults that are immediately replaced.
166+
//byte[] salt = new byte[]{124, 26, -30, -113, 87, 0, -111, -97, -126, 91, -12, 50, 77, 75, 6, -4}; // Dynamic
167+
byte[] salt = new byte[16];
168+
169+
// The actual encrypted content is after the first 32 bytes of the decoded data.
170+
// The first 16 bytes are used as the salt, and bytes 17-32 are skipped/unused.
171+
byte[] encryptedContent = new byte[decodedData.length - 32];
172+
173+
// Extract the salt from the first 16 bytes of the decoded data
174+
System.arraycopy(decodedData, 0, salt, 0, 16);
175+
// Extract the encrypted content, skipping the first 32 bytes (16 for salt, 16 unused)
176+
System.arraycopy(decodedData, 32, encryptedContent, 0, decodedData.length - 32);
177+
178+
// Configure the Password-Based Key Derivation Function (PBKDF2)
179+
// Uses the provided password, extracted salt, an iteration count of 1278, and a key length of 256 bits.
180+
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, iterationCount, 256); // Dynamic - iteration count
181+
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
182+
183+
// Generate the secret key from the PBEKeySpec
184+
byte[] derivedKey = secretKeyFactory.generateSecret(pbeKeySpec).getEncoded();
185+
186+
// Create a SecretKeySpec for AES using the derived key
187+
SecretKeySpec secretKeySpec = new SecretKeySpec(derivedKey, "AES");
188+
189+
// Initialize the Cipher for AES decryption in CBC mode with PKCS5Padding
190+
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
191+
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(ivArray));
192+
193+
// Perform the decryption
194+
byte[] decryptedBytes = cipher.doFinal(encryptedContent);
195+
196+
// Convert the decrypted bytes to a String using UTF-8 encoding
197+
return new String(decryptedBytes, StandardCharsets.UTF_8);
198+
} catch (Exception e) {
199+
throw new RuntimeException(e);
200+
}
201+
}
202+
}

deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/qprotect/qProtectInvokeDynamicTransformer.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
import uwu.narumi.deobfuscator.api.transformer.Transformer;
1313

1414
import java.util.Arrays;
15+
import java.util.HashMap;
1516
import java.util.Map;
1617

1718
/**
1819
* Transforms encrypted method invocations in qProtect obfuscated code. Example here: {@link qprotect.InvokeDynamicSample}
1920
*/
2021
public class qProtectInvokeDynamicTransformer extends Transformer {
21-
private DecryptionInfo decryptionInfo = null;
2222

2323
@Override
2424
protected void transform() throws Exception {
25+
Map<MethodRef, DecryptionInfo> decryptionMethods = new HashMap<>();
26+
2527
scopedClasses().forEach(classWrapper -> {
2628
classWrapper.methods().forEach(methodNode -> {
2729

@@ -32,14 +34,16 @@ protected void transform() throws Exception {
3234
insn.getOpcode() == INVOKEDYNAMIC &&
3335
invokeDynamicInsn.bsm.getDesc().equals("(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;")
3436
) {
37+
// Get decryption information
38+
MethodRef decryptMethodRef = MethodRef.of(invokeDynamicInsn.bsm);
39+
DecryptionInfo decryptionInfo = decryptionMethods.computeIfAbsent(decryptMethodRef, (k) -> {
40+
// Compute if absent
41+
return extractDecryptionInformation(invokeDynamicInsn);
42+
});
43+
3544
String xorKey = invokeDynamicInsn.name;
3645
String encryptedData = (String) invokeDynamicInsn.bsmArgs[0];
3746

38-
if (decryptionInfo == null) {
39-
// Lazy load decryption information
40-
this.decryptionInfo = extractDecryptionInformation(invokeDynamicInsn);
41-
}
42-
4347
// Decrypt the method invocation
4448
AbstractInsnNode decryptedInsn = decryptMethodInvocation(xorKey, encryptedData, decryptionInfo);
4549
methodNode.instructions.set(invokeDynamicInsn, decryptedInsn);
@@ -49,6 +53,12 @@ protected void transform() throws Exception {
4953
}
5054
});
5155
});
56+
57+
// Remove decryption methods
58+
decryptionMethods.keySet().forEach(methodRef -> {
59+
context().getClassesMap().get(methodRef.owner()).methods()
60+
.removeIf(methodNode -> methodNode.name.equals(methodRef.name()) && methodNode.desc.equals(methodRef.desc()));
61+
});
5262
}
5363

5464
/**
@@ -84,7 +94,7 @@ private DecryptionInfo extractDecryptionInformation(InvokeDynamicInsnNode invoke
8494

8595
// Store decryption information
8696
return new DecryptionInfo(
87-
new MethodRef(invokeDynamicInsn.bsm.getOwner(), invokeDynamicInsn.bsm.getName(), invokeDynamicInsn.bsm.getDesc()),
97+
MethodRef.of(invokeDynamicInsn.bsm),
8898
dataSeparator,
8999
invocationTypes
90100
);

0 commit comments

Comments
 (0)