Skip to content

Commit 57b9155

Browse files
committed
fix bytes: comments missing on inner class fields; add int[] support
1 parent d3018d7 commit 57b9155

10 files changed

Lines changed: 282 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ JadxStringDecoderPlugin.init()
2626
→ registers B64DeobfuscateOptions
2727
→ registers B64DeobfuscatePass (method-body Base64 string detection)
2828
→ registers B64FieldInitPass (field initializer Base64 detection)
29-
→ registers ByteArrayStringPass (byte[] field string detection)
29+
→ registers ByteArrayStringPass (byte[]/int[] field string detection)
3030
```
3131

3232
All three passes are skipped when `enable` is false.
@@ -49,7 +49,7 @@ Comments are added via `field.addCodeComment()` which renders on a separate line
4949

5050
**`ByteArrayStringPass`** — runs `.after("ExtractFieldInit")`
5151

52-
Looks for `byte[]` fields initialised with a `FilledNewArrayNode` of all literal bytes. If the bytes decode as valid UTF-8 with sufficient printable characters, adds a `bytes: "..."` comment on the field.
52+
Looks for `byte[]` and `int[]` fields initialised with a `FilledNewArrayNode` of all literal values. For `int[]`, any element outside `[0, 255]` (e.g. a `-1` sentinel in a decode table) causes the whole array to be skipped. The elements are then treated as bytes, decoded as UTF-8, and if the result is sufficiently printable a `bytes: "..."` comment is added on the field. `visit(ClassNode)` returns `true` so `DepthTraversal` recurses into inner classes.
5353

5454
### Detection logic (`B64Detector`)
5555

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ A JADX plugin that detects likely encoded string constants during decompilation
66

77
**Base64 string detection** — scans string constants in method bodies, and field initializers. When a string looks to be Base64 encoded, a `// b64: <decoded>` comment is added next to it.
88

9-
**Byte array string detection** — scans `byte[]` fields initialised with literal byte arrays. When the bytes decode as valid UTF-8 with sufficient printable characters, a `// bytes: "<string>"` comment is added.
9+
**Byte array string detection** — scans `byte[]` and `int[]` fields initialised with literal arrays. The values are treated as bytes and decoded as UTF-8; if the result has enough printable characters, a `// bytes: "<string>"` comment is added.
1010

1111
### Installation
1212

