Skip to content

Commit 3f7c15c

Browse files
committed
more base64 edge cases being handled (multi-line/pem)
1 parent 7065dac commit 3f7c15c

5 files changed

Lines changed: 129 additions & 4 deletions

File tree

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
public final class B64Detector {
1212

13-
private static final Pattern BASE64_STANDARD = Pattern.compile("^[A-Za-z0-9+/]+=*$");
14-
private static final Pattern BASE64_URL_SAFE = Pattern.compile("^[A-Za-z0-9_-]+=*$");
13+
// Allow embedded \n/\r (PEM/MIME line-wrapped Base64); = padding and trailing newline are optional
14+
private static final Pattern BASE64_STANDARD = Pattern.compile("^[A-Za-z0-9+/\\n\\r]*=*[\\n\\r]*$");
15+
private static final Pattern BASE64_URL_SAFE = Pattern.compile("^[A-Za-z0-9_\\-\\n\\r]*=*[\\n\\r]*$");
1516
private static final Pattern IDENTIFIER_LIKE = Pattern.compile("^[A-Za-z][A-Za-z0-9]*$");
1617

1718
private B64Detector() {
@@ -46,7 +47,15 @@ private static String tryDecode(String str, B64DeobfuscateOptions options) {
4647
if (result != null) {
4748
return result;
4849
}
49-
return attemptDecode(Base64.getUrlDecoder(), str, options);
50+
result = attemptDecode(Base64.getUrlDecoder(), str, options);
51+
if (result != null) {
52+
return result;
53+
}
54+
// MIME decoder ignores embedded whitespace (PEM line-wrapped Base64)
55+
if (str.indexOf('\n') >= 0 || str.indexOf('\r') >= 0) {
56+
return attemptDecode(Base64.getMimeDecoder(), str, options);
57+
}
58+
return null;
5059
}
5160

5261
private static String attemptDecode(Base64.Decoder decoder, String str, B64DeobfuscateOptions options) {
@@ -107,7 +116,7 @@ private static boolean isAlphanumeric(String str, double minRatio) {
107116
* Invalid UTF-8 bytes are replaced rather than causing a rejection.
108117
*/
109118
public static String decodeForced(String str, int maxCommentLength) {
110-
for (Base64.Decoder decoder : new Base64.Decoder[]{Base64.getDecoder(), Base64.getUrlDecoder()}) {
119+
for (Base64.Decoder decoder : new Base64.Decoder[]{Base64.getDecoder(), Base64.getUrlDecoder(), Base64.getMimeDecoder()}) {
111120
try {
112121
byte[] bytes = decoder.decode(str);
113122
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@
1010
import jadx.api.plugins.pass.types.JadxDecompilePass;
1111
import jadx.core.dex.attributes.AType;
1212
import jadx.core.dex.attributes.FieldInitInsnAttr;
13+
import jadx.core.dex.info.FieldInfo;
1314
import jadx.core.dex.info.MethodInfo;
1415
import jadx.core.dex.instructions.ConstStringNode;
16+
import jadx.core.dex.instructions.IndexInsnNode;
17+
import jadx.core.dex.instructions.InsnType;
1518
import jadx.core.dex.instructions.InvokeNode;
1619
import jadx.core.dex.instructions.args.InsnArg;
1720
import jadx.core.dex.instructions.args.InsnWrapArg;
@@ -95,13 +98,70 @@ private boolean findAndAnnotateInArgTree(FieldNode field, InsnNode insn, int dep
9598
if (annotateField(field, str, isBase64Call)) {
9699
return true;
97100
}
101+
} else if (argInsn.getType() == InsnType.SGET) {
102+
// const-string may have been replaced by an SGET to a CONSTANT_VALUE field
103+
// (JADX's replaceConsts rewrites const-string to sget when a matching field exists)
104+
String str = resolveConstStringFromSget(field, (IndexInsnNode) argInsn);
105+
if (str != null) {
106+
if (annotateField(field, str, isBase64Call)) {
107+
// The string is a direct Base64.decode arg — also force-annotate the source
108+
// String field so it gets a comment even if it fails the alphanumeric check
109+
if (isBase64Call) {
110+
forceAnnotateSourceField(field, (IndexInsnNode) argInsn, str);
111+
}
112+
return true;
113+
}
114+
}
98115
} else if (findAndAnnotateInArgTree(field, argInsn, depth + 1)) {
99116
return true;
100117
}
101118
}
102119
return false;
103120
}
104121

122+
/**
123+
* When a CONSTANT_VALUE String field is the direct arg to a Base64.decode call,
124+
* force-annotates it so the source field gets a comment regardless of the alphanumeric
125+
* threshold (the explicit decode call is sufficient evidence of intent).
126+
*/
127+
private void forceAnnotateSourceField(FieldNode contextField, IndexInsnNode sgetInsn, String str) {
128+
FieldInfo refFieldInfo = (FieldInfo) sgetInsn.getIndex();
129+
ClassNode declCls = contextField.root().resolveClass(refFieldInfo.getDeclClass());
130+
if (declCls == null) {
131+
return;
132+
}
133+
FieldNode srcField = declCls.searchField(refFieldInfo);
134+
if (srcField == null || srcField.get(AType.FIELD_INIT_INSN) != null) {
135+
return;
136+
}
137+
String decoded = B64Detector.decodeForced(str, options.getMaxCommentLength());
138+
if (decoded != null) {
139+
srcField.addCodeComment("b64: " + decoded);
140+
}
141+
}
142+
143+
/** Follows an SGET to the referenced field's CONSTANT_VALUE string, or returns null. */
144+
private static String resolveConstStringFromSget(FieldNode contextField, IndexInsnNode sgetInsn) {
145+
FieldInfo refFieldInfo = (FieldInfo) sgetInsn.getIndex();
146+
RootNode root = contextField.root();
147+
ClassNode declCls = root.resolveClass(refFieldInfo.getDeclClass());
148+
if (declCls == null) {
149+
return null;
150+
}
151+
FieldNode refField = declCls.searchField(refFieldInfo);
152+
if (refField == null) {
153+
return null;
154+
}
155+
EncodedValue constVal = refField.get(JadxAttrType.CONSTANT_VALUE);
156+
if (constVal != null && constVal.getType() == EncodedType.ENCODED_STRING) {
157+
Object val = constVal.getValue();
158+
if (val instanceof String) {
159+
return (String) val;
160+
}
161+
}
162+
return null;
163+
}
164+
105165
private static InsnNode resolveArgInsn(InsnArg arg) {
106166
if (arg instanceof InsnWrapArg) {
107167
return ((InsnWrapArg) arg).getWrapInsn();

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,23 @@ public void byteArrayStringTest() throws Exception {
228228
assertThat(code).contains("bytes: \"Hello, World!\"");
229229
}
230230

231+
@Test
232+
public void multilineFieldB64Test() throws Exception {
233+
// PEM-style line-wrapped Base64 string passed to Base64.decode — MIME decoder required
234+
// Both the String CONSTANT_VALUE field and the byte[] result field should be annotated
235+
String code = decompileSmali("b64/multiline_field_b64.smali");
236+
System.out.println(code);
237+
assertThat(code).contains("b64: Hello, World!");
238+
}
239+
240+
@Test
241+
public void pemB64FieldTest() throws Exception {
242+
// PEM Base64 String CONSTANT_VALUE + byte[] field via Base64.decode — both should get b64: comment
243+
String code = decompileSmali("b64/pem_b64_field.smali");
244+
System.out.println(code);
245+
assertThat(code).contains("b64:");
246+
}
247+
231248
@Test
232249
public void byteArrayStringPassDisabledTest() throws Exception {
233250
// with enableByteArrayStringPass=false the bytes: comment must not appear
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.class public Lmultiline/HelloWorld;
2+
.super Ljava/lang/Object;
3+
4+
# Field whose value is a PEM-style line-wrapped Base64 string (64-char lines)
5+
# The value is "SGVsbG8s\nIFdvcmxk\nIQ==" which decodes to "Hello, World!"
6+
.field public static final ENCODED:Ljava/lang/String; = "SGVsbG8s\nIFdvcmxk\nIQ=="
7+
8+
# byte[] field initialised by Base64.decode(ENCODED, 0)
9+
.field public static final DECODED:[B
10+
11+
.method static constructor <clinit>()V
12+
.registers 3
13+
14+
const-string v0, "SGVsbG8s\nIFdvcmxk\nIQ=="
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+
sput-object v0, Lmultiline/HelloWorld;->DECODED:[B
19+
return-void
20+
.end method
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.class public Lpem/KeyStore;
2+
.super Ljava/lang/Object;
3+
4+
# CONSTANT_VALUE String fields (PEM-wrapped Base64)
5+
.field public static final GOOGLE_KEY:Ljava/lang/String; = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamgu\nD/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpA=="
6+
7+
# byte[] field initialised by Base64.decode(GOOGLE_KEY, 0)
8+
.field public static final GOOGLE_BYTES:[B
9+
10+
.method static constructor <clinit>()V
11+
.registers 2
12+
13+
const-string v0, "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamgu\nD/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpA=="
14+
const/4 v1, 0x0
15+
invoke-static {v0, v1}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
16+
move-result-object v0
17+
sput-object v0, Lpem/KeyStore;->GOOGLE_BYTES:[B
18+
return-void
19+
.end method

0 commit comments

Comments
 (0)