Skip to content

Commit f119596

Browse files
Branchlock compability transformer (#113)
* Branchlock compability transformer * test data * requested changes * branchlock salting * branchlock number variations * composed branchlock * composed branchlock * tests * fix tests * tests dec files * fix weird issues with charset and delete unused result files --------- Co-authored-by: EpicPlayerA10 <adasko.lesiak@gmail.com>
1 parent dc9a241 commit f119596

File tree

79 files changed

+6652
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+6652
-6
lines changed

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,46 @@ public static void registerInterpreter(MethodInterpreter interpreter) {
152152
return null;
153153
}
154154
));
155+
// Long#parseLong or Long#valueOf
156+
registerInterpreter(MethodInterpreter.of(
157+
MethodMatch.invokeStatic()
158+
.owner("java/lang/Long")
159+
.name("parseLong", "valueOf")
160+
.desc("(Ljava/lang/String;)J"),
161+
(insn, stackValues) -> {
162+
// Get value from stack
163+
OriginalSourceValue sourceValue = stackValues.get(stackValues.size() - 1);
164+
if (sourceValue.getConstantValue() != null && sourceValue.getConstantValue().value() instanceof String text) {
165+
try {
166+
return OriginalSourceValue.ConstantValue.of(Long.parseLong(text));
167+
} catch (NumberFormatException e) {
168+
return null;
169+
}
170+
}
171+
return null;
172+
}
173+
));
174+
// Long#parseLong with radix
175+
registerInterpreter(MethodInterpreter.of(
176+
MethodMatch.invokeStatic()
177+
.owner("java/lang/Long")
178+
.name("parseLong", "valueOf")
179+
.desc("(Ljava/lang/String;I)J"),
180+
(insn, stackValues) -> {
181+
// Get values from stack
182+
OriginalSourceValue firstValue = stackValues.get(stackValues.size() - 2);
183+
OriginalSourceValue secondValue = stackValues.get(stackValues.size() - 1);
184+
if (firstValue.getConstantValue() != null && firstValue.getConstantValue().value() instanceof String text &&
185+
secondValue.getConstantValue() != null && secondValue.getConstantValue().value() instanceof Integer radix) {
186+
try {
187+
return OriginalSourceValue.ConstantValue.of(Long.parseLong(text, radix));
188+
} catch (NumberFormatException e) {
189+
return null;
190+
}
191+
}
192+
return null;
193+
}
194+
));
155195
// Double#doubleToLongBits
156196
registerInterpreter(MethodInterpreter.of(
157197
MethodMatch.invokeStatic()

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
package uwu.narumi.deobfuscator;
22

3-
import uwu.narumi.deobfuscator.core.other.composed.ComposedGruntTransformer;
4-
import uwu.narumi.deobfuscator.core.other.composed.ComposedHP888Transformer;
5-
import uwu.narumi.deobfuscator.core.other.composed.ComposedSuperblaubeereTransformer;
6-
import uwu.narumi.deobfuscator.core.other.composed.ComposedUnknownObf1Transformer;
7-
import uwu.narumi.deobfuscator.core.other.composed.ComposedZelixTransformer;
8-
import uwu.narumi.deobfuscator.core.other.composed.Composed_qProtectTransformer;
3+
import uwu.narumi.deobfuscator.core.other.composed.*;
94
import uwu.narumi.deobfuscator.core.other.composed.general.ComposedGeneralFlowTransformer;
105
import uwu.narumi.deobfuscator.core.other.composed.general.ComposedPeepholeCleanTransformer;
6+
import uwu.narumi.deobfuscator.core.other.impl.branchlock.BranchlockCompabilityStringTransformer;
117
import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.JsrInlinerTransformer;
128
import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UselessPopCleanTransformer;
139
import uwu.narumi.deobfuscator.core.other.impl.pool.InlineLocalVariablesTransformer;
@@ -212,5 +208,15 @@ protected void registerAll() {
212208
.transformers(UselessPopCleanTransformer::new)
213209
.inputClass(InputType.CUSTOM_CLASS, "KotlinSample3.class")
214210
.register();
211+
212+
test("Branchlock String")
213+
.transformers(UniversalNumberTransformer::new, () -> new BranchlockCompabilityStringTransformer(true))
214+
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_JAR, "branchlock/branchlock-string.jar")
215+
.register();
216+
217+
test("Branchlock String + Salting + Number")
218+
.transformers(ComposedBranchlockTransformer::new)
219+
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_JAR, "branchlock/branchlock-string-salting-number.jar")
220+
.register();
215221
}
216222
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package uwu.narumi.deobfuscator.core.other.composed;
2+
3+
import uwu.narumi.deobfuscator.api.transformer.ComposedTransformer;
4+
import uwu.narumi.deobfuscator.core.other.composed.general.ComposedGeneralFlowTransformer;
5+
import uwu.narumi.deobfuscator.core.other.composed.general.ComposedGeneralRepairTransformer;
6+
import uwu.narumi.deobfuscator.core.other.impl.branchlock.BranchlockCompabilityStringTransformer;
7+
import uwu.narumi.deobfuscator.core.other.impl.branchlock.BranchlockSaltingTransformer;
8+
import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalNumberTransformer;
9+
10+
public class ComposedBranchlockTransformer extends ComposedTransformer {
11+
public ComposedBranchlockTransformer() {
12+
super(
13+
() -> new ComposedTransformer(true,
14+
UniversalNumberTransformer::new,
15+
() -> new BranchlockCompabilityStringTransformer(false),
16+
BranchlockSaltingTransformer::new),
17+
ComposedGeneralRepairTransformer::new, // Deletes "Logic Scrambler"
18+
ComposedGeneralFlowTransformer::new // Deobfuscates part of flow obfuscation
19+
);
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package uwu.narumi.deobfuscator.core.other.impl.branchlock;
2+
3+
import org.objectweb.asm.tree.*;
4+
import uwu.narumi.deobfuscator.api.asm.MethodContext;
5+
import uwu.narumi.deobfuscator.api.asm.matcher.Match;
6+
import uwu.narumi.deobfuscator.api.asm.matcher.MatchContext;
7+
import uwu.narumi.deobfuscator.api.asm.matcher.group.SequenceMatch;
8+
import uwu.narumi.deobfuscator.api.asm.matcher.impl.*;
9+
import uwu.narumi.deobfuscator.api.transformer.Transformer;
10+
11+
import java.util.Arrays;
12+
import java.util.concurrent.atomic.AtomicInteger;
13+
14+
public class BranchlockCompabilityStringTransformer extends Transformer {
15+
16+
private final boolean deleteClinit;
17+
private String[] decryptedStrings;
18+
private FieldInsnNode stringArray;
19+
20+
public BranchlockCompabilityStringTransformer(boolean deleteClinit) {
21+
this.deleteClinit = deleteClinit;
22+
}
23+
24+
@Override
25+
protected void transform() throws Exception {
26+
scopedClasses().forEach(classWrapper -> {
27+
decryptedStrings = null;
28+
stringArray = null;
29+
classWrapper.findClInit().ifPresent(clinit -> {
30+
MethodContext methodContext = MethodContext.of(classWrapper, clinit);
31+
String className = classWrapper.name().replace("/", ".");
32+
String methodName = clinit.name;
33+
Arrays.stream(clinit.instructions.toArray())
34+
.filter(ain -> ain instanceof LdcInsnNode)
35+
.map(LdcInsnNode.class::cast)
36+
.filter(ldc -> ldc.cst instanceof String)
37+
.findFirst().ifPresent(ldc -> {
38+
Match stringArr = OpcodeMatch.of(PUTSTATIC).capture("string-arr");
39+
stringArray = stringArr.findFirstMatch(methodContext).insn().asFieldInsn();
40+
41+
String encryptedString = (String) ldc.cst;
42+
char[] encryptedStringArray = encryptedString.toCharArray();
43+
Match match = SequenceMatch.of(OpcodeMatch.of(DUP), NumberMatch.numInteger().capture("array-to"), OpcodeMatch.of(SWAP), NumberMatch.numInteger().capture("array-from"), OpcodeMatch.of(CALOAD), OpcodeMatch.of(CASTORE), OpcodeMatch.of(CASTORE));
44+
45+
/* First "char swapper" salting */
46+
for (MatchContext allMatch : match.findAllMatches(methodContext)) {
47+
int arrayFrom = allMatch.captures().get("array-from").insn().asInteger();
48+
int arrayTo = allMatch.captures().get("array-to").insn().asInteger();
49+
try {
50+
char store = encryptedStringArray[arrayFrom];
51+
encryptedStringArray[arrayFrom] = encryptedStringArray[arrayTo];
52+
encryptedStringArray[arrayTo] = store;
53+
} catch (Exception e) {
54+
break;
55+
}
56+
}
57+
58+
int encCharIndex = 0; // Under LDC
59+
60+
int decStrIndex = 0; // Under new StringArr
61+
62+
/* Finding new String Array creator for decrypted Strings */
63+
Match newArray = SequenceMatch.of(NumberMatch.numInteger().capture("salt"), OpcodeMatch.of(IXOR), OpcodeMatch.of(ILOAD), OpcodeMatch.of(IXOR), OpcodeMatch.of(ANEWARRAY));
64+
int salt = newArray.findFirstMatch(methodContext).captures().get("salt").insn().asInteger();
65+
int methodSalt = (methodName.hashCode() & 0xFFFF);
66+
67+
char[] classNameArray = className.toCharArray();
68+
69+
decryptedStrings = new String[encryptedStringArray[encCharIndex++] ^ salt ^ methodSalt];
70+
71+
Match saltOfStrLen = SequenceMatch.of(OpcodeMatch.of(IINC), OpcodeMatch.of(CALOAD), NumberMatch.numInteger().capture("salt"), OpcodeMatch.of(IXOR), OpcodeMatch.of(ILOAD), OpcodeMatch.of(IXOR), OpcodeMatch.of(ISTORE));
72+
int salt2 = saltOfStrLen.findFirstMatch(methodContext).captures().get("salt").insn().asInteger();
73+
74+
Match switchMatch = SequenceMatch.of(NumberMatch.numInteger().capture("switch-salt"), OpcodeMatch.of(IXOR), OpcodeMatch.of(LOOKUPSWITCH).capture("switch-table"));
75+
MatchContext switchContext = switchMatch.findFirstMatch(methodContext);
76+
int switchSalt = switchContext.captures().get("switch-salt").insn().asInteger();
77+
LookupSwitchInsnNode switchInsnNode = (LookupSwitchInsnNode) switchContext.captures().get("switch-table").insn();
78+
79+
Match switch2Match = SequenceMatch.of(OpcodeMatch.of(ILOAD), OpcodeMatch.of(TABLESWITCH).capture("switch-table"));
80+
MatchContext switch2Context = switch2Match.findFirstMatch(methodContext);
81+
TableSwitchInsnNode tableSwitchInsnNode = (TableSwitchInsnNode) switch2Context.captures().get("switch-table").insn();
82+
83+
84+
/* Creating same simulation */
85+
while (encCharIndex < encryptedStringArray.length) {
86+
int strLength = encryptedStringArray[encCharIndex++] ^ salt2 ^ methodSalt;
87+
char[] toDecrypt = new char[strLength];
88+
int decCharIndex = 0; // Under var9_8 = new char[var2_2];
89+
90+
while (strLength > 0) {
91+
char nowDecrypted = encryptedStringArray[encCharIndex];
92+
int switch2Value = 0;
93+
94+
Match swapKey = SequenceMatch.of(NumberMatch.numInteger().capture("swap-key"), OpcodeMatch.of(ISTORE), OpcodeMatch.of(GOTO));
95+
Match xorKey = SequenceMatch.of(OpcodeMatch.of(ILOAD), NumberMatch.numInteger().capture("xor-key"), OpcodeMatch.of(IXOR));
96+
97+
int switchValue = classNameArray[encCharIndex % classNameArray.length] ^ switchSalt;
98+
LabelNode switchCase = getLabelByKey(switchInsnNode, switchValue);
99+
AtomicInteger s2v = new AtomicInteger(-1337);
100+
swapKey.findAllMatches(methodContext).forEach(matchContext -> {
101+
MatchContext capturedSwapKey = matchContext.captures().get("swap-key");
102+
if (isInsnInLabelRange(clinit, switchCase, capturedSwapKey.insn())) {
103+
s2v.set(capturedSwapKey.insn().asInteger());
104+
}
105+
});
106+
107+
if (s2v.get() != -1337) {
108+
switch2Value = s2v.get();
109+
}
110+
111+
AtomicInteger xor = new AtomicInteger(-1337);
112+
xorKey.findAllMatches(methodContext).forEach(matchContext -> {
113+
MatchContext capturedXorKey = matchContext.captures().get("xor-key");
114+
if (isInsnInLabelRange(clinit, switchCase, capturedXorKey.insn())) {
115+
xor.set(capturedXorKey.insn().asInteger());
116+
}
117+
});
118+
119+
if (xor.get() != -1337) {
120+
nowDecrypted ^= xor.get();
121+
}
122+
123+
if (switch2Value == 0 && xor.get() == -1337) {
124+
toDecrypt[decCharIndex] = nowDecrypted;
125+
++decCharIndex;
126+
++encCharIndex;
127+
--strLength;
128+
switch2Value = 0;
129+
continue;
130+
}
131+
132+
if (switch2Value == 1) {
133+
toDecrypt[decCharIndex] = nowDecrypted;
134+
++decCharIndex;
135+
++encCharIndex;
136+
--strLength;
137+
switch2Value = 0;
138+
continue;
139+
}
140+
141+
while (true) {
142+
LabelNode tableCase = getLabelByKey(tableSwitchInsnNode, switch2Value);
143+
144+
AtomicInteger s2v2 = new AtomicInteger(-1337);
145+
swapKey.findAllMatches(methodContext).forEach(matchContext -> {
146+
MatchContext capturedSwapKey = matchContext.captures().get("swap-key");
147+
if (isInsnInLabelRange(clinit, tableCase, capturedSwapKey.insn())) {
148+
s2v2.set(capturedSwapKey.insn().asInteger());
149+
}
150+
});
151+
152+
switch2Value = s2v2.get();
153+
154+
AtomicInteger xor2 = new AtomicInteger(-1337);
155+
xorKey.findAllMatches(methodContext).forEach(matchContext -> {
156+
MatchContext capturedXorKey = matchContext.captures().get("xor-key");
157+
if (isInsnInLabelRange(clinit, tableCase, capturedXorKey.insn())) {
158+
xor2.set(capturedXorKey.insn().asInteger());
159+
}
160+
});
161+
162+
if (xor2.get() != -1337) {
163+
nowDecrypted ^= xor2.get();
164+
}
165+
166+
if (switch2Value == 1) {
167+
toDecrypt[decCharIndex] = nowDecrypted;
168+
++decCharIndex;
169+
++encCharIndex;
170+
--strLength;
171+
switch2Value = 0;
172+
break;
173+
}
174+
}
175+
}
176+
decryptedStrings[decStrIndex++] = new String(toDecrypt).intern();
177+
}
178+
});
179+
});
180+
181+
if (stringArray != null) {
182+
if (deleteClinit) {
183+
classWrapper.methods().remove(classWrapper.findClInit().get());
184+
}
185+
classWrapper.fields().removeIf(fieldNode -> fieldNode.name.equals(stringArray.name) && fieldNode.desc.equals(stringArray.desc));
186+
187+
classWrapper.methods().forEach(methodNode -> {
188+
MethodContext methodContext = MethodContext.of(classWrapper, methodNode);
189+
methodNode.instructions.forEach(abstractInsnNode -> {
190+
Match getString = SequenceMatch.of(FieldMatch.getStatic().owner(stringArray.owner).name(stringArray.name).desc(stringArray.desc), NumberMatch.numInteger().capture("array-index"), OpcodeMatch.of(AALOAD));
191+
getString.findAllMatches(methodContext).forEach(matchContext -> {
192+
int arrayIndex = matchContext.captures().get("array-index").insn().asInteger();
193+
if (decryptedStrings.length <= arrayIndex) {
194+
LOGGER.error("Number on GETSTATIC isnt properly deobfuscated");
195+
return;
196+
197+
}
198+
methodNode.instructions.insert(matchContext.insn(), new LdcInsnNode(decryptedStrings[arrayIndex]));
199+
matchContext.removeAll();
200+
markChange();
201+
});
202+
});
203+
});
204+
}
205+
});
206+
}
207+
208+
public LabelNode getLabelByKey(LookupSwitchInsnNode lsi, int key) {
209+
int index = lsi.keys.indexOf(key);
210+
if (index == -1) {
211+
return lsi.dflt;
212+
}
213+
return lsi.labels.get(index);
214+
}
215+
216+
public LabelNode getLabelByKey(TableSwitchInsnNode tsi, int key) {
217+
if (key < tsi.min || key > tsi.max) {
218+
return tsi.dflt;
219+
}
220+
int index = key - tsi.min;
221+
return tsi.labels.get(index);
222+
}
223+
224+
public boolean isInsnInLabelRange(MethodNode method, LabelNode startLabel, AbstractInsnNode insn) {
225+
InsnList instructions = method.instructions;
226+
227+
int startIndex = instructions.indexOf(startLabel);
228+
if (startIndex == -1) return false;
229+
230+
int insnIndex = instructions.indexOf(insn);
231+
if (insnIndex == -1) return false;
232+
233+
int endIndex = instructions.size();
234+
for (int i = startIndex + 1; i < instructions.size(); i++) {
235+
if (instructions.get(i) instanceof LabelNode) {
236+
endIndex = i;
237+
break;
238+
}
239+
}
240+
241+
return insnIndex > startIndex && insnIndex < endIndex;
242+
}
243+
}

0 commit comments

Comments
 (0)