Skip to content

Commit 2a18fa6

Browse files
committed
fix multi-b64 comment drop in chained expressions
1 parent 5bb8c9e commit 2a18fa6

3 files changed

Lines changed: 143 additions & 1 deletion

File tree

src/main/java/jadx/plugins/stringdecoder/B64DeobfuscatePass.java

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import jadx.core.dex.attributes.AType;
2121
import jadx.core.dex.instructions.ConstStringNode;
2222
import jadx.core.dex.instructions.FilledNewArrayNode;
23+
import jadx.core.dex.instructions.InvokeNode;
2324
import jadx.core.dex.instructions.args.InsnArg;
2425
import jadx.core.dex.instructions.args.RegisterArg;
2526
import jadx.core.dex.instructions.args.SSAVar;
@@ -135,7 +136,13 @@ public void visit(MethodNode mth) {
135136
if (fieldConstants.contains(str)) {
136137
continue;
137138
}
138-
csn.addAttr(AType.CODE_COMMENTS, new CodeComment(decoded.commentText(), CommentStyle.LINE));
139+
// Attach to the top-level statement rather than the ConstStringNode so that
140+
// comments from multiple decoded strings in one expression all survive.
141+
// inheritMetadata uses the untyped addAttr (map.put = replace), so comments
142+
// riding up through inlining chains overwrite each other; attaching directly
143+
// to the statement uses the typed addAttr (list.add = merge) and bypasses that.
144+
InsnNode stmtInsn = findStatementInsn(csn);
145+
stmtInsn.addAttr(AType.CODE_COMMENTS, new CodeComment(decoded.commentText(), CommentStyle.LINE));
139146
}
140147
}
141148

@@ -149,6 +156,86 @@ public void visit(MethodNode mth) {
149156
}
150157
}
151158

