Skip to content

Commit ae14694

Browse files
committed
updating to support commenting on static field definitions
1 parent a864301 commit ae14694

6 files changed

Lines changed: 152 additions & 65 deletions

File tree

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
package jadx.plugins.stringdecoder;
22

3-
import java.nio.ByteBuffer;
4-
import java.nio.charset.CharacterCodingException;
5-
import java.nio.charset.CharsetDecoder;
6-
import java.nio.charset.CodingErrorAction;
7-
import java.nio.charset.StandardCharsets;
8-
import java.util.Base64;
93
import java.util.List;
10-
import java.util.regex.Pattern;
114

125
import jadx.api.data.CommentStyle;
136
import jadx.api.plugins.pass.JadxPassInfo;
@@ -24,12 +17,6 @@
2417

2518
public class B64DeobfuscatePass implements JadxDecompilePass {
2619

27-
private static final Pattern BASE64_STANDARD = Pattern.compile("^[A-Za-z0-9+/]+=*$");
28-
private static final Pattern BASE64_URL_SAFE = Pattern.compile("^[A-Za-z0-9_-]+=*$");
29-
private static final int MIN_LENGTH = 8;
30-
private static final double MIN_PRINTABLE_RATIO = 0.75;
31-
private static final int MAX_COMMENT_LENGTH = 100;
32-
3320
@Override
3421
public JadxPassInfo getInfo() {
3522
return new OrderedJadxPassInfo(
@@ -67,59 +54,9 @@ public void visit(MethodNode mth) {
6754
}
6855

6956
private static void processConstString(ConstStringNode insn) {
70-
String str = insn.getString();
71-
if (str.length() < MIN_LENGTH) {
72-
return;
73-
}
74-
if (!BASE64_STANDARD.matcher(str).matches() && !BASE64_URL_SAFE.matcher(str).matches()) {
75-
return;
76-
}
77-
String decoded = tryDecode(str);
57+
String decoded = B64Detector.detect(insn.getString());
7858
if (decoded != null) {
79-
insn.addAttr(AType.CODE_COMMENTS, new CodeComment("b64: " + truncate(decoded), CommentStyle.LINE));
80-
}
81-
}
82-
83-
private static String tryDecode(String str) {
84-
String result = attemptDecode(Base64.getDecoder(), str);
85-
if (result != null) {
86-
return result;
87-
}
88-
return attemptDecode(Base64.getUrlDecoder(), str);
89-
}
90-
91-
private static String attemptDecode(Base64.Decoder decoder, String str) {
92-
try {
93-
byte[] bytes = decoder.decode(str);
94-
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
95-
.onMalformedInput(CodingErrorAction.REPORT)
96-
.onUnmappableCharacter(CodingErrorAction.REPORT);
97-
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
98-
if (isPrintable(decoded)) {
99-
return decoded;
100-
}
101-
} catch (CharacterCodingException ignored) {
102-
// decoded bytes are not valid UTF-8
103-
} catch (Exception ignored) {
104-
}
105-
return null;
106-
}
107-
108-
private static boolean isPrintable(String str) {
109-
if (str.isEmpty()) {
110-
return false;
111-
}
112-
long printableCount = str.chars()
113-
.filter(c -> c >= 32 && c <= 126)
114-
.count();
115-
return (double) printableCount / str.length() >= MIN_PRINTABLE_RATIO;
116-
}
117-
118-
private static String truncate(String str) {
119-
String safe = str.replace("\n", "\\n").replace("\r", "\\r");
120-
if (safe.length() > MAX_COMMENT_LENGTH) {
121-
return safe.substring(0, MAX_COMMENT_LENGTH) + "...";
59+
insn.addAttr(AType.CODE_COMMENTS, new CodeComment("b64: " + decoded, CommentStyle.LINE));
12260
}
123-
return safe;
12461
}
12562
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package jadx.plugins.stringdecoder;
2+
3+
import java.nio.ByteBuffer;
4+
import java.nio.charset.CharacterCodingException;
5+
import java.nio.charset.CharsetDecoder;
6+
import java.nio.charset.CodingErrorAction;
7+
import java.nio.charset.StandardCharsets;
8+
import java.util.Base64;
9+
import java.util.regex.Pattern;
10+
11+
public final class B64Detector {
12+
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_-]+=*$");
15+
private static final int MIN_LENGTH = 8;
16+
private static final double MIN_PRINTABLE_RATIO = 0.75;
17+
static final int MAX_COMMENT_LENGTH = 100;
18+
19+
private B64Detector() {
20+
}
21+
22+
/** Returns the decoded+truncated string if {@code str} looks like Base64, otherwise null. */
23+
public static String detect(String str) {
24+
if (str.length() < MIN_LENGTH) {
25+
return null;
26+
}
27+
if (!BASE64_STANDARD.matcher(str).matches() && !BASE64_URL_SAFE.matcher(str).matches()) {
28+
return null;
29+
}
30+
String decoded = tryDecode(str);
31+
if (decoded == null) {
32+
return null;
33+
}
34+
return truncate(decoded);
35+
}
36+
37+
private static String tryDecode(String str) {
38+
String result = attemptDecode(Base64.getDecoder(), str);
39+
if (result != null) {
40+
return result;
41+
}
42+
return attemptDecode(Base64.getUrlDecoder(), str);
43+
}
44+
45+
private static String attemptDecode(Base64.Decoder decoder, String str) {
46+
try {
47+
byte[] bytes = decoder.decode(str);
48+
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
49+
.onMalformedInput(CodingErrorAction.REPORT)
50+
.onUnmappableCharacter(CodingErrorAction.REPORT);
51+
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
52+
if (isPrintable(decoded)) {
53+
return decoded;
54+
}
55+
} catch (CharacterCodingException ignored) {
56+
// decoded bytes are not valid UTF-8
57+
} catch (Exception ignored) {
58+
}
59+
return null;
60+
}
61+
62+
private static boolean isPrintable(String str) {
63+
if (str.isEmpty()) {
64+
return false;
65+
}
66+
long printableCount = str.chars()
67+
.filter(c -> c >= 32 && c <= 126)
68+
.count();
69+
return (double) printableCount / str.length() >= MIN_PRINTABLE_RATIO;
70+
}
71+
72+
static String truncate(String str) {
73+
String safe = str.replace("\n", "\\n").replace("\r", "\\r");
74+
if (safe.length() > MAX_COMMENT_LENGTH) {
75+
return safe.substring(0, MAX_COMMENT_LENGTH) + "...";
76+
}
77+
return safe;
78+
}
79+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package jadx.plugins.stringdecoder;
2+
3+
import jadx.api.plugins.pass.JadxPassInfo;
4+
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo;
5+
import jadx.api.plugins.pass.types.JadxDecompilePass;
6+
import jadx.core.dex.attributes.AType;
7+
import jadx.core.dex.attributes.FieldInitInsnAttr;
8+
import jadx.core.dex.instructions.ConstStringNode;
9+
import jadx.core.dex.nodes.ClassNode;
10+
import jadx.core.dex.nodes.FieldNode;
11+
import jadx.core.dex.nodes.InsnNode;
12+
import jadx.core.dex.nodes.MethodNode;
13+
import jadx.core.dex.nodes.RootNode;
14+
15+
public class B64FieldInitPass implements JadxDecompilePass {
16+
17+
@Override
18+
public JadxPassInfo getInfo() {
19+
return new OrderedJadxPassInfo(
20+
"B64FieldInitDeobfuscate",
21+
"Detect and decode likely Base64-encoded static field initializers")
22+
.after("ExtractFieldInit");
23+
}
24+
25+
@Override
26+
public void init(RootNode root) {
27+
}
28+
29+
@Override
30+
public boolean visit(ClassNode cls) {
31+
for (FieldNode field : cls.getFields()) {
32+
FieldInitInsnAttr initAttr = field.get(AType.FIELD_INIT_INSN);
33+
if (initAttr == null) {
34+
continue;
35+
}
36+
InsnNode initInsn = initAttr.getInsn();
37+
if (!(initInsn instanceof ConstStringNode)) {
38+
continue;
39+
}
40+
String decoded = B64Detector.detect(((ConstStringNode) initInsn).getString());
41+
if (decoded != null) {
42+
field.addCodeComment("b64: " + decoded);
43+
}
44+
}
45+
return false;
46+
}
47+
48+
@Override
49+
public void visit(MethodNode mth) {
50+
}
51+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public void init(JadxPluginContext context) {
2525
context.registerOptions(options);
2626
if (options.isEnable()) {
2727
context.addPass(new B64DeobfuscatePass());
28+
context.addPass(new B64FieldInitPass());
2829
}
2930
}
3031
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,14 @@ public void multilineDecodedStringTest() throws Exception {
7676
assertThat(code).contains("b64: Line 1\\nLine 2\\nLine 3");
7777
}
7878

79+
@Test
80+
public void staticFieldB64Test() throws Exception {
81+
// Base64 string assigned to a static field via <clinit>; comment should appear on the field declaration
82+
String code = decompileSmali("b64/static_field_b64.smali");
83+
System.out.println(code);
84+
assertThat(code).contains("b64: hello");
85+
}
86+
7987
private String decompileSmali(String fileName) throws Exception {
8088
JadxArgs args = new JadxArgs();
8189
args.getInputFiles().add(getSampleFile(fileName));
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.class public LStaticFieldB64;
2+
.super Ljava/lang/Object;
3+
4+
.field public static myField:Ljava/lang/String;
5+
6+
.method static constructor <clinit>()V
7+
.registers 1
8+
const-string v0, "aGVsbG8="
9+
sput-object v0, LStaticFieldB64;->myField:Ljava/lang/String;
10+
return-void
11+
.end method

0 commit comments

Comments
 (0)