소스 검색

A Kotlin and Proguard horror story

master
Chris Smith 4 년 전
부모
커밋
2d81b75939
로그인 계정: Chris Smith <chris@chameth.com> GPG Key ID: 3A2D4BBDC4A3C9A9
2개의 변경된 파일224개의 추가작업 그리고 0개의 파일을 삭제
  1. 224
    0
      site/content/post/2019-10-21-obfuscating-kotlin-proguard.md
  2. BIN
      site/static/res/images/obfuscation/obfuscated.png

+ 224
- 0
site/content/post/2019-10-21-obfuscating-kotlin-proguard.md 파일 보기

@@ -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.

BIN
site/static/res/images/obfuscation/obfuscated.png 파일 보기


Loading…
취소
저장