Skip to content

Commit 045874c

Browse files
committed
adding pass that decodes byte arrays that potentially can be decoded as strings
1 parent 6717c0c commit 045874c

4 files changed

Lines changed: 145 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
9+
import jadx.api.plugins.pass.JadxPassInfo;
10+
import jadx.api.plugins.pass.impl.OrderedJadxPassInfo;
11+
import jadx.api.plugins.pass.types.JadxDecompilePass;
12+
import jadx.core.dex.attributes.AType;
13+
import jadx.core.dex.attributes.FieldInitInsnAttr;
14+
import jadx.core.dex.instructions.FilledNewArrayNode;
15+
import jadx.core.dex.instructions.args.ArgType;
16+
import jadx.core.dex.instructions.args.InsnArg;
17+
import jadx.core.dex.instructions.args.LiteralArg;
18+
import jadx.core.dex.nodes.ClassNode;
19+
import jadx.core.dex.nodes.FieldNode;
20+
import jadx.core.dex.nodes.InsnNode;
21+
import jadx.core.dex.nodes.MethodNode;
22+
import jadx.core.dex.nodes.RootNode;
23+
24+
public class ByteArrayStringPass implements JadxDecompilePass {
25+
26+
private final B64DeobfuscateOptions options;
27+
28+
public ByteArrayStringPass(B64DeobfuscateOptions options) {
29+
this.options = options;
30+
}
31+
32+
@Override
33+
public JadxPassInfo getInfo() {
34+
return new OrderedJadxPassInfo(
35+
"ByteArrayString",
36+
"Detect byte array fields whose bytes form a printable string and add comment")
37+
.after("ExtractFieldInit");
38+
}
39+
40+
@Override
41+
public void init(RootNode root) {
42+
}
43+
44+
@Override
45+
public boolean visit(ClassNode cls) {
46+
for (FieldNode field : cls.getFields()) {
47+
processField(field);
48+
}
49+
return false;
50+
}
51+
52+
private void processField(FieldNode field) {
53+
FieldInitInsnAttr initAttr = field.get(AType.FIELD_INIT_INSN);
54+
if (initAttr == null) {
55+
return;
56+
}
57+
InsnNode insn = initAttr.getInsn();
58+
if (!(insn instanceof FilledNewArrayNode)) {
59+
return;
60+
}
61+
FilledNewArrayNode filledArr = (FilledNewArrayNode) insn;
62+
if (!ArgType.BYTE.equals(filledArr.getElemType())) {
63+
return;
64+
}
65+
byte[] bytes = extractLiteralBytes(filledArr);
66+
if (bytes == null || bytes.length < Math.max(1, options.getMinInputLength())) {
67+
return;
68+
}
69+
String decoded = tryDecodeAsString(bytes);
70+
if (decoded != null) {
71+
field.addCodeComment("bytes: \"" + B64Detector.truncate(decoded, options.getMaxCommentLength()) + "\"");
72+
}
73+
}
74+
75+
private static byte[] extractLiteralBytes(FilledNewArrayNode insn) {
76+
int count = insn.getArgsCount();
77+
byte[] bytes = new byte[count];
78+
for (int i = 0; i < count; i++) {
79+
InsnArg arg = insn.getArg(i);
80+
if (!(arg instanceof LiteralArg)) {
81+
// Non-literal arg (e.g. SGET to a named constant) — skip this array
82+
return null;
83+
}
84+
bytes[i] = (byte) ((LiteralArg) arg).getLiteral();
85+
}
86+
return bytes;
87+
}
88+
89+
private String tryDecodeAsString(byte[] bytes) {
90+
try {
91+
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()
92+
.onMalformedInput(CodingErrorAction.REPORT)
93+
.onUnmappableCharacter(CodingErrorAction.REPORT);
94+
String decoded = utf8.decode(ByteBuffer.wrap(bytes)).toString();
95+
if (decoded.isEmpty()) {
96+
return null;
97+
}
98+
long printableCount = decoded.chars()
99+
.filter(c -> (c >= 32 && c <= 126) || c == '\t' || c == '\n' || c == '\r')
100+
.count();
101+
if ((double) printableCount / decoded.length() < options.getMinPrintableRatio()) {
102+
return null;
103+
}
104+
int minDecoded = options.getMinDecodedLength();
105+
if (minDecoded > 0 && decoded.length() < minDecoded) {
106+
return null;
107+
}
108+
return decoded;
109+
} catch (CharacterCodingException e) {
110+
return null;
111+
}
112+
}
113+
114+
@Override
115+
public void visit(MethodNode mth) {
116+
}
117+
}

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

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

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ public void minDecodedLengthAllowsLongDecodeTest() throws Exception {
220220
assertThat(code).contains("b64: Hello, World!");
221221
}
222222

223+
@Test
224+
public void byteArrayStringTest() throws Exception {
225+
// byte[] field whose bytes are ASCII "Hello, World!" — plugin should add a bytes: comment
226+
String code = decompileSmali("bytes/byte_array_string.smali");
227+
System.out.println(code);
228+
assertThat(code).contains("bytes: \"Hello, World!\"");
229+
}
230+
223231
private String decompileSmali(String fileName) throws Exception {
224232
return decompileSmali(fileName, Map.of());
225233
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
.class public Lbytearr/HelloWorld;
2+
.super Ljava/lang/Object;
3+
4+
.field public static final GREETING:[B
5+
6+
.method static constructor <clinit>()V
7+
.registers 2
8+
9+
const/16 v0, 0xd
10+
new-array v0, v0, [B
11+
fill-array-data v0, :array_greeting
12+
sput-object v0, Lbytearr/HelloWorld;->GREETING:[B
13+
return-void
14+
15+
:array_greeting
16+
.array-data 1
17+
72 101 108 108 111 44 32 87 111 114 108 100 33
18+
.end array-data
19+
.end method

0 commit comments

Comments
 (0)