22
33import java .util .Collections ;
44import java .util .HashSet ;
5+ import java .util .LinkedHashMap ;
6+ import java .util .LinkedHashSet ;
57import java .util .List ;
8+ import java .util .Map ;
69import java .util .Set ;
10+ import java .util .TreeMap ;
711
812import jadx .api .data .CommentStyle ;
913import jadx .api .plugins .input .data .annotations .EncodedType ;
1519import jadx .core .codegen .utils .CodeComment ;
1620import jadx .core .dex .attributes .AType ;
1721import jadx .core .dex .instructions .ConstStringNode ;
22+ import jadx .core .dex .instructions .FilledNewArrayNode ;
23+ import jadx .core .dex .instructions .args .InsnArg ;
1824import jadx .core .dex .instructions .args .RegisterArg ;
25+ import jadx .core .dex .instructions .args .SSAVar ;
1926import jadx .core .dex .nodes .BlockNode ;
2027import jadx .core .dex .nodes .ClassNode ;
2128import jadx .core .dex .nodes .FieldNode ;
2229import jadx .core .dex .nodes .InsnNode ;
2330import jadx .core .dex .nodes .MethodNode ;
2431import jadx .core .dex .nodes .RootNode ;
25- import jadx .core .dex .instructions .args .SSAVar ;
2632
2733public class B64DeobfuscatePass implements JadxDecompilePass {
2834
@@ -61,6 +67,11 @@ public void visit(MethodNode mth) {
6167 }
6268 // Lazily built on the first B64 hit — avoids field scan for methods with no B64 strings
6369 Set <String > fieldConstants = null ;
70+ // Arrays where ≥1 string passed normal detect() — required anchor for contextual decoding
71+ Set <InsnNode > arrayAnchors = null ;
72+ // All valid-B64+UTF-8 strings in arrays (superset of anchors; used for the final comment)
73+ Map <InsnNode , TreeMap <Integer , String >> arrayCandidates = null ;
74+
6475 for (BlockNode block : blocks ) {
6576 for (InsnNode insn : block .getInstructions ()) {
6677 if (!(insn instanceof ConstStringNode )) {
@@ -75,18 +86,109 @@ public void visit(MethodNode mth) {
7586 String decoded = forced
7687 ? B64Detector .decodeForced (str , options .getMaxCommentLength ())
7788 : B64Detector .detect (str , options );
89+
90+ // If this string feeds a FilledNewArrayNode, use contextual detection:
91+ // a string that fails heuristics (e.g. skipIdentifiers) is still included as a
92+ // candidate if at least one sibling in the array passes normal detection.
93+ FilledNewArrayNode arrayParent = findFilledNewArrayParent (csn );
94+ if (arrayParent != null ) {
95+ int idx = argIndexOf (arrayParent , csn .getResult ().getSVar ());
96+ if (idx >= 0 ) {
97+ // Determine the best available decoded value for this slot
98+ String candidate = decoded != null ? decoded
99+ : B64Detector .decodeIfValid (str , options .getMaxCommentLength ());
100+ if (candidate != null ) {
101+ if (fieldConstants == null ) {
102+ fieldConstants = collectConstantValueFieldStrings (mth .getParentClass ());
103+ }
104+ if (!fieldConstants .contains (str )) {
105+ if (arrayCandidates == null ) {
106+ arrayCandidates = new LinkedHashMap <>();
107+ }
108+ arrayCandidates .computeIfAbsent (arrayParent , k -> new TreeMap <>()).put (idx , candidate );
109+ if (decoded != null ) {
110+ if (arrayAnchors == null ) {
111+ arrayAnchors = new LinkedHashSet <>();
112+ }
113+ arrayAnchors .add (arrayParent );
114+ }
115+ }
116+ }
117+ continue ;
118+ }
119+ }
120+
78121 if (decoded == null ) {
79122 continue ;
80123 }
81124 if (fieldConstants == null ) {
82125 fieldConstants = collectConstantValueFieldStrings (mth .getParentClass ());
83126 }
84127 // Skip strings that are static final CONSTANT_VALUE fields — B64FieldInitPass handles those
85- if (! fieldConstants .contains (str )) {
86- csn . addAttr ( AType . CODE_COMMENTS , new CodeComment ( "b64: " + decoded , CommentStyle . LINE )) ;
128+ if (fieldConstants .contains (str )) {
129+ continue ;
87130 }
131+ csn .addAttr (AType .CODE_COMMENTS , new CodeComment ("b64: " + decoded , CommentStyle .LINE ));
132+ }
133+ }
134+
135+ // Only emit array comments for arrays with at least one confirmed detection (the anchor).
136+ // This prevents annotating arrays that happen to contain valid-looking Base64 by coincidence.
137+ if (arrayAnchors != null ) {
138+ for (InsnNode arrayInsn : arrayAnchors ) {
139+ String comment = buildArrayComment (arrayCandidates .get (arrayInsn ));
140+ arrayInsn .addAttr (AType .CODE_COMMENTS , new CodeComment (comment , CommentStyle .LINE ));
141+ }
142+ }
143+ }
144+
145+ /** Returns the FilledNewArrayNode that directly uses the result of {@code csn}, or null. */
146+ private static FilledNewArrayNode findFilledNewArrayParent (ConstStringNode csn ) {
147+ RegisterArg result = csn .getResult ();
148+ if (result == null ) {
149+ return null ;
150+ }
151+ SSAVar ssaVar = result .getSVar ();
152+ if (ssaVar == null ) {
153+ return null ;
154+ }
155+ for (RegisterArg use : ssaVar .getUseList ()) {
156+ InsnNode parent = use .getParentInsn ();
157+ if (parent instanceof FilledNewArrayNode ) {
158+ return (FilledNewArrayNode ) parent ;
159+ }
160+ }
161+ return null ;
162+ }
163+
164+ /** Returns the index of the arg in {@code insn} whose SSAVar matches {@code ssaVar}, or -1. */
165+ private static int argIndexOf (InsnNode insn , SSAVar ssaVar ) {
166+ for (int i = 0 ; i < insn .getArgsCount (); i ++) {
167+ InsnArg arg = insn .getArg (i );
168+ if (arg instanceof RegisterArg && ((RegisterArg ) arg ).getSVar () == ssaVar ) {
169+ return i ;
170+ }
171+ }
172+ return -1 ;
173+ }
174+
175+ /**
176+ * Builds a multi-line comment text for all decoded strings in a FilledNewArrayNode.
177+ * Each entry is formatted as "b64[N]: decoded" on its own line, sorted by index.
178+ * The renderer (appendMultiLineString) splits on \n and starts each continuation
179+ * line with "// ", producing one "// b64[N]: ..." line per decoded string.
180+ */
181+ private static String buildArrayComment (TreeMap <Integer , String > decodings ) {
182+ StringBuilder sb = new StringBuilder ();
183+ boolean first = true ;
184+ for (Map .Entry <Integer , String > e : decodings .entrySet ()) {
185+ if (!first ) {
186+ sb .append ('\n' );
88187 }
188+ sb .append ("b64[" ).append (e .getKey ()).append ("]: " ).append (e .getValue ());
189+ first = false ;
89190 }
191+ return sb .toString ();
90192 }
91193
92194 /**
0 commit comments