Skip to content

Commit 4ef400f

Browse files
committed
adding more false-positve b64 decoding issues
1 parent 4f3b750 commit 4ef400f

11 files changed

Lines changed: 632 additions & 1 deletion

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public class B64DeobfuscateOptions extends BasePluginOptionsBuilder {
1313
private int minAlphanumericPercent;
1414
private boolean requireValidLength;
1515
private boolean skipCamelCase;
16+
private boolean skipPascalCase;
17+
private boolean skipSnakeCase;
18+
private boolean skipDictionaryWords;
1619

1720
@Override
1821
public void registerOptions() {
@@ -46,6 +49,18 @@ public void registerOptions() {
4649
.description("Skip short strings that look like camelCase identifiers (e.g. getContext, fillItem)")
4750
.defaultValue(true)
4851
.setter(v -> skipCamelCase = v);
52+
boolOption(JadxStringDecoderPlugin.PLUGIN_ID + ".skipPascalCase")
53+
.description("Skip short strings that look like PascalCase type names (e.g. FileUtil, SecureRandom)")
54+
.defaultValue(true)
55+
.setter(v -> skipPascalCase = v);
56+
boolOption(JadxStringDecoderPlugin.PLUGIN_ID + ".skipSnakeCase")
57+
.description("Skip short strings that are all-uppercase (CURSOR, FOO_BAR) or all-lowercase (closed, foo_bar) — these are almost never intentional Base64")
58+
.defaultValue(true)
59+
.setter(v -> skipSnakeCase = v);
60+
boolOption(JadxStringDecoderPlugin.PLUGIN_ID + ".skipDictionaryWords")
61+
.description("Skip strings whose segments (split on camelCase/underscore boundaries) are all common programming/English words")
62+
.defaultValue(true)
63+
.setter(v -> skipDictionaryWords = v);
4964

5065
// ByteArrayStringPass options
5166
boolOption(JadxStringDecoderPlugin.PLUGIN_ID + ".enableByteArrayStringPass")
@@ -90,6 +105,18 @@ public boolean isSkipCamelCase() {
90105
return skipCamelCase;
91106
}
92107

108+
public boolean isSkipPascalCase() {
109+
return skipPascalCase;
110+
}
111+
112+
public boolean isSkipSnakeCase() {
113+
return skipSnakeCase;
114+
}
115+
116+
public boolean isSkipDictionaryWords() {
117+
return skipDictionaryWords;
118+
}
119+
93120
public int getMinDecodedLength() {
94121
return minDecodedLength;
95122
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@ public void visit(MethodNode mth) {
8181
String str = csn.getString();
8282

8383
// If the string is an arg to an explicit Base64.decode call, decode unconditionally.
84-
// The call itself is strong evidence of intent — skip false-positive heuristics.
84+
// The call itself is strong evidence of intent — skip false-positive heuristics,
85+
// but still respect the explicit blocklist.
8586
boolean forced = isUsedAsBase64DecodeArg(csn);
87+
if (forced && B64FalsePositives.contains(str)) {
88+
forced = false;
89+
}
8690
B64Result decoded = forced
8791
? B64Detector.decodeForced(str, options.getMaxCommentLength())
8892
: B64Detector.detect(str, options);

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ public final class B64Detector {
1515
private static final Pattern BASE64_URL_SAFE = Pattern.compile("^[A-Za-z0-9_\\-\\n\\r]*=*[\\n\\r]*$");
1616
// camelCase: starts with lowercase run, then one or more UppercaseLowercase+ groups, no digits/symbols
1717
private static final Pattern CAMEL_CASE = Pattern.compile("^[a-z]+([A-Z][a-z]+)+$");
18+
// PascalCase: starts with exactly one uppercase, then a lowercase run, then more UpperLower groups
19+
private static final Pattern PASCAL_CASE = Pattern.compile("^[A-Z][a-z]+([A-Z][a-z0-9]+)+$");
20+
// all-uppercase (letters, digits, hyphens, underscores) — covers CURSOR, UTF-16BE, FOO_BAR, SETTING
21+
private static final Pattern ALL_CAPS = Pattern.compile("^[A-Z][A-Z0-9\\-_]*$");
22+
// all-lowercase (letters, digits, underscores) — covers foo_bar, closed, callback, binding
23+
private static final Pattern ALL_LOWER = Pattern.compile("^[a-z][a-z0-9_]*$");
1824
private static final Base64.Decoder[] STANDARD_AND_URL = { Base64.getDecoder(), Base64.getUrlDecoder() };
1925
private static final Base64.Decoder[] ALL_DECODERS = { Base64.getDecoder(), Base64.getUrlDecoder(), Base64.getMimeDecoder() };
2026
private static final String[] STANDARD_AND_URL_TAGS = { "", "url" };
@@ -37,6 +43,16 @@ public static B64Result detect(String str, B64DeobfuscateOptions options) {
3743
if (options.isSkipCamelCase() && str.length() < 40 && CAMEL_CASE.matcher(str).matches()) {
3844
return null;
3945
}
46+
if (options.isSkipPascalCase() && str.length() < 40 && PASCAL_CASE.matcher(str).matches()) {
47+
return null;
48+
}
49+
if (options.isSkipSnakeCase() && str.length() < 40
50+
&& (ALL_CAPS.matcher(str).matches() || ALL_LOWER.matcher(str).matches())) {
51+
return null;
52+
}
53+
if (options.isSkipDictionaryWords() && str.length() < 40 && B64DictionaryFilter.isAllDictionaryWords(str)) {
54+
return null;
55+
}
4056
if (!BASE64_STANDARD.matcher(str).matches() && !BASE64_URL_SAFE.matcher(str).matches()) {
4157
return null;
4258
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package jadx.plugins.stringdecoder;
2+
3+
import java.io.BufferedReader;
4+
import java.io.InputStream;
5+
import java.io.InputStreamReader;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.Arrays;
8+
import java.util.HashSet;
9+
import java.util.List;
10+
import java.util.Set;
11+
import java.util.regex.Pattern;
12+
import java.util.stream.Collectors;
13+
14+
final class B64DictionaryFilter {
15+
16+
// Split on underscore, hyphen, plus, or camelCase boundaries
17+
private static final Pattern SPLIT_PATTERN = Pattern.compile(
18+
"[_+\\-]|(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])");
19+
20+
private static final Set<String> WORDS = loadWords();
21+
22+
private B64DictionaryFilter() {
23+
}
24+
25+
private static Set<String> loadWords() {
26+
Set<String> words = new HashSet<>();
27+
try (InputStream is = B64DictionaryFilter.class.getResourceAsStream(
28+
"/jadx/plugins/stringdecoder/words.txt")) {
29+
if (is == null) {
30+
return words;
31+
}
32+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
33+
String line;
34+
while ((line = reader.readLine()) != null) {
35+
String w = line.trim();
36+
if (!w.isEmpty()) {
37+
words.add(w);
38+
}
39+
}
40+
}
41+
} catch (Exception ignored) {
42+
}
43+
return words;
44+
}
45+
46+
/**
47+
* Returns true if {@code str} splits entirely into dictionary words, suggesting it is an
48+
* identifier rather than an intentionally encoded string (e.g. "getContext", "fill_item").
49+
* Segments shorter than 2 chars are ignored so single-letter separators don't block the check.
50+
*/
51+
static boolean isAllDictionaryWords(String str) {
52+
if (WORDS.isEmpty()) {
53+
return false;
54+
}
55+
List<String> segments = Arrays.stream(SPLIT_PATTERN.split(str))
56+
.map(String::toLowerCase)
57+
.filter(s -> s.length() >= 2)
58+
.collect(Collectors.toList());
59+
if (segments.isEmpty()) {
60+
return false;
61+
}
62+
return segments.stream().allMatch(WORDS::contains);
63+
}
64+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ private void forceAnnotateSourceField(FieldNode contextField, IndexInsnNode sget
136136
if (srcField == null || srcField.get(AType.FIELD_INIT_INSN) != null) {
137137
return;
138138
}
139+
if (B64FalsePositives.contains(str)) {
140+
return;
141+
}
139142
B64Result result = B64Detector.decodeForced(str, options.getMaxCommentLength());
140143
if (result != null) {
141144
srcField.addCodeComment(result.commentText());
@@ -191,6 +194,9 @@ static boolean isBase64DecodeCall(InsnNode insn) {
191194
* Returns true if a comment was added.
192195
*/
193196
private boolean annotateField(FieldNode field, String str, boolean forced) {
197+
if (B64FalsePositives.contains(str)) {
198+
return false;
199+
}
194200
B64Result result = forced
195201
? B64Detector.decodeForced(str, options.getMaxCommentLength())
196202
: B64Detector.detect(str, options);

0 commit comments

Comments
 (0)