Sytuacja kobiet w IT w 2024 roku
10.08.20204 min
Idan Atsmon

Idan AtsmonAndroid DeveloperironSource

Czy kod Kotlina jest naprawdę zaciemniony?

Poznaj specyfikę zaciemniania kodu w Kotlinie.

Czy kod Kotlina jest naprawdę zaciemniony?

Staram się co tydzień obejrzeć jedną prezentację z Google I/O lub Android Dev Summit. Zawsze uczę się czegoś nowego, nawet w tematach, które już nieźle znam i właśnie dlatego podtrzymuję ten zwyczaj od kilku lat.

Oglądając prezentację o interoperacyjności Javy i Kotlina, natknąłem się na coś o generowaniu i obfuskacji kodu Kotlina, co mnie zaskoczyło.

Każda poważna aplikacja powinna używać jakiejś formy mimimalizacji kodu i narzędzia zaciemniania kodu. Dla Androida typowymi narzędziami są ProGuard i od jakiegoś czasu R8. Te narzędzia są w pełni kompatybilne z Kotlinem bez dodawania specjalnych reguł (poza jakimiś niszowymi przypadkami, które pominę w tym poście). To oznacza, że możesz uruchomić ProGuard na swojej kotlinowej apce i wszystko powinno działać, jak należy.

Ale czy Twój kod w Kotlinie jest naprawdę zaciemniony?

W poniższym przykładzie pokażę, że w rzeczywistości kod nie jest do końca obsufkowany.

Oto przykładowa klasa z:

  • Publiczną funkcją z parametrami, które nie są nullami
  • Publiczną funkcję rozszerzającą
  • Publiczną zmienną lateinit.
// note: this code is for example purposes, i know its not the right why to do things :)
class SomeClass {
    lateinit var importantVar: String
    
    fun funcWithParams(importantString: String, importantList: List<Int>) {
    }

    fun String.importantExtensionFunc() {
    }
}


Przeanalizujmy zdekompilowany kod Javy, który wyprodukował kompilator Kotlina dla tej klasy:

// removed some of the irrelevant code for simplicity
public final class SomeClass {
   @NotNull public String importantVar;

   @NotNull
   public final String getImportantVar() {
      String var10000 = this.importantVar;
      if (var10000 == null) {
         Intrinsics.throwUninitializedPropertyAccessException("importantVar");
      }
      return var10000;
   }

   public final void funcWithParams(@NotNull String importantString, @NotNull List importantList) {
      Intrinsics.checkParameterIsNotNull(importantString, "importantString");
      Intrinsics.checkParameterIsNotNull(importantList, "importantList");
   }

   public final void importantExtensionFunc(@NotNull String $this$importantExtensionFunc) {
      Intrinsics.checkParameterIsNotNull($this$importantExtensionFunc, "$this$importantExtensionFunc");
   }
}


Zauważ, że wygenerowany kod odwołuje się do klasy zwanej Intrisics.

Tego rodzaju wywołania weryfikują różnego rodzaju warunki stawiane przez Kotlina, np. to, że parametry nie powinny być nullem albo powinny zostać zainicjalizowane przed dostępem do nich.

Teraz zbudujmy zoptymalizowany i zobfuskowany APK (używając ProGuard), zobaczmy, co jest w środku (używam do tego świetnego Bytecode Viewer) i przeanalizujmy kod jeszcze raz:

public final class a {
   public String a;

   public final String a() {
      String var1 = this.a;
      if (var1 == null) {
         b.b("importantVar");
      }
      return var1;
   }

   public final void a(String var1) {
      b.b(var1, "$this$importantExtensionFunc");
   }

   public final void a(String var1, List var2) {
      b.b(var1, "importantString");
      b.b(var2, "importantList");
   }

   public final void b() {
      String var1 = this.a;
      if (var1 == null) {
         b.b("importantVar");
      }
      this.a(var1);
   }
}


Widzisz tu problem? Nazwy zmiennych i metod rozszerzających nadal tu są!

Te nazwy mogą ułatwić zrozumienie naszej logiki biznesowej dla kogoś z zewnątrz. Te informacje mogą zostać użyte w niecny sposób. Nie jest to wielki problem z punktu widzenia bezpieczeństwa, niemniej jednak jest to pewnego rodzaju zagrożenie.

Rozwiązanie

Reguły ProGuard mogą być użyte do czegoś więcej niż tylko do zapobiegania zaciemniania i usuwania pewnych partii kodu. Mogą być również użyte, by usunąć kod.

Dodanie następującej reguły ProGuard do pliku proguard-rules.pro będzie skutkowało usunięciem wszystkich wymienionych wywołań klasy Intrisincs.

-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
  public static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String);
  public static void checkFieldIsNotNull(java.lang.Object, java.lang.String);
  public static void checkFieldIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
  public static void checkNotNull(java.lang.Object);
  public static void checkNotNull(java.lang.Object, java.lang.String);
  public static void checkNotNullExpressionValue(java.lang.Object, java.lang.String);
  public static void checkNotNullParameter(java.lang.Object, java.lang.String);
  public static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
  public static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String);
  public static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String);
  public static void throwUninitializedPropertyAccessException(java.lang.String);
}

-assumenosideeffects:

... w kroku optymalizacji ProGuard może usunąć wywołania do wymienionych metod, jeżeli stwierdzi, że zwracane wartości nie są używane. Ma to zastosowanie tylko, jeżeli wykonywany jest krok optymalizacji.

By włączyć optymalizacje w ProGuard, nazwa domyślnego pliku powinna zostać zmieniona z proguard-android.txt na proguard-android-optimize.txt w pliku build.gradle aplikacji.

Ten krok nie jest potrzebny przy użyciu R8.

Podsumowanie

O ile zauważy się problem, to rozwiązanie jest dość proste. Co więcej, gdy poszukałem informacji o tym problemie, to natknąłem się na kilka innych wpisów na blogach, ale nie rozumiem, czemu nie ma o tym nawet wzmianki w oficjalnej dokumentacji Kotlina.

Tego typu sprawdzenia negatywnie wpływają zarówno na bezpieczeństwo i na wydajność. Dlatego moim zdaniem powinny zostać wyłączone z przy tworzeniu aplikacji w trybie release.

Sugerowałbym pozostawienie wywołań do Intrisics w przypadku buildów debug, dzięki czemu szybko można by się dowiedzieć czy coś jest nie tak i usunąłbym je z buildów gotowych do dostarczenia na produkcję.



Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>