159+
/**
160+
* Walks the SSA def-use chain from {@code csn} upward to find the top-level statement
161+
* instruction (the one that is not itself consumed by another instruction).
162+
* Attaching CODE_COMMENTS there — using the typed, list-merging addAttr — avoids the
163+
* replace-on-copy problem in inheritMetadata when multiple decoded strings live in the
164+
* same chained expression.
165+
*/
166+
private static InsnNode findStatementInsn(ConstStringNode csn) {
167+
RegisterArg result = csn.getResult();
168+
if (result == null) {
169+
return csn;
170+
}
171+
SSAVar var = result.getSVar();
172+
if (var == null) {
173+
return csn;
174+
}
175+
InsnNode current = csn;
176+
for (int depth = 0; depth < 16; depth++) {
177+
List<RegisterArg> uses = var.getUseList();
178+
if (uses.size() != 1) {
179+
break;
180+
}
181+
InsnNode useInsn = uses.get(0).getParentInsn();
182+
if (useInsn == null) {
183+
break;
184+
}
185+
RegisterArg useResult = useInsn.getResult();
186+
if (useResult == null) {
187+
// Constructor call (invoke-direct on <init>): the constructed object flows through
188+
// arg[0] rather than a result register — pivot to its SSAVar and continue tracing.
189+
if (useInsn instanceof InvokeNode && ((InvokeNode) useInsn).getCallMth().isConstructor()) {
190+
InsnArg arg0 = useInsn.getArg(0);
191+
if (arg0 instanceof RegisterArg) {
192+
SSAVar ctorVar = ((RegisterArg) arg0).getSVar();
193+
if (ctorVar != null) {
194+
InsnNode postCtorUser = singlePostCtorUser(ctorVar, useInsn);
195+
if (postCtorUser != null) {
196+
RegisterArg postResult = postCtorUser.getResult();
197+
if (postResult == null) {
198+
return postCtorUser;
199+
}
200+
SSAVar postVar = postResult.getSVar();
201+
if (postVar == null || postVar.getUseList().isEmpty()) {
202+
return postCtorUser;
203+
}
204+
current = postCtorUser;
205+
var = postVar;
206+
continue;
207+
}
208+
}
209+
}
210+
}
211+
return useInsn;
212+
}
213+
SSAVar useVar = useResult.getSVar();
214+
if (useVar == null || useVar.getUseList().isEmpty()) {
215+
return useInsn;
216+
}
217+
current = useInsn;
218+
var = useVar;
219+
}
220+
return current;
221+
}
222+
223+
/** Returns the single use of {@code ctorVar} that is not the constructor call itself, or null. */
224+
private static InsnNode singlePostCtorUser(SSAVar ctorVar, InsnNode ctorInsn) {
225+
InsnNode found = null;
226+
for (RegisterArg use : ctorVar.getUseList()) {
227+
InsnNode parent = use.getParentInsn();
228+
if (parent == null || parent == ctorInsn) {
229+
continue;
230+
}
231+
if (found != null) {
232+
return null;
233+
}
234+
found = parent;
235+
}
236+
return found;
237+
}
238+
152239
/** Returns the FilledNewArrayNode that directly uses the result of {@code csn}, or null. */
153240
private static FilledNewArrayNode findFilledNewArrayParent(ConstStringNode csn) {
154241
RegisterArg result = csn.getResult();

src/test/java/jadx/plugins/stringdecoder/JadxStringDecoderPluginTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,18 @@ public void pascalCaseSkipDisabledTest() throws Exception {
361361
assertThat(code).doesNotContain("b64:");
362362
}
363363

364+
@Test
365+
public void multiB64InvokeChainTest() throws Exception {
366+
// Two const-strings both used as direct Base64.decode args in the same method chain:
367+
// Class.forName(new String(Base64.decode("...", 0)))
368+
// .getMethod(new String(Base64.decode("...", 0)), ...)
369+
// Both should get b64: comments — bug: only the first was annotated
370+
String code = decompileSmali("b64/multi_b64_invoke.smali");
371+
System.out.println(code);
372+
assertThat(code).contains("b64: android.app.ActivityThread");
373+
assertThat(code).contains("b64: getPackageManager");
374+
}
375+
364376
@Test
365377
public void byteArrayStringPassDisabledTest() throws Exception {
366378
// with enableByteArrayStringPass=false the bytes: comment must not appear
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.class public LMultiB64Invoke;
2+
.super Ljava/lang/Object;
3+
4+
# Models the pattern:
5+
# Class.forName(new String(Base64.decode("YW5k...", 0)))
6+
# .getMethod(new String(Base64.decode("Z2V0...", 0)), new Class[0])
7+
# .invoke(null, new Object[0])
8+
# Both const-strings are direct args to Base64.decode — both should get b64: comments.
9+
10+
.method public static a()Ljava/lang/Object;
11+
.registers 6
12+
13+
# "YW5kcm9pZC5hcHAuQWN0aXZpdHlUaHJlYWQ=" -> "android.app.ActivityThread"
14+
const-string v0, "YW5kcm9pZC5hcHAuQWN0aXZpdHlUaHJlYWQ="
15+
const/4 v1, 0x0
16+
invoke-static {v0, v1}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
17+
move-result-object v0
18+
new-instance v2, Ljava/lang/String;
19+
invoke-direct {v2, v0}, Ljava/lang/String;-><init>([B)V
20+
invoke-static {v2}, Ljava/lang/Class;->forName(Ljava/lang/String;)Ljava/lang/Class;
21+
move-result-object v2
22+
23+
# "Z2V0UGFja2FnZU1hbmFnZXI=" -> "getPackageManager"
24+
const-string v0, "Z2V0UGFja2FnZU1hbmFnZXI="
25+
const/4 v1, 0x0
26+
invoke-static {v0, v1}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
27+
move-result-object v0
28+
new-instance v3, Ljava/lang/String;
29+
invoke-direct {v3, v0}, Ljava/lang/String;-><init>([B)V
30+
31+
const/4 v4, 0x0
32+
new-array v4, v4, [Ljava/lang/Class;
33+
invoke-virtual {v2, v3, v4}, Ljava/lang/Class;->getMethod(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;
34+
move-result-object v2
35+
36+
const/4 v3, 0x0
37+
const/4 v4, 0x0
38+
new-array v4, v4, [Ljava/lang/Object;
39+
invoke-virtual {v2, v3, v4}, Ljava/lang/reflect/Method;->invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
40+
move-result-object v0
41+
42+
return-object v0
43+
.end method

0 commit comments

Comments
 (0)