@@ -37,4 +37,7 @@ void example() {
3737

3838
// bytes: "Hello, World!"
3939
private static final byte[] MSG = {72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33};
40+
41+
// bytes: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
42+
private static final int[] BASE64_ENCODE_TABLE = {65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, ...};
4043
```

build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ java {
4242
targetCompatibility = JavaVersion.VERSION_11
4343
}
4444

45+
tasks.withType<JavaCompile> {
46+
options.encoding = "UTF-8"
47+
}
48+
4549
version = System.getenv("VERSION") ?: "dev"
4650

4751
tasks {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public boolean visit(ClassNode cls) {
5050
for (FieldNode field : cls.getFields()) {
5151
processField(field);
5252
}
53-
return false;
53+
return true;
5454
}
5555

5656
private void processField(FieldNode field) {

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

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public boolean visit(ClassNode cls) {
4646
for (FieldNode field : cls.getFields()) {
4747
processField(field);
4848
}
49-
return false;
49+
return true;
5050
}
5151

5252
private void processField(FieldNode field) {
@@ -59,10 +59,15 @@ private void processField(FieldNode field) {
5959
return;
6060
}
6161
FilledNewArrayNode filledArr = (FilledNewArrayNode) insn;
62-
if (!ArgType.BYTE.equals(filledArr.getElemType())) {
62+
ArgType elemType = filledArr.getElemType();
63+
byte[] bytes;
64+
if (ArgType.BYTE.equals(elemType)) {
65+
bytes = extractLiteralBytes(filledArr);
66+
} else if (ArgType.INT.equals(elemType)) {
67+
bytes = extractIntAsAsciiBytes(filledArr);
68+
} else {
6369
return;
6470
}
65-
byte[] bytes = extractLiteralBytes(filledArr);
6671
if (bytes == null || bytes.length == 0) {
6772
return;
6873
}
@@ -78,14 +83,30 @@ private static byte[] extractLiteralBytes(FilledNewArrayNode insn) {
7883
for (int i = 0; i < count; i++) {
7984
InsnArg arg = insn.getArg(i);
8085
if (!(arg instanceof LiteralArg)) {
81-
// Non-literal arg (e.g. SGET to a named constant) — skip this array
8286
return null;
8387
}
8488
bytes[i] = (byte) ((LiteralArg) arg).getLiteral();
8589
}
8690
return bytes;
8791
}
8892

93+
private static byte[] extractIntAsAsciiBytes(FilledNewArrayNode insn) {
94+
int count = insn.getArgsCount();
95+
byte[] bytes = new byte[count];
96+
for (int i = 0; i < count; i++) {
97+
InsnArg arg = insn.getArg(i);
98+
if (!(arg instanceof LiteralArg)) {
99+
return null;
100+
}
101+
long val = ((LiteralArg) arg).getLiteral();
102+
if (val < 0 || val > 255) {
103+
return null;
104+
}
105+
bytes[i] = (byte) val;
106+
}
107+
return bytes;
108+
}
109+
89110
private String tryDecodeAsString(byte[] bytes) {
90111
try {
91112
CharsetDecoder utf8 = StandardCharsets.UTF_8.newDecoder()

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,41 @@ public void byteArrayStringTest() throws Exception {
201201
assertThat(code).contains("bytes: \"Hello, World!\"");
202202
}
203203

204+
@Test
205+
public void intArrayUtf8Test() throws Exception {
206+
// int[] storing "café" as unsigned UTF-8 bytes {99, 97, 102, 195, 169}.
207+
// Values 195 and 169 exceed signed byte range; byte[] would need negative literals, int[] avoids that.
208+
String code = decompileSmali("bytes/int_array_utf8.smali");
209+
System.out.println(code);
210+
assertThat(code).contains("bytes: \"café\"");
211+
}
212+
213+
@Test
214+
public void twoByteArraysInClinitTest() throws Exception {
215+
// Two byte[] fields initialised sequentially in <clinit> via new-array + fill-array-data.
216+
// Mirrors the real-world Base64$Encoder pattern — both fields should get bytes: comments.
217+
String code = decompileSmali("bytes/two_byte_arrays.smali");
218+
System.out.println(code);
219+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"");
220+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\"");
221+
}
222+
223+
@Test
224+
public void byteArrayB64AlphabetTest() throws Exception {
225+
// byte[] alphabet field — standard Base64 (+/)
226+
String code = decompileSmali("bytes/b64_alphabet_byte_array.smali");
227+
System.out.println(code);
228+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"");
229+
}
230+
231+
@Test
232+
public void intArrayB64AlphabetTest() throws Exception {
233+
// int[] alphabet field — URL-safe Base64 (-_); verifies int[] is treated identically to byte[]
234+
String code = decompileSmali("bytes/b64_alphabet_byte_array.smali");
235+
System.out.println(code);
236+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\"");
237+
}
238+
204239
@Test
205240
public void multilineFieldB64Test() throws Exception {
206241
// PEM-style line-wrapped Base64 string passed to Base64.decode — MIME decoder required
@@ -373,6 +408,16 @@ public void multiB64InvokeChainTest() throws Exception {
373408
assertThat(code).contains("b64[1]: getPackageManager");
374409
}
375410

411+
@Test
412+
public void base64EncoderInnerClassTest() throws Exception {
413+
// Synthetic inner class with two byte[] fields (standard and URL-safe Base64 alphabets)
414+
// initialised sequentially in <clinit>. Verifies ByteArrayStringPass recurses into inner classes.
415+
String code = decompileSmali("bytes/base64_encoder_inner_class.smali");
416+
System.out.println(code);
417+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\"");
418+
assertThat(code).contains("bytes: \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\"");
419+
}
420+
376421
@Test
377422
public void byteArrayStringPassDisabledTest() throws Exception {
378423
// with enableByteArrayStringPass=false the bytes: comment must not appear
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
.class public Lbytes/B64Alphabets;
2+
.super Ljava/lang/Object;
3+
4+
# Standard Base64 encode alphabet as byte[] — ABCDE...+/
5+
.field public static final alphabet:[B
6+
7+
# URL-safe Base64 encode alphabet as int[] — ABCDE...-_
8+
# int[] is a common real-world pattern to avoid signed-byte confusion with high values.
9+
.field public static final alphabetUrlSafe:[I
10+
11+
.method static constructor <clinit>()V
12+
.registers 2
13+
14+
const/16 v0, 0x40
15+
new-array v0, v0, [B
16+
fill-array-data v0, :array_standard_bytes
17+
sput-object v0, Lbytes/B64Alphabets;->alphabet:[B
18+
19+
const/16 v0, 0x40
20+
new-array v0, v0, [I
21+
fill-array-data v0, :array_urlsafe_ints
22+
sput-object v0, Lbytes/B64Alphabets;->alphabetUrlSafe:[I
23+
24+
return-void
25+
26+
:array_standard_bytes
27+
.array-data 1
28+
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
29+
81 82 83 84 85 86 87 88 89 90 97 98 99 100 101 102
30+
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
31+
119 120 121 122 48 49 50 51 52 53 54 55 56 57 43 47
32+
.end array-data
33+
34+
:array_urlsafe_ints
35+
.array-data 4
36+
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
37+
81 82 83 84 85 86 87 88 89 90 97 98 99 100 101 102
38+
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
39+
119 120 121 122 48 49 50 51 52 53 54 55 56 57 45 95
40+
.end array-data
41+
42+
.end method
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Synthetic test fixture — NOT taken from any APK.
2+
# Reproduces the inner-class byte[] field pattern:
3+
# - static class inside an outer class (EnclosingClass + InnerClass annotations)
4+
# - two byte[] fields (standard and URL-safe Base64 alphabets) initialised in <clinit>
5+
# - a virtual method whose body is irrelevant to the test
6+
# Tests that ByteArrayStringPass recurses into inner classes (visit() returns true).
7+
.class Ltestdata/b64impl/Base64Impl$Encoder;
8+
.super Ltestdata/b64impl/Base64Impl$Coder;
9+
.source "Base64Impl.java"
10+
11+
# annotations
12+
.annotation system Ldalvik/annotation/EnclosingClass;
13+
value = Ltestdata/b64impl/Base64Impl;
14+
.end annotation
15+
16+
.annotation system Ldalvik/annotation/InnerClass;
17+
accessFlags = 0x8
18+
name = "Encoder"
19+
.end annotation
20+
21+
22+
# static fields
23+
.field private static final k:[B
24+
25+
.field private static final l:[B
26+
27+
28+
# instance fields
29+
.field public final useUrlSafe:Z
30+
31+
32+
# direct methods
33+
.method static constructor <clinit>()V
34+
.registers 2
35+
36+
const/16 v0, 0x40
37+
38+
new-array v1, v0, [B
39+
fill-array-data v1, :array_standard
40+
sput-object v1, Ltestdata/b64impl/Base64Impl$Encoder;->k:[B
41+
42+
new-array v0, v0, [B
43+
fill-array-data v0, :array_urlsafe
44+
sput-object v0, Ltestdata/b64impl/Base64Impl$Encoder;->l:[B
45+
46+
return-void
47+
48+
nop
49+
50+
:array_standard
51+
.array-data 1
52+
0x41t 0x42t 0x43t 0x44t 0x45t 0x46t 0x47t 0x48t
53+
0x49t 0x4at 0x4bt 0x4ct 0x4dt 0x4et 0x4ft 0x50t
54+
0x51t 0x52t 0x53t 0x54t 0x55t 0x56t 0x57t 0x58t
55+
0x59t 0x5at 0x61t 0x62t 0x63t 0x64t 0x65t 0x66t
56+
0x67t 0x68t 0x69t 0x6at 0x6bt 0x6ct 0x6dt 0x6et
57+
0x6ft 0x70t 0x71t 0x72t 0x73t 0x74t 0x75t 0x76t
58+
0x77t 0x78t 0x79t 0x7at 0x30t 0x31t 0x32t 0x33t
59+
0x34t 0x35t 0x36t 0x37t 0x38t 0x39t 0x2bt 0x2ft
60+
.end array-data
61+
62+
:array_urlsafe
63+
.array-data 1
64+
0x41t 0x42t 0x43t 0x44t 0x45t 0x46t 0x47t 0x48t
65+
0x49t 0x4at 0x4bt 0x4ct 0x4dt 0x4et 0x4ft 0x50t
66+
0x51t 0x52t 0x53t 0x54t 0x55t 0x56t 0x57t 0x58t
67+
0x59t 0x5at 0x61t 0x62t 0x63t 0x64t 0x65t 0x66t
68+
0x67t 0x68t 0x69t 0x6at 0x6bt 0x6ct 0x6dt 0x6et
69+
0x6ft 0x70t 0x71t 0x72t 0x73t 0x74t 0x75t 0x76t
70+
0x77t 0x78t 0x79t 0x7at 0x30t 0x31t 0x32t 0x33t
71+
0x34t 0x35t 0x36t 0x37t 0x38t 0x39t 0x2dt 0x5ft
72+
.end array-data
73+
.end method
74+
75+
.method public constructor <init>(Z)V
76+
.registers 2
77+
78+
invoke-direct {p0}, Ltestdata/b64impl/Base64Impl$Coder;-><init>()V
79+
iput-boolean p1, p0, Ltestdata/b64impl/Base64Impl$Encoder;->useUrlSafe:Z
80+
return-void
81+
.end method
82+
83+
84+
# virtual methods
85+
.method public encode([B)[B
86+
.registers 2
87+
88+
const/4 v0, 0x0
89+
return-object v0
90+
.end method
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.class public Lbytes/IntArrayUtf8;
2+
.super Ljava/lang/Object;
3+
4+
# int[] storing "café" as unsigned UTF-8 bytes — values 195 and 169 exceed signed byte range
5+
.field public static final MSG:[I
6+
7+
.method static constructor <clinit>()V
8+
.registers 2
9+
10+
const/4 v0, 0x5
11+
new-array v0, v0, [I
12+
fill-array-data v0, :array_cafe
13+
sput-object v0, Lbytes/IntArrayUtf8;->MSG:[I
14+
return-void
15+
16+
:array_cafe
17+
.array-data 4
18+
99 97 102 195 169
19+
.end array-data
20+
21+
.end method
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.class public Lbytes/TwoByteArrays;
2+
.super Ljava/lang/Object;
3+
4+
# Two byte[] encode-table fields initialized sequentially in <clinit>.
5+
# Mirrors the real-world pattern in Base64$Encoder (k and l fields).
6+
.field private static final k:[B
7+
8+
.field private static final l:[B
9+
10+
.method static constructor <clinit>()V
11+
.registers 2
12+
13+
const/16 v0, 0x40
14+
new-array v1, v0, [B
15+
fill-array-data v1, :array_standard
16+
sput-object v1, Lbytes/TwoByteArrays;->k:[B
17+
18+
new-array v0, v0, [B
19+
fill-array-data v0, :array_urlsafe
20+
sput-object v0, Lbytes/TwoByteArrays;->l:[B
21+
22+
return-void
23+
24+
:array_standard
25+
.array-data 1
26+
0x41t 0x42t 0x43t 0x44t 0x45t 0x46t 0x47t 0x48t
27+
0x49t 0x4at 0x4bt 0x4ct 0x4dt 0x4et 0x4ft 0x50t
28+
0x51t 0x52t 0x53t 0x54t 0x55t 0x56t 0x57t 0x58t
29+
0x59t 0x5at 0x61t 0x62t 0x63t 0x64t 0x65t 0x66t
30+
0x67t 0x68t 0x69t 0x6at 0x6bt 0x6ct 0x6dt 0x6et
31+
0x6ft 0x70t 0x71t 0x72t 0x73t 0x74t 0x75t 0x76t
32+
0x77t 0x78t 0x79t 0x7at 0x30t 0x31t 0x32t 0x33t
33+
0x34t 0x35t 0x36t 0x37t 0x38t 0x39t 0x2bt 0x2ft
34+
.end array-data
35+
36+
:array_urlsafe
37+
.array-data 1
38+
0x41t 0x42t 0x43t 0x44t 0x45t 0x46t 0x47t 0x48t
39+
0x49t 0x4at 0x4bt 0x4ct 0x4dt 0x4et 0x4ft 0x50t
40+
0x51t 0x52t 0x53t 0x54t 0x55t 0x56t 0x57t 0x58t
41+
0x59t 0x5at 0x61t 0x62t 0x63t 0x64t 0x65t 0x66t
42+
0x67t 0x68t 0x69t 0x6at 0x6bt 0x6ct 0x6dt 0x6et
43+
0x6ft 0x70t 0x71t 0x72t 0x73t 0x74t 0x75t 0x76t
44+
0x77t 0x78t 0x79t 0x7at 0x30t 0x31t 0x32t 0x33t
45+
0x34t 0x35t 0x36t 0x37t 0x38t 0x39t 0x2dt 0x5ft
46+
.end array-data
47+
48+
.end method

0 commit comments

Comments
 (0)