Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterと難読化

Kazuki Chigita
November 21, 2024
1

 Flutterと難読化

FlutterKaigi2024 2024/11/22 14:30 - 15:10

https://fortee.jp/flutterkaigi-2024/proposal/bfed4133-d675-4e57-af29-c6e5305cbd8a
https://2024.flutterkaigi.jp/session/05b3d592-16e4-4831-b6f4-44870c555dd1

セッション内容概要リリースしたアプリのクラッシュ事例を追うべく、Crashlyticsなどのサービスを眺めていると、身に覚えのないスタックトレースを見た経験はありませんか? Flutter では release buildの場合には、コードが難読化されバイナリに含まれます。
FlutterはAndroid・iOSをはじめとして多くのプラットフォームで動作させる事ができるフレームワークです。Flutterはどのようにして、多くのプラットフォームをサポートする難読化を実現しているのでしょうか?このセッションは、大きく2つの要素から構成されます。
1つ目は、難読化されたコードに直面したときのトラブルシューティングに焦点を当てたものです。例えば、難読化されたスタックトレースを読む方法や、flutter symbolize の仕組みやオプションについて紹介します。
2つ目は、Flutter の難読化そのものがどのようにして行われるかについて焦点を当てます。Flutterが難読化を行うステップと各プラットフォームでの実例を紹介しつつどのような難読化が行われるかについて紹介します。想定視聴者•難読化に関連したトラブルシューティングについて知りたい人•難読化そのものについて理解を深めたい人•Flutter 特有のマルチプラットフォームでの難読化について知りたい人

Kazuki Chigita

November 21, 2024
Tweet

