Skip to content

Commit 3ec7c95

Browse files
committed
annotate what type or b64 encoding was used in the comment
1 parent bbc0ad5 commit 3ec7c95

5 files changed

Lines changed: 64 additions & 39 deletions

File tree

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public void visit(MethodNode mth) {
7070
// Arrays where ≥1 string passed normal detect() — required anchor for contextual decoding
7171
Set<InsnNode> arrayAnchors = null;
7272
// All valid-B64+UTF-8 strings in arrays (superset of anchors; used for the final comment)
73-
Map<InsnNode, TreeMap<Integer, String>> arrayCandidates = null;
73+
Map<InsnNode, TreeMap<Integer, B64Result>> arrayCandidates = null;
7474

7575
for (BlockNode block : blocks) {
7676
for (InsnNode insn : block.getInstructions()) {
@@ -83,7 +83,7 @@ public void visit(MethodNode mth) {
8383
// If the string is an arg to an explicit Base64.decode call, decode unconditionally.
8484
// The call itself is strong evidence of intent — skip false-positive heuristics.
8585
boolean forced = isUsedAsBase64DecodeArg(csn);
86-
String decoded = forced
86+
B64Result decoded = forced
8787
? B64Detector.decodeForced(str, options.getMaxCommentLength())
8888
: B64Detector.detect(str, options);
8989

@@ -97,7 +97,7 @@ public void visit(MethodNode mth) {
9797
continue;
9898
}
9999
// Determine the best available decoded value for this slot
100-
String candidate = decoded != null ? decoded
100+
B64Result candidate = decoded != null ? decoded
101101
: B64Detector.decodeIfValid(str, options.getMaxCommentLength());
102102
if (candidate == null) {
103103
continue;
@@ -131,7 +131,7 @@ public void visit(MethodNode mth) {
131131
if (fieldConstants.contains(str)) {
132132
continue;
133133
}
134-
csn.addAttr(AType.CODE_COMMENTS, new CodeComment("b64: " + decoded, CommentStyle.LINE));
134+
csn.addAttr(AType.CODE_COMMENTS, new CodeComment(decoded.commentText(), CommentStyle.LINE));
135135
}
136136
}
137137

@@ -175,11 +175,11 @@ private static int argIndexOf(InsnNode insn, SSAVar ssaVar) {
175175
return -1;
176176
}
177177

178-
private static String buildArrayComment(TreeMap<Integer, String> decodings) {
178+
private static String buildArrayComment(TreeMap<Integer, B64Result> decodings) {
179179
StringBuilder sb = new StringBuilder();
180-
for (Map.Entry<Integer, String> e : decodings.entrySet()) {
180+
for (Map.Entry<Integer, B64Result> e : decodings.entrySet()) {
181181
sb.append('\n');
182-
sb.append("b64[").append(e.getKey()).append("]: ").append(e.getValue());
182+
sb.append(e.getValue().indexedCommentText(e.getKey()));
183183
}
184184
return sb.toString();
185185
}

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ public final class B64Detector {
1717
private static final Pattern CAMEL_CASE = Pattern.compile("^[a-z]+([A-Z][a-z]+)+$");
1818
private static final Base64.Decoder[] STANDARD_AND_URL = { Base64.getDecoder(), Base64.getUrlDecoder() };
1919
private static final Base64.Decoder[] ALL_DECODERS = { Base64.getDecoder(), Base64.getUrlDecoder(), Base64.getMimeDecoder() };
20+
private static final String[] STANDARD_AND_URL_TAGS = { "", "url" };
21+
private static final String[] ALL_DECODER_TAGS = { "", "url", "mime" };
2022

2123
private B64Detector() {
2224
}
2325

2426
/**
25-
* Returns the decoded+truncated string if {@code str} looks like intentional Base64, otherwise null.
27+
* Returns a {@link B64Result} if {@code str} looks like intentional Base64, otherwise null.
2628
* All detection thresholds are taken from {@code options}.
2729
*/
28-
public static String detect(String str, B64DeobfuscateOptions options) {
30+
public static B64Result detect(String str, B64DeobfuscateOptions options) {
2931
if (B64FalsePositives.contains(str)) {
3032
return null;
3133
}
@@ -41,30 +43,26 @@ public static String detect(String str, B64DeobfuscateOptions options) {
4143
if (!BASE64_STANDARD.matcher(str).matches() && !BASE64_URL_SAFE.matcher(str).matches()) {
4244
return null;
4345
}
44-
String decoded = tryDecode(str, options);
45-
if (decoded == null) {
46-
return null;
47-
}
48-
return truncate(decoded, options.getMaxCommentLength());
46+
return tryDecode(str, options);
4947
}
5048

51-
private static String tryDecode(String str, B64DeobfuscateOptions options) {
52-
String result = attemptDecode(Base64.getDecoder(), str, options);
49+
private static B64Result tryDecode(String str, B64DeobfuscateOptions options) {
50+
B64Result result = attemptDecode(Base64.getDecoder(), str, options, "");
5351
if (result != null) {
5452
return result;
5553
}
56-
result = attemptDecode(Base64.getUrlDecoder(), str, options);
54+
result = attemptDecode(Base64.getUrlDecoder(), str, options, "url");
5755
if (result != null) {
5856
return result;
5957
}
6058
// MIME decoder ignores embedded whitespace (PEM line-wrapped Base64)
6159
if (str.indexOf('\n') >= 0 || str.indexOf('\r') >= 0) {
62-
return attemptDecode(Base64.getMimeDecoder(), str, options);
60+
return attemptDecode(Base64.getMimeDecoder(), str, options, "mime");
6361
}
6462
return null;
6563
}
6664

67-
private static String attemptDecode(Base64.Decoder decoder, String str, B64DeobfuscateOptions options) {
65+
private static B64Result attemptDecode(Base64.Decoder decoder, String str, B64DeobfuscateOptions options, String tag) {
6866
try {
6967
byte[] bytes = decoder.decode(str);
7068
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
@@ -81,7 +79,7 @@ private static String attemptDecode(Base64.Decoder decoder, String str, B64Deobf
8179
if (minLen > 0 && decoded.length() < minLen) {
8280
return null;
8381
}
84-
return decoded;
82+
return new B64Result(truncate(decoded, options.getMaxCommentLength()), tag);
8583
} catch (CharacterCodingException ignored) {
8684
// decoded bytes are not valid UTF-8
8785
} catch (Exception ignored) {
@@ -116,25 +114,27 @@ private static boolean isAlphanumeric(String str, double minRatio) {
116114
}
117115

118116
/**
119-
* Returns the decoded string if {@code str} is valid Base64 that decodes to valid UTF-8,
117+
* Returns a {@link B64Result} if {@code str} is valid Base64 that decodes to valid UTF-8,
120118
* without applying heuristic filters (no length, printable-ratio, or alphanumeric-ratio checks).
121119
* Used for array-element candidates when at least one sibling passes normal detection.
122120
* Returns null if the charset check fails, Base64 decode throws, or the result is not valid UTF-8.
123121
*/
124-
public static String decodeIfValid(String str, int maxCommentLength) {
122+
public static B64Result decodeIfValid(String str, int maxCommentLength) {
125123
if (!BASE64_STANDARD.matcher(str).matches() && !BASE64_URL_SAFE.matcher(str).matches()) {
126124
return null;
127125
}
128126
Base64.Decoder[] decoders = (str.indexOf('\n') >= 0 || str.indexOf('\r') >= 0)
129127
? ALL_DECODERS : STANDARD_AND_URL;
130-
for (Base64.Decoder decoder : decoders) {
128+
String[] tags = (str.indexOf('\n') >= 0 || str.indexOf('\r') >= 0)
129+
? ALL_DECODER_TAGS : STANDARD_AND_URL_TAGS;
130+
for (int i = 0; i < decoders.length; i++) {
131131
try {
132-
byte[] bytes = decoder.decode(str);
132+
byte[] bytes = decoders[i].decode(str);
133133
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
134134
.onMalformedInput(CodingErrorAction.REPORT)
135135
.onUnmappableCharacter(CodingErrorAction.REPORT);
136136
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
137-
return truncate(decoded, maxCommentLength);
137+
return new B64Result(truncate(decoded, maxCommentLength), tags[i]);
138138
} catch (Exception ignored) {
139139
}
140140
}
@@ -147,15 +147,15 @@ public static String decodeIfValid(String str, int maxCommentLength) {
147147
* is strong evidence of intent. Returns null only if the string is not valid Base64.
148148
* Invalid UTF-8 bytes are replaced rather than causing a rejection.
149149
*/
150-
public static String decodeForced(String str, int maxCommentLength) {
151-
for (Base64.Decoder decoder : ALL_DECODERS) {
150+
public static B64Result decodeForced(String str, int maxCommentLength) {
151+
for (int i = 0; i < ALL_DECODERS.length; i++) {
152152
try {
153-
byte[] bytes = decoder.decode(str);
153+
byte[] bytes = ALL_DECODERS[i].decode(str);
154154
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
155155
.onMalformedInput(CodingErrorAction.REPLACE)
156156
.onUnmappableCharacter(CodingErrorAction.REPLACE);
157157
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
158-
return truncate(decoded, maxCommentLength);
158+
return new B64Result(truncate(decoded, maxCommentLength), ALL_DECODER_TAGS[i]);
159159
} catch (Exception ignored) {
160160
}
161161
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ private void forceAnnotateSourceField(FieldNode contextField, IndexInsnNode sget
136136
if (srcField == null || srcField.get(AType.FIELD_INIT_INSN) != null) {
137137
return;
138138
}
139-
String decoded = B64Detector.decodeForced(str, options.getMaxCommentLength());
140-
if (decoded != null) {
141-
srcField.addCodeComment("b64: " + decoded);
139+
B64Result result = B64Detector.decodeForced(str, options.getMaxCommentLength());
140+
if (result != null) {
141+
srcField.addCodeComment(result.commentText());
142142
}
143143
}
144144

@@ -191,11 +191,11 @@ static boolean isBase64DecodeCall(InsnNode insn) {
191191
* Returns true if a comment was added.
192192
*/
193193
private boolean annotateField(FieldNode field, String str, boolean forced) {
194-
String decoded = forced
194+
B64Result result = forced
195195
? B64Detector.decodeForced(str, options.getMaxCommentLength())
196196
: B64Detector.detect(str, options);
197-
if (decoded != null) {
198-
field.addCodeComment("b64: " + decoded);
197+
if (result != null) {
198+
field.addCodeComment(result.commentText());
199199
return true;
200200
}
201201
return false;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package jadx.plugins.stringdecoder;
2+
3+
final class B64Result {
4+
private final String decoded;
5+
private final String tag; // "" = standard, "url", "mime"
6+
7+
B64Result(String decoded, String tag) {
8+
this.decoded = decoded;
9+
this.tag = tag;
10+
}
11+
12+
String getDecoded() {
13+
return decoded;
14+
}
15+
16+
/** Returns e.g. "b64: hello", "b64(url): hello", "b64(mime): hello" */
17+
String commentText() {
18+
return tag.isEmpty() ? "b64: " + decoded : "b64(" + tag + "): " + decoded;
19+
}
20+
21+
/** Returns e.g. "b64[0]: hello", "b64(url)[0]: hello" */
22+
String indexedCommentText(int index) {
23+
return tag.isEmpty() ? "b64[" + index + "]: " + decoded : "b64(" + tag + ")[" + index + "]: " + decoded;
24+
}
25+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -215,15 +215,15 @@ public void multilineFieldB64Test() throws Exception {
215215
// Both the String CONSTANT_VALUE field and the byte[] result field should be annotated
216216
String code = decompileSmali("b64/multiline_field_b64.smali");
217217
System.out.println(code);
218-
assertThat(code).contains("b64: Hello, World!");
218+
assertThat(code).contains("b64(mime): Hello, World!");
219219
}
220220

221221
@Test
222222
public void pemB64FieldTest() throws Exception {
223-
// PEM Base64 String CONSTANT_VALUE + byte[] field via Base64.decode — both should get b64: comment
223+
// PEM Base64 String CONSTANT_VALUE + byte[] field via Base64.decode — both should get b64(mime): comment
224224
String code = decompileSmali("b64/pem_b64_field.smali");
225225
System.out.println(code);
226-
assertThat(code).contains("b64:");
226+
assertThat(code).contains("b64(mime):");
227227
}
228228

229229
@Test
@@ -272,7 +272,7 @@ public void urlSafeB64Test() throws Exception {
272272
// "SGVsbG9-" is URL-safe Base64 for "Hello~" — '-' maps to index 62 (standard '+')
273273
String code = decompileSmali("b64/urlsafe_b64.smali");
274274
System.out.println(code);
275-
assertThat(code).contains("b64: Hello~");
275+
assertThat(code).contains("b64(url): Hello~");
276276
}
277277

278278
@Test

0 commit comments

Comments
 (0)