Skip to content

Commit 3c563e6

Browse files
committed
initial commit of jadx-string-decoder
1 parent c89a1fd commit 3c563e6

9 files changed

Lines changed: 145 additions & 54 deletions

File tree

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,8 @@ idea/
3232
*.log
3333
*.cfg
3434
*.orig
35+
36+
37+
# local
38+
.claude/
39+
dev/

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11

2-
## JADX Example plugin
2+
## jadx-string-decoder
33

4-
Simple example of jadx plugin.
5-
Adds a jadx watermark comment to every generated java class
4+
A JADX plugin that detects likely Base64-encoded string constants during decompilation and annotates them with a decoded comment inline.
65

7-
Install using location id: `github:jadx-decompiler:jadx-example-plugin`
6+
Install using location id: `github:nklapste:jadx-string-decoder`
87

98
In jadx-cli:
109
```bash
11-
jadx plugins --install "github:jadx-decompiler:jadx-example-plugin"
10+
jadx plugins --install "github:nklapste:jadx-string-decoder"
1211
```

settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11

2-
rootProject.name = "jadx-example-plugin"
2+
rootProject.name = "jadx-string-decoder"

src/main/java/jadx/plugins/example/AddCommentPass.java

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/main/java/jadx/plugins/example/ExampleOptions.java renamed to src/main/java/jadx/plugins/example/B64DeobfuscateOptions.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
44

5-
public class ExampleOptions extends BasePluginOptionsBuilder {
5+
public class B64DeobfuscateOptions extends BasePluginOptionsBuilder {
66

77
private boolean enable;
88

99
@Override
1010
public void registerOptions() {
1111
boolOption(JadxExamplePlugin.PLUGIN_ID + ".enable")
12-
.description("enable comment")
12+
.description("Enable Base64 string detection and decoding")
1313
.defaultValue(true)
1414
.setter(v -> enable = v);
1515
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package jadx.plugins.example;
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.List;
10+
import java.util.regex.Pattern;
11+
12+
import jadx.api.data.CommentStyle;
13+
import jadx.api.plugins.pass.JadxPassInfo;
14+
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo;
15+
import jadx.api.plugins.pass.types.JadxDecompilePass;
16+
import jadx.core.codegen.utils.CodeComment;
17+
import jadx.core.dex.attributes.AType;
18+
import jadx.core.dex.instructions.ConstStringNode;
19+
import jadx.core.dex.nodes.BlockNode;
20+
import jadx.core.dex.nodes.ClassNode;
21+
import jadx.core.dex.nodes.InsnNode;
22+
import jadx.core.dex.nodes.MethodNode;
23+
import jadx.core.dex.nodes.RootNode;
24+
25+
public class B64DeobfuscatePass implements JadxDecompilePass {
26+
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+
33+
@Override
34+
public JadxPassInfo getInfo() {
35+
return new OrderedJadxPassInfo(
36+
"B64Deobfuscate",
37+
"Detect and decode likely Base64-encoded string constants")
38+
.after("SSATransform")
39+
.before("ConstInlineVisitor");
40+
}
41+
42+
@Override
43+
public void init(RootNode root) {
44+
}
45+
46+
@Override
47+
public boolean visit(ClassNode cls) {
48+
return true;
49+
}
50+
51+
@Override
52+
public void visit(MethodNode mth) {
53+
if (mth.isNoCode()) {
54+
return;
55+
}
56+
List<BlockNode> blocks = mth.getBasicBlocks();
57+
if (blocks == null) {
58+
return;
59+
}
60+
for (BlockNode block : blocks) {
61+
for (InsnNode insn : block.getInstructions()) {
62+
if (insn instanceof ConstStringNode) {
63+
processConstString((ConstStringNode) insn);
64+
}
65+
}
66+
}
67+
}
68+
69+
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);
78+
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) + "...";
122+
}
123+
return safe;
124+
}
125+
}

src/main/java/jadx/plugins/example/JadxExamplePlugin.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
import jadx.api.plugins.JadxPluginInfoBuilder;
77

88
public class JadxExamplePlugin implements JadxPlugin {
9-
public static final String PLUGIN_ID = "example-plugin";
9+
public static final String PLUGIN_ID = "b64-deobfuscate";
1010

11-
private final ExampleOptions options = new ExampleOptions();
11+
private final B64DeobfuscateOptions options = new B64DeobfuscateOptions();
1212

1313
@Override
1414
public JadxPluginInfo getPluginInfo() {
1515
return JadxPluginInfoBuilder.pluginId(PLUGIN_ID)
16-
.name("Jadx example plugin")
17-
.description("Add jadx watermark comment to every class")
18-
.homepage("https://github.com/jadx-decompiler/jadx-example-plugin")
16+
.name("String Decoder")
17+
.description("Detect likely Base64-encoded string constants and add decoded value as a comment")
18+
.homepage("https://github.com/nklapste/jadx-string-decoder")
1919
.requiredJadxVersion("1.5.1, r2333")
2020
.build();
2121
}
@@ -24,7 +24,7 @@ public JadxPluginInfo getPluginInfo() {
2424
public void init(JadxPluginContext context) {
2525
context.registerOptions(options);
2626
if (options.isEnable()) {
27-
context.addPass(new AddCommentPass());
27+
context.addPass(new B64DeobfuscatePass());
2828
}
2929
}
3030
}

src/test/java/jadx/plugins/example/JadxExamplePluginTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public void integrationTest() throws Exception {
2222
JavaClass cls = jadx.getClasses().get(0);
2323
String clsCode = cls.getCode();
2424
System.out.println(clsCode);
25-
assertThat(clsCode).contains("Class generated by jadx decompiler");
25+
assertThat(clsCode).contains("b64: Hello, World!");
2626
}
2727
}
2828

src/test/resources/samples/hello.smali

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
.method public static main([Ljava/lang/String;)V
55
.registers 2
66
sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
7-
const-string v0, "Hello, World"
7+
const-string v0, "SGVsbG8sIFdvcmxkIQ=="
88
invoke-virtual {p0, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
99
return-void
1010
.end method

0 commit comments

Comments
 (0)