|
@@ -0,0 +1,224 @@
|
|
1
|
+---
|
|
2
|
+date: 2019-10-21
|
|
3
|
+title: Obfuscating Kotlin code with ProGuard
|
|
4
|
+url: /2019/10/21/obfuscating-kotlin-proguard/
|
|
5
|
+image: /res/images/obfuscation/obfuscated.png
|
|
6
|
+description: In which Kotlin tries to be helpful, and we smite it
|
|
7
|
+area: Android
|
|
8
|
+---
|
|
9
|
+
|
|
10
|
+Obfuscating code is the process of modifying source code or build output in
|
|
11
|
+order to make it harder for humans to understand. It's often employed as a
|
|
12
|
+tactic to deter reverse engineering of commercial applications or libraries
|
|
13
|
+when you have no choice but to ship binaries or byte code. For Android apps,
|
|
14
|
+[ProGuard](https://www.guardsquare.com/en/products/proguard) is part of the
|
|
15
|
+default toolchain and obfuscation is usually only a config switch away.
|
|
16
|
+
|
|
17
|
+I was recently working on an Android library written in Kotlin that my client
|
|
18
|
+wanted obfuscated to try and protect some of their trade secrets that were
|
|
19
|
+included. Not a problem, I thought: it's just a few lines of ProGuard config
|
|
20
|
+and we're away. Four hours and lots of hair pulling later I finally got it
|
|
21
|
+working...
|
|
22
|
+
|
|
23
|
+<!--more-->
|
|
24
|
+
|
|
25
|
+## If at first you don't succeed...
|
|
26
|
+
|
|
27
|
+At first it seemed like ProGuard was refusing to obfuscate any class with a
|
|
28
|
+`keep` rule. With a simple test class:
|
|
29
|
+
|
|
30
|
+```kotlin
|
|
31
|
+class Test {
|
|
32
|
+
|
|
33
|
+ private val secret = 123
|
|
34
|
+ var name = "Chris"
|
|
35
|
+
|
|
36
|
+ fun greet() {
|
|
37
|
+ println("Hi $name! Enter a number: ")
|
|
38
|
+ readLine()?.let { guess(it.toInt()) }
|
|
39
|
+ }
|
|
40
|
+
|
|
41
|
+ private fun guess(attempt: Int) = println(
|
|
42
|
+ if (attempt == secret) {
|
|
43
|
+ "Correct!"
|
|
44
|
+ } else {
|
|
45
|
+ "Nope!"
|
|
46
|
+ }
|
|
47
|
+ )
|
|
48
|
+
|
|
49
|
+}
|
|
50
|
+```
|
|
51
|
+
|
|
52
|
+And a ProGuard rule of:
|
|
53
|
+
|
|
54
|
+```proguard
|
|
55
|
+-keep public class Test {
|
|
56
|
+ public void greet();
|
|
57
|
+}
|
|
58
|
+```
|
|
59
|
+
|
|
60
|
+I expected that the `Test` class and the `greet` method would remain, but both
|
|
61
|
+fields and the `guess` method would be obfuscated. When I built the project
|
|
62
|
+and opened the class from Android Studio's APK inspector I was disappointed:
|
|
63
|
+
|
|
64
|
+```kotlin
|
|
65
|
+public final class Test public constructor() {
|
|
66
|
+ public final var name: kotlin.String /* compiled code */
|
|
67
|
+
|
|
68
|
+ private final val secret: kotlin.Int /* compiled code */
|
|
69
|
+
|
|
70
|
+ public final fun greet(): kotlin.Unit { /* compiled code */ }
|
|
71
|
+
|
|
72
|
+ private final fun guess(attempt: kotlin.Int): kotlin.Unit { /* compiled code */ }
|
|
73
|
+}
|
|
74
|
+```
|
|
75
|
+
|
|
76
|
+Having your secret sauce in a field labelled "secret" isn't exactly the level of
|
|
77
|
+obfuscation I was hoping for. ProGuard has lots of knobs that you can twist to
|
|
78
|
+affect what it keeps and what it renames, but all the incantations of `-keep`,
|
|
79
|
+`-keepmembernames`, `-allowobfuscation`, and so on, that I could come up with
|
|
80
|
+either resulted in the class completely vanishing (because it wasn't kept) or
|
|
81
|
+showing up with all its symbols intact.
|
|
82
|
+
|
|
83
|
+There are lots of useful Stack Overflow posts describing how to obfuscate
|
|
84
|
+a single method, or keep a single method and obfuscate the rest, but nothing
|
|
85
|
+I tried seemed to make a difference. I'm not the biggest fan of ProGuard, but
|
|
86
|
+I've used it enough before to know that it's not usually that hard to make
|
|
87
|
+it submit to your demands. Obviously something else was going on.
|
|
88
|
+
|
|
89
|
+## ... Maybe you're solving the wrong problem
|
|
90
|
+
|
|
91
|
+My next thought was that perhaps Android Studio was doing something clever
|
|
92
|
+like reading the ProGuard mapping file and automatically deobfuscating the
|
|
93
|
+output for me. Looking at the mapping file it seems that ProGuard has
|
|
94
|
+indeed decided to rename some things:
|
|
95
|
+
|
|
96
|
+```
|
|
97
|
+Test -> Test:
|
|
98
|
+ int secret -> a
|
|
99
|
+ java.lang.String name -> b
|
|
100
|
+ void greet() -> greet
|
|
101
|
+ void <init>() -> <init>
|
|
102
|
+```
|
|
103
|
+
|
|
104
|
+The obvious solution is to look at the class file in something less smart
|
|
105
|
+than Android Studio. A couple of unzips later and I could do a quick test
|
|
106
|
+to see if the original names were still present:
|
|
107
|
+
|
|
108
|
+```console
|
|
109
|
+$ strings Test.class | grep secret
|
|
110
|
+secret
|
|
111
|
+```
|
|
112
|
+
|
|
113
|
+The problem is evidently not with Android Studio, as `secret` shouldn't end
|
|
114
|
+up in the class file at all: it should have been entirely replaced with `a` like
|
|
115
|
+the mapping file says. The output from `javap -p` doesn't show any hint of the
|
|
116
|
+original names, however:
|
|
117
|
+
|
|
118
|
+```console
|
|
119
|
+$ javap -p Test
|
|
120
|
+public final class Test {
|
|
121
|
+ private final int a;
|
|
122
|
+ private java.lang.String b;
|
|
123
|
+ public final void greet();
|
|
124
|
+ public Test();
|
|
125
|
+}
|
|
126
|
+```
|
|
127
|
+
|
|
128
|
+But, given the names show up in `strings`, they must be kicking around
|
|
129
|
+somewhere. None of the various outputs from `javap` helped until I hit
|
|
130
|
+`-verbose`. Right at the end of the class is:
|
|
131
|
+
|
|
132
|
+```
|
|
133
|
+RuntimeVisibleAnnotations:
|
|
134
|
+ 0: #58(#84=[I#2,I#2,I#4],#69=[I#2,I#1,I#3],#81=I#2,#70=[s#40],#71=[s#55,s#39,s#43,s#85,s#39,s#72,s#42,s#91,s#47,s#90,s#39,s#73,s#39,s#74,s#67,s#83])
|
|
135
|
+ kotlin.Metadata(
|
|
136
|
+ mv=[1,1,15]
|
|
137
|
+ bv=[1,0,3]
|
|
138
|
+ k=1
|
|
139
|
+ d1=["\u0000\"\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0005\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u000b\u001a\u00020\fJ\u0010\u0010\r\u001a\u00020\f2\u0006\u0010\u000e\u001a\u00020\nH\u0002R\u001a\u0010\u0003\u001a\u00020\u0004X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0005\u0010\u0006\"\u0004\b\u0007\u0010\bR\u000e\u0010\t\u001a\u00020\nX\u0082D¢\u0006\u0002\n\u0000¨\u0006\u000f"]
|
|
140
|
+ d2=["LTest;","","()V","name","","getName","()Ljava/lang/String;","setName","(Ljava/lang/String;)V","secret","","greet","","guess","attempt","lib_release"]
|
|
141
|
+ )
|
|
142
|
+```
|
|
143
|
+
|
|
144
|
+There's a Kotlin *annotation* containing all of the symbols we were trying to
|
|
145
|
+obfuscate away! Kotlin apparently uses this annotation for reflection and for
|
|
146
|
+keeping track of various language features that don't have a direct mapping in
|
|
147
|
+Java bytecode (such as members with `internal` access). Sure enough, switching
|
|
148
|
+back to Android Studio and making it "decompile" the Kotlin code into Java
|
|
149
|
+shows the annotation:
|
|
150
|
+
|
|
151
|
+```java
|
|
152
|
+@Metadata(
|
|
153
|
+ mv = {1, 1, 15},
|
|
154
|
+ bv = {1, 0, 3},
|
|
155
|
+ k = 1,
|
|
156
|
+ d1 = {"\u0000\"\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0005\n\u0002\u0010\b\n\u0000\n\u0002\u0010\u0002\n\u0002\b\u0003\u0018\u00002\u00020\u0001B\u0005¢\u0006\u0002\u0010\u0002J\u0006\u0010\u000b\u001a\u00020\fJ\u0010\u0010\r\u001a\u00020\f2\u0006\u0010\u000e\u001a\u00020\nH\u0002R\u001a\u0010\u0003\u001a\u00020\u0004X\u0086\u000e¢\u0006\u000e\n\u0000\u001a\u0004\b\u0005\u0010\u0006\"\u0004\b\u0007\u0010\bR\u000e\u0010\t\u001a\u00020\nX\u0082D¢\u0006\u0002\n\u0000¨\u0006\u000f"},
|
|
157
|
+ d2 = {"LTest;", "", "()V", "name", "", "getName", "()Ljava/lang/String;", "setName", "(Ljava/lang/String;)V", "secret", "", "greet", "", "guess", "attempt", "lib_release"}
|
|
158
|
+)
|
|
159
|
+public final class Test {
|
|
160
|
+```
|
|
161
|
+
|
|
162
|
+## Solving the right problem
|
|
163
|
+
|
|
164
|
+Now I had an idea of what was happening I was able to find a couple of other
|
|
165
|
+reports of people having the same issue. There's an open
|
|
166
|
+[feature request for ProGuard to support Kotlin's metadata annotation](https://sourceforge.net/p/proguard/feature-requests/182/)
|
|
167
|
+but it's not yet supported.
|
|
168
|
+
|
|
169
|
+As this was a library with a fairly straight forward interface I reasoned I
|
|
170
|
+could probably get ProGuard to strip out the metadata annotation. If I stopped
|
|
171
|
+Kotlin reflection working in the process then that would actually be a small
|
|
172
|
+bonus. Unfortunately other people with the same idea had reported back that
|
|
173
|
+they were unsuccessful: even when not using the default ProGuard config,
|
|
174
|
+somehow the annotations are kept.
|
|
175
|
+
|
|
176
|
+Adding a `-printconfiguration` instruction to my configuration lets me see
|
|
177
|
+the full configuration being passed to ProGuard, and the reason for keeping
|
|
178
|
+quickly becomes obvious:
|
|
179
|
+
|
|
180
|
+```proguard
|
|
181
|
+-keepattributes *Annotation*,*Annotation*
|
|
182
|
+```
|
|
183
|
+
|
|
184
|
+This appears to be added by the Android build plugin before it invokes
|
|
185
|
+ProGuard, and there's no obvious way to disable it. ProGuard doesn't offer
|
|
186
|
+a way to reverse this instruction, either, but fortunately the build plugin
|
|
187
|
+seems to concatenate all of the `-keepattribute` values together and puts our
|
|
188
|
+user-supplied ones first. Adding a negative filter:
|
|
189
|
+
|
|
190
|
+```proguard
|
|
191
|
+-keepattributes !*Annotation*
|
|
192
|
+```
|
|
193
|
+
|
|
194
|
+Results in the following in the printed configuration:
|
|
195
|
+
|
|
196
|
+```proguard
|
|
197
|
+-keepattributes !*Annotation*,*Annotation*,*Annotation*
|
|
198
|
+```
|
|
199
|
+
|
|
200
|
+The negative filter prevents any subsequent filters from matching. Recompiling
|
|
201
|
+and looking at the class file again looks a lot more sensible:
|
|
202
|
+
|
|
203
|
+```java
|
|
204
|
+import kotlin.io.ConsoleKt;
|
|
205
|
+
|
|
206
|
+public final class Test {
|
|
207
|
+ private final int a = 123;
|
|
208
|
+ private String b = "Chris";
|
|
209
|
+
|
|
210
|
+ public final void greet() {
|
|
211
|
+ String var1 = "Hi " + this.b + "! Enter a number: ";
|
|
212
|
+ System.out.println(var1);
|
|
213
|
+ String var10000 = ConsoleKt.readLine();
|
|
214
|
+ if (var10000 != null) {
|
|
215
|
+ var1 = var10000;
|
|
216
|
+ int var3 = Integer.parseInt(var1);
|
|
217
|
+ var1 = var3 == 123 ? "Correct!" : "Nope!";
|
|
218
|
+ System.out.println(var1);
|
|
219
|
+ }
|
|
220
|
+ }
|
|
221
|
+}
|
|
222
|
+```
|
|
223
|
+
|
|
224
|
+And that's the story of how I spent half a day making a one line change.
|