Skip to content

Commit 7458b88

Browse files
committed
resolving various b64 decode edge cases
1 parent e591c4a commit 7458b88

5 files changed

Lines changed: 171 additions & 6 deletions

File tree

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

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515
import jadx.core.codegen.utils.CodeComment;
1616
import jadx.core.dex.attributes.AType;
1717
import jadx.core.dex.instructions.ConstStringNode;
18+
import jadx.core.dex.instructions.args.RegisterArg;
1819
import jadx.core.dex.nodes.BlockNode;
1920
import jadx.core.dex.nodes.ClassNode;
2021
import jadx.core.dex.nodes.FieldNode;
2122
import jadx.core.dex.nodes.InsnNode;
2223
import jadx.core.dex.nodes.MethodNode;
2324
import jadx.core.dex.nodes.RootNode;
25+
import jadx.core.dex.instructions.args.SSAVar;
2426

2527
public class B64DeobfuscatePass implements JadxDecompilePass {
2628

@@ -65,21 +67,50 @@ public void visit(MethodNode mth) {
6567
continue;
6668
}
6769
ConstStringNode csn = (ConstStringNode) insn;
68-
String decoded = B64Detector.detect(csn.getString(), options);
70+
String str = csn.getString();
71+
72+
// If the string is an arg to an explicit Base64.decode call, decode unconditionally.
73+
// The call itself is strong evidence of intent — skip false-positive heuristics.
74+
boolean forced = isUsedAsBase64DecodeArg(csn);
75+
String decoded = forced
76+
? B64Detector.decodeForced(str, options.getMaxCommentLength())
77+
: B64Detector.detect(str, options);
6978
if (decoded == null) {
7079
continue;
7180
}
7281
if (fieldConstants == null) {
7382
fieldConstants = collectConstantValueFieldStrings(mth.getParentClass());
7483
}
7584
// Skip strings that are static final CONSTANT_VALUE fields — B64FieldInitPass handles those
76-
if (!fieldConstants.contains(csn.getString())) {
85+
if (!fieldConstants.contains(str)) {
7786
csn.addAttr(AType.CODE_COMMENTS, new CodeComment("b64: " + decoded, CommentStyle.LINE));
7887
}
7988
}
8089
}
8190
}
8291