Transcript

  1. 自己紹介 名前: Kazuki Chigita (@chigichan24) お仕事: Androidアプリ開発者 Flutter関連でのアウトプット: flutter_blue へ

    のcontribution、日本語での記事執筆など 最近の興味: Impeller https://www.slideshare.net/slideshow/flutterble/125792150 https://qiita.com/chigichan24/items/89cb686e880f0274ed1c
  2. アイスブレーク class A extends B { const A({super.c,}); @override X

    ut(Vx hk, E mb) { final l = Ml.t(hk);final wh = mb.pc(g); final q = mb.pc(vv).y?.rp((e) => e is Nf && wh.q.lt(e.id),) ?? [];return Et( mp: Wk(k: [Uu.tgf(lrc: Nb(l.tp),), Tq(xo: const Ci.zz(16), sr: So.oi(cn: [for (final x in q) Yz( ie: x as Nf, cz: () async => Wsx(bb: x.id).di(hk),) ,],),),],),);} }
  3. アイスブレーク class A extends B { const A({super.c,}); @override X

    ut(Vx hk, E mb) { final l = Ml.t(hk);final wh = mb.pc(g); final q = mb.pc(vv).y?.rp((e) => e is Nf && wh.q.lt(e.id),) ?? [];return Et( mp: Wk(k: [Uu.tgf(lrc: Nb(l.tp),), Tq(xo: const Ci.zz(16), sr: So.oi(cn: [for (final x in q) Yz( ie: x as Nf, cz: () async => Wsx(bb: x.id).di(hk),) ,],),),],),);} } Q. FlutterKaigiアプリのどの画面?
  4. アイスブレーク class A extends B { const A({super.c,}); @override X

    ut(Vx hk, E mb) { final l = Ml.t(hk);final wh = mb.pc(g); final q = mb.pc(vv).y?.rp((e) => e is Nf && wh.q.lt(e.id),) ?? [];return Et( mp: Wk(k: [Uu.tgf(lrc: Nb(l.tp),), Tq(xo: const Ci.zz(16), sr: So.oi(cn: [for (final x in q) Yz( ie: x as Nf, cz: () async => Wsx(bb: x.id).di(hk),) ,],),),],),);} } Q. FlutterKaigiアプリのどの画面?
  5. 1. 難読化とは • よくある誤解 ◦ 難読化とは暗号化である ▪ データやリソースが暗号化されるわけでない ▪ 例えばシークレットキーなどを平文で持ってしまっては

    いけない ◦ リバースエンジニアリングを完全に防ぐことができる ▪ クラス・変数名からのリバースエンジニアリングは できないが、ロジックはわかる
  6. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される 難読化前のコード class _MyHomePageState

    extends State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } }
  7. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _MyHomePageState extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } }
  8. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _MyHomePageState extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map
  9. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _MyHomePageState extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU
  10. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _MyHomePageState extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU
  11. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU
  12. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU
  13. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    State<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU State → Nq
  14. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    Nq<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU State → Nq
  15. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    Nq<MyHomePage> { int _counter = 0; void _incrementCounter() { setState( () { _counter++; }); } } Obfuscation map _MyHomePageState → _aU State → Nq …
  16. 1. 難読化とは • Flutterでの難読化 ◦ release buildでは基本的に難読化される class _aU extends

    Nq<ZT> { int _gic = 0; void _hic() { ISa( () { _gic++; }); } } Obfuscation map _MyHomePageState → _aU State → Nq MyHomePage → ZT _counter → _gic _incrementCounter → _hic setState → ISa
  17. 1. 難読化とは • Flutterでの難読化 ◦ flutter build コマンドで --obfuscate オプションを使う。

    ◦ flutter build ipa --help In a release build, this flag removes identifiers and replaces them with randomized values for the purposes of source code obfuscation. This flag must always be combined with "--split-debug-info" option, the mapping between the values and the original identifiers is stored in the symbol map created in the specified directory. For an app built with this flag, the "flutter symbolize" command with the right program symbol file is required to obtain a human readable stack trace.
  18. 1. 難読化とは • Flutterでの難読化 ◦ flutter build コマンドで --obfuscate オプションを使う。

    ◦ flutter build ipa --help In a release build, this flag removes identifiers and replaces them with randomized values for the purposes of source code obfuscation. This flag must always be combined with "--split-debug-info" option, the mapping between the values and the original identifiers is stored in the symbol map created in the specified directory. For an app built with this flag, the "flutter symbolize" command with the right program symbol file is required to obtain a human readable stack trace. 単体では使えない
  19. 1. 難読化とは • Flutterでの難読化 ◦ flutter build apk --split-debug-info =

    /path/to/symbol In a release build, this flag reduces application size by storing Dart program symbols in a separate file on the host rather than in the application. The value of the flag should be a directory where program symbol files can be stored for later use. These symbol files contain the information needed to symbolize Dart stack traces. For an app built with this flag, the "flutter symbolize" command with the right program symbolfile is required to obtain a human readable stack trace. This flag cannot be combined with "--analyze-size".
  20. 1. 難読化とは • Flutterでの難読化 ◦ flutter build apk --split-debug-info =

    /path/to/symbol In a release build, this flag reduces application size by storing Dart program symbols in a separate file on the host rather than in the application. The value of the flag should be a directory where program symbol files can be stored for later use. These symbol files contain the information needed to symbolize Dart stack traces. For an app built with this flag, the "flutter symbolize" command with the right program symbolfile is required to obtain a human readable stack trace. This flag cannot be combined with "--analyze-size".
  21. 1. 難読化とは • 難読化されたコードを戻すときは symbols file を用いる • flutter symbolize

    -i /path/to/log -d /path/to/symbols-file 難読化前のコード 難読化後のコード symbols file
  22. 1. 難読化とは • 難読化されたコードを戻すときは symbols file を用いる • flutter symbolize

    -i /path/to/log -d /path/to/symbols-file 難読化前のコード 難読化後のコード symbols file Production のスタックトレース解析には必須
  23. 1. 難読化とは • どういった難読化がされたかを確認することもできる。 難読化前のコード 難読化後のコード symbols file obfuscation-map ["_initialLifecycleStateAccessed@1506558

    9","_DCa@15065589","untransformedEndP osition","gHb","_color@92443363","_DYa @92443363","crossAxisAlignment","PWa", "_lastPosition@196518508","_xHb@19651 8508","__ConstMap&_HashVMImmutableBa se&MapMixin&_HashBase&_OperatorEquals AndCanonicalHashCode&_LinkedHashMapMi xin&_UnmodifiableMapMixin&_ImmutableLi nkedHashMapMixin","_Lc", … ]
  24. 1. 難読化とは • どういった難読化がされたかを確認することもできる。 難読化前のコード 難読化後のコード symbols file obfuscation-map ["_initialLifecycleStateAccessed@1506558

    9","_DCa@15065589","untransformedEndP osition","gHb","_color@92443363","_DYa @92443363","crossAxisAlignment","PWa", "_lastPosition@196518508","_xHb@19651 8508","__ConstMap&_HashVMImmutableBa se&MapMixin&_HashBase&_OperatorEquals AndCanonicalHashCode&_LinkedHashMapMi xin&_UnmodifiableMapMixin&_ImmutableLi nkedHashMapMixin","_Lc", … ]
  25. 1. 難読化とは • 例えば Sentry では ◦ CI のためのプラグインが symbols

    のアップロードを サポート ◦ > 難読化が解除されたログの状態で、(Slack等で)Sentryの エラー通知を受け入れることができる。 ◦ • Crashlyticsにも同様のものがある https://github.com/getsentry/sentry-dart-plugin
  26. 1. 難読化とは • コマンド集 ◦ リリースビルド ▪ flutter build apk

    ◦ debug infoを同時に吐く ▪ flutter build apk --obfuscate \ --split-debug-info=/path/to/symbols ◦ Obfuscation mapを同時に吐く ▪ flutter build apk --obfuscate \ --split-debug-info=/path/to/symbols \ --extra-gen-snapshot-options=--save-obfuscation-map= /path/to/map.json
  27. 2. 難読化の仕組み • オプションの説明を思い出す。 ◦ In a release build, this

    flag removes identifiers and replaces them with randomized values for the purposes of source code obfuscation. • どうやって、ランダムな文字列がマップされているのだろう?
  28. 2. 難読化の仕組み • JIT (Just in Time) pipeline mode ◦

    開発時のhotreload を支える仕組み ◦ コード実行時にコンパイルをする ◦ デバッグモードで主に使用される • AOT (Ahead of Time) pipeline mode ◦ コンパイルのあるステップで難読化をサポートする ◦ コード実行前にコンパイルをする ◦ リリースモードで主に使用される https://mrale.ph/dartvm/
  29. 2. 難読化の仕組み • AOT pipeline mode での難読化処理 ◦ dart-lang/sdk の

    precompiler.h / precompiler.cc が 担っている ◦ その中でも処理は Obfuscator クラスに集約されている https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler /aot/precompiler.cc#L3345-L3414
  30. 2. 難読化の仕組み • 大まかなステップ 1. 難読化が 必要なものを 集める 2. ルールに

    則って rename作業 をする 3. mapを 保存する https://github.com/dart-lang/sdk/blob/main/runtime/vm/compiler/aot/prec ompiler.cc#L3986-L4082
  31. void Obfuscator::ObfuscationState::NextName() { // We apply the following rules: //

    inc(a) = b, ... , inc(z) = A, ..., inc(Z) = a & carry. for (intptr_t i = 0;; i++) { const char digit = name_[i]; if (digit == '\0') { name_[i] = 'a'; } else if (digit < 'Z') { name_[i]++; } else if (digit == 'Z') { name_[i] = 'a'; continue; // Carry. } else if (digit < 'z') { name_[i]++; } else { name_[i] = 'A'; } break; } } 本質パートはこれだけ https://github.com/dart-lang/sdk/blo b/main/runtime/vm/compiler/aot/prec ompiler.cc#L3986-L4007
  32. StringPtr Obfuscator::ObfuscationState::NewAtomicRename( bool should_be_private) { do { NextName(); renamed_ =

    Symbols::NewFormatted( thread_, "%s%s", should_be_private ? "_" : "", name_ ); // Must check if our generated name clashes with something that will // have an identity renaming. } while (renames_.GetOrNull(renamed_) == renamed_.ptr()); return renamed_.ptr(); } private のアンダースコアは、 難読化後も残す https://github.com/dart-lang/sdk/blo b/main/runtime/vm/compiler/aot/prec ompiler.cc#L4009-L4019
  33. StringPtr Obfuscator::ObfuscationState::BuildRename( const String& name, bool atomic ) { //

    Omit code... // Handle non-obfuscation case if (atomic) { return NewAtomicRename(name.CharAt(0) == '_'); } // Omit code... // Parse `name` and split into atomic component. // After that call `BuildName` recursively. } private かどうかは先頭が ‘_’ どうかしかみていない https://github.com/dart-lang/sdk/blob/main/runtime/vm/co mpiler/aot/precompiler.cc#L4021-L4082
  34. StringPtr Obfuscator::ObfuscationState::BuildRename( const String& name, bool atomic ) { //

    Omit code... // Handle non-obfuscation case if (atomic) { return NewAtomicRename(name.CharAt(0) == '_'); } // Omit code... // Parse `name` and split into atomic component. // After that call `BuildName` recursively. } private かどうかは先頭が ‘_’ どうかしかみていない 再帰的に処理
  35. 3. 複数のプラットフォームで動作する仕組み • Androidでの例 ◦ flutter build apk --obfuscate --split-debug-info

    = /path ◦ tips: command parser等は dart で書かれているよ ◦ build_apk.dart や flutter.groovy が処理を担っている。
  36. 3. 複数のプラットフォームで動作する仕組み • Androidでの例 ◦ flutter build apk --obfuscate --split-debug-info

    = /path ◦ tips: command parser等は dart で書かれているよ ◦ build_apk.dart や flutter.groovy が処理を担っている。
  37. 3. 複数のプラットフォームで動作する仕組み • Androidでの例 ◦ flutter build apk --obfuscate --split-debug-info

    = /path ◦ tips: command parser等は dart で書かれているよ ◦ build_apk.dart や flutter.groovy が処理を担っている。 release { minifyEnabled(true) shirinkResources(isBuildAsApp(project)) proguardFiles( project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro" ) }
  38. 3. 複数のプラットフォームで動作する仕組み • Androidでの例 ◦ flutter build apk --obfuscate --split-debug-info

    = /path ◦ tips: command parser等は dart で書かれているよ ◦ build_apk.dart や flutter.groovy が処理を担っている。 release { minifyEnabled(true) shirinkResources(isBuildAsApp(project)) proguardFiles( project.android.getDefaultProguardFile("proguard-android.txt"), flutterProguardRules, "proguard-rules.pro" ) } R8 (Androidの世界の難読化) も 適用している!
  39. 3. 複数のプラットフォームで動作する仕組み Dartコード DartVM (AOT mode) flutter runtime obfuscation shrinking

    libapp.so Androidの コード リソース ファイル R8の適用 obfuscation shrinking apk
  40. 3. 複数のプラットフォームで動作する仕組み Androidの コード リソース ファイル R8の適用 libapp.so obfuscation shrinking

    apk mapping.txt Dartコード flutter runtime DartVM (AOT mode) obfuscation shrinking debug-info 2 phaseの難読化が 行われている
  41. 3. 複数のプラットフォームで動作する仕組み Dartコード DartVM (AOT mode) flutter runtime obfuscation shrinking

    libapp.so Androidの コード リソース ファイル R8の適用 obfuscation shrinking apk Androidでのパターン
  42. 3. 複数のプラットフォームで動作する仕組み Dartコード DartVM (AOT mode) flutter runtime obfuscation shrinking

    libapp.so Androidの コード リソース ファイル R8の適用 obfuscation shrinking apk Androidでのパターン その他のプラットフォーム
  43. 3. 複数のプラットフォームで動作する仕組み Dartコード DartVM (AOT mode) flutter runtime obfuscation shrinking

    libapp.so Androidの コード リソース ファイル R8の適用 obfuscation shrinking apk Androidでのパターン その他のプラットフォーム 各プラットフォームに 詳細実装を移譲している
  44. 3. 複数のプラットフォームで動作する仕組み • Flutterでの難読化 aar linux ios windows apk app

    bundle ios- framework ipa macos macos- framework Webはどこにいった?
  45. 3. 複数のプラットフォームで動作する仕組み • まとめ ◦ 難読化は大きく2つのパートからなる ▪ 1. Dart のコード

    ▪ 2. それぞれのプラットフォームのコード・リソース ◦ Flutterではこれらを包括的に扱かっている ▪ flutter symbolize で基本的にはすべて解決する ▪ 一部はプラットフォームの難読化解除を使用する ◦ DartVMの仕組みに依存するため、 ターゲットがwebかどうかで大きく動作が異なる
  46. 4. トラブルシューティングtips • Q. 一部のクラスや変数名については難読化を適用したくない (R8のkeep ruleのようなことをしたい) • A. @pragma("vm:entry-point")

    を使おう。 ◦ Class / Procedure / Field に適用可能 ◦ 例えば、getter だけ難読化を適用しないということも可能 @pragma("vm:entry-point") class Foo @pragma("vm:entry-point", "get") int foo;
  47. 4. トラブルシューティングtips • Q. --split-debug-info と --obfuscate の差分がわからない • A.

    --obfuscate は単体では使うことができない ◦ --split-debug-info はdebug symbols をバイナリに含めない ためのオプション ◦ アプリサイズの削減にも使われるtips
  48. 4. トラブルシューティングtips • Q. runtimeで型を比較するようなケースで失敗する • A. 難読化されてるから、runtimeだと命名が違う ◦ FooBar

    だったものが Xz などになってしまう ◦ ログを送るケースなどでも気をつけないと、難読化されたロ グを送ってしまうことになるので注意が必要
  49. まとめ (セッションのゴールへのアンサー) 1. 難読化の仕組みと目的を理解する (Chapter 1) → リバースエンジニアリングへの対策、 rename のマップを保持する。symbols

    ファイルを使う 2. 難読化に関して実践的なトラブルシューティングが できるようになる (Chapter 1, 4) → 様々な flutter command を使用する 3. Flutterでの難読化の動作原理や限界を理解する (Chapter 2, 3) → Dartの難読化とプラットフォームの難読化のステップがある web のプラットフォームは特殊化されている
  50. 参考資料 (1) 1. https://www.slideshare.net/slideshow/flutterble/125792150 2. https://qiita.com/chigichan24/items/89cb686e880f0274ed1c 3. https://docs.flutter.dev/deployment/obfuscate 4. https://docs.flutter.dev/perf/app-size

    5. https://docs.flutter.dev/deployment/android 6. https://docs.flutter.dev/deployment/ios 7. https://docs.flutter.dev/deployment/web 8. https://github.com/flutter/flutter 9. https://github.com/flutter/engine 10. https://developer.android.com/build/shrink-code 11. https://pub.dev/documentation/native_stack_traces/latest/index.html 12. https://pub.dev/documentation/meta/latest/index.html 13. https://wiki.dwarfstd.org/ 14. https://docs.flutter.dev/resources/faq 15. https://docs.sentry.io/platforms/flutter/upload-debug/ 16. https://github.com/getsentry/sentry-dart-plugin 17. https://zenn.dev/tsuruo/articles/48909d22d49ffe