92+
/**
93+
* Returns true if any use of {@code csn}'s result register is a direct argument
94+
* to a Base64.decode-like method call.
95+
*/
96+
private static boolean isUsedAsBase64DecodeArg(ConstStringNode csn) {
97+
RegisterArg result = csn.getResult();
98+
if (result == null) {
99+
return false;
100+
}
101+
SSAVar ssaVar = result.getSVar();
102+
if (ssaVar == null) {
103+
return false;
104+
}
105+
for (RegisterArg use : ssaVar.getUseList()) {
106+
InsnNode parent = use.getParentInsn();
107+
if (B64FieldInitPass.isBase64DecodeCall(parent)) {
108+
return true;
109+
}
110+
}
111+
return false;
112+
}
113+
83114
/** Collects literal string values of all CONSTANT_VALUE fields in the class. */
84115
private static Set<String> collectConstantValueFieldStrings(ClassNode cls) {
85116
Set<String> result = null;

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,27 @@ private static boolean isAlphanumeric(String str, double minRatio) {
100100
return (double) alnumCount / str.length() >= minRatio;
101101
}
102102

103+
/**
104+
* Decodes {@code str} as Base64 without applying any false-positive heuristics.
105+
* Use when the string is explicitly passed to a Base64.decode call — the call itself
106+
* is strong evidence of intent. Returns null only if the string is not valid Base64.
107+
* Invalid UTF-8 bytes are replaced rather than causing a rejection.
108+
*/
109+
public static String decodeForced(String str, int maxCommentLength) {
110+
for (Base64.Decoder decoder : new Base64.Decoder[]{Base64.getDecoder(), Base64.getUrlDecoder()}) {
111+
try {
112+
byte[] bytes = decoder.decode(str);
113+
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
114+
.onMalformedInput(CodingErrorAction.REPLACE)
115+
.onUnmappableCharacter(CodingErrorAction.REPLACE);
116+
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
117+
return truncate(decoded, maxCommentLength);
118+
} catch (Exception ignored) {
119+
}
120+
}
121+
return null;
122+
}
123+
103124
static String truncate(String str, int maxLength) {
104125
String safe = str.replace("\n", "\\n").replace("\r", "\\r");
105126
if (maxLength > 0 && safe.length() > maxLength) {

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

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package jadx.plugins.stringdecoder;
22

3+
import java.util.Locale;
4+
35
import jadx.api.plugins.input.data.annotations.EncodedType;
46
import jadx.api.plugins.input.data.annotations.EncodedValue;
57
import jadx.api.plugins.input.data.attributes.JadxAttrType;
@@ -8,7 +10,12 @@
810
import jadx.api.plugins.pass.types.JadxDecompilePass;
911
import jadx.core.dex.attributes.AType;
1012
import jadx.core.dex.attributes.FieldInitInsnAttr;
13+
import jadx.core.dex.info.MethodInfo;
1114
import jadx.core.dex.instructions.ConstStringNode;
15+
import jadx.core.dex.instructions.InvokeNode;
16+
import jadx.core.dex.instructions.args.InsnArg;
17+
import jadx.core.dex.instructions.args.InsnWrapArg;
18+
import jadx.core.dex.instructions.args.RegisterArg;
1219
import jadx.core.dex.nodes.ClassNode;
1320
import jadx.core.dex.nodes.FieldNode;
1421
import jadx.core.dex.nodes.InsnNode;
@@ -49,7 +56,9 @@ private void processField(FieldNode field) {
4956
if (initAttr != null) {
5057
InsnNode initInsn = initAttr.getInsn();
5158
if (initInsn instanceof ConstStringNode) {
52-
annotateField(field, ((ConstStringNode) initInsn).getString());
59+
annotateField(field, ((ConstStringNode) initInsn).getString(), false);
60+
} else {
61+
findAndAnnotateInArgTree(field, initInsn, 0);
5362
}
5463
return;
5564
}
@@ -59,16 +68,75 @@ private void processField(FieldNode field) {
5968
if (constVal != null && constVal.getType() == EncodedType.ENCODED_STRING) {
6069
Object val = constVal.getValue();
6170
if (val instanceof String) {
62-
annotateField(field, (String) val);
71+
annotateField(field, (String) val, false);
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Recursively walks the instruction arg tree looking for a ConstStringNode.
78+
* When the ConstStringNode is a direct arg of a Base64.decode-like call, decodes
79+
* unconditionally (the call itself is strong evidence of intent).
80+
* Otherwise, applies normal false-positive checks via {@link B64Detector#detect}.
81+
*/
82+
private boolean findAndAnnotateInArgTree(FieldNode field, InsnNode insn, int depth) {
83+
if (insn == null || depth > 8) {
84+
return false;
85+
}
86+
boolean isBase64Call = isBase64DecodeCall(insn);
87+
for (int i = 0; i < insn.getArgsCount(); i++) {
88+
InsnArg arg = insn.getArg(i);
89+
InsnNode argInsn = resolveArgInsn(arg);
90+
if (argInsn == null) {
91+
continue;
6392
}
93+
if (argInsn instanceof ConstStringNode) {
94+
String str = ((ConstStringNode) argInsn).getString();
95+
if (annotateField(field, str, isBase64Call)) {
96+
return true;
97+
}
98+
} else if (findAndAnnotateInArgTree(field, argInsn, depth + 1)) {
99+
return true;
100+
}
101+
}
102+
return false;
103+
}
104+
105+
private static InsnNode resolveArgInsn(InsnArg arg) {
106+
if (arg instanceof InsnWrapArg) {
107+
return ((InsnWrapArg) arg).getWrapInsn();
108+
}
109+
if (arg instanceof RegisterArg) {
110+
return ((RegisterArg) arg).getAssignInsn();
64111
}
112+
return null;
65113
}
66114

67-
private void annotateField(FieldNode field, String str) {
68-
String decoded = B64Detector.detect(str, options);
115+
/** Returns true if {@code insn} looks like a call to a Base64 decode method. */
116+
static boolean isBase64DecodeCall(InsnNode insn) {
117+
if (!(insn instanceof InvokeNode)) {
118+
return false;
119+
}
120+
MethodInfo mth = ((InvokeNode) insn).getCallMth();
121+
String clsName = mth.getDeclClass().getFullName().toLowerCase(Locale.ROOT);
122+
String mthName = mth.getName().toLowerCase(Locale.ROOT);
123+
return clsName.contains("base64") && mthName.contains("decode");
124+
}
125+
126+
/**
127+
* Annotates the field if the string is valid Base64.
128+
* When {@code forced} is true, skips false-positive heuristics.
129+
* Returns true if a comment was added.
130+
*/
131+
private boolean annotateField(FieldNode field, String str, boolean forced) {
132+
String decoded = forced
133+
? B64Detector.decodeForced(str, options.getMaxCommentLength())
134+
: B64Detector.detect(str, options);
69135
if (decoded != null) {
70136
field.addCodeComment("b64: " + decoded);
137+
return true;
71138
}
139+
return false;
72140
}
73141

74142
@Override

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ public void minAlphanumericPercentOptionTest() throws Exception {
174174
assertThat(code).doesNotContain("b64:");
175175
}
176176

177+
@Test
178+
public void decodedFieldB64Test() throws Exception {
179+
// new String(Base64.decode("...", 0)) field init — arg tree walk should find the encoded string
180+
// and forced decode (bypassing heuristics) because it's an explicit Base64.decode call
181+
String code = decompileSmali("b64/decoded_field_b64.smali");
182+
System.out.println(code);
183+
assertThat(code).contains("b64: room://cloud.tencent.com/rtc");
184+
}
185+
177186
@Test
178187
public void skipIdentifiersFiltersIdentifierLikeTest() throws Exception {
179188
// "fillItem" looks like a Java identifier; with skipIdentifiers=true it must not be flagged
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.class public Ldecodedfield/HelloWorld;
2+
.super Ljava/lang/Object;
3+
4+
.field public static final ADDRESS:Ljava/lang/String;
5+
6+
.method public static constructor <clinit>()V
7+
.registers 3
8+
9+
new-instance v0, Ljava/lang/String;
10+
11+
const-string v1, "cm9vbTovL2Nsb3VkLnRlbmNlbnQuY29tL3J0Yw=="
12+
13+
const/4 v2, 0x0
14+
15+
invoke-static {v1, v2}, Landroid/util/Base64;->decode(Ljava/lang/String;I)[B
16+
17+
move-result-object v1
18+
19+
invoke-direct {v0, v1}, Ljava/lang/String;-><init>([B)V
20+
21+
sput-object v0, Ldecodedfield/HelloWorld;->ADDRESS:Ljava/lang/String;
22+
23+
return-void
24+
.end method
25+
26+
.method public static main([Ljava/lang/String;)V
27+
.registers 3
28+
29+
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
30+
31+
sget-object v1, Ldecodedfield/HelloWorld;->ADDRESS:Ljava/lang/String;
32+
33+
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
34+
35+
return-void
36+
.end method

0 commit comments

Comments
 (0)