읽기 어려운 데이터 코드의 Crashlytics 로그 디코딩

Dart 코드의 난독화


원본 코드의 난독화는 가공 응용 프로그램의 2진법을 통해 난독하게 하여 역방향 공정의 난이도를 높일 수 있다.Flutter에는 컴파일된 Dart 코드의 함수 이름과 클래스 이름을 숨길 수 있는 읽기 어려운 명령도 있습니다.2022년 1월까지 지원되는 플랫폼은 안드로이드/iOS/macOS뿐이다.
Flutter’s code obfuscation, when supported, works only on a release build.
또한 문서에서 설명한 대로 버전 빌드에서만 작동합니다.
https://docs.flutter.dev/deployment/obfuscate

Crashlytics의 디코딩 문제


원래 읽기 어려운 코드를 디코딩할 때 원시적인 창고 추적과 디버깅 파일flutter symbolize을 준비하면 명령을 사용하여 디코딩할 수 있다.
Firebase Crashlytics(이하 Crashlytics)를 사용하여 애플리케이션의 충돌 보고서(Non-Fatal 오류 포함)를 보냈지만 컴파일된 코드가 읽기 어려워져서 Crashlytics에 보낸 보고서도 읽기 어려운 것이 되었다.
Crashlytics 로그에서 소스 코드의 해당 위치→수정 주기를 확인하는 것은 일반적으로 보편적이지만 읽기 어려워 클래스 이름과 파일 이름이 숨겨져 어떤 원인으로 인한 보고인지 확인하기 어렵다.예를 들어 다음 Crashlytics 로그를 얻을 수 있지만 그것만으로는 알 수 없습니다.
Non-fatal Exception: FlutterError
0  ???  0x0 (null).  #00 abs 0 _kDartIsolateSnapshotInstructions+0x3e277b
1  ???  0x0 (null).  #01 abs 0 _kDartIsolateSnapshotInstructions+0x59f67b
※ 이전에는 읽기 어려운 문자정보를 잃어버려 원시기호를 수동으로 복원할 수 없었던 문제가 있었는데 지금은 속수무책인 상태입니다2.0.1. 아래 PR을 병합하면 디코딩이 가능합니다.
https://github.com/FirebaseExtended/flutterfire/pull/4407
이 글에서 나는 크래쉬리틱스로 보내진 읽기 어려운 Dart 코드부터 디버그 파일을 사용하여 디코딩할 때까지(반은 비망록)를 집필했다.

스택 추적/디버그 파일 준비


Reading an obfuscated stack trace에 따라 명령을 집행하기 위해서는 먼저 필요한 두 개의 서류를 준비해야 한다.

1. 스택 추적 파일 준비


우선 디코딩하고 싶은 창고 추적을 손에 넣으세요.Firebase 콘솔에서 Crashlytics의 로그(. #00 ・(.java)などが付いているもの)를 복사errors.txt로 저장합니다.

오류의 종류에 따라 창고 추적의 형식도 다르지만 내가 확인하면 다음과 같은 두 가지 유형으로 나눌 수 있다.
마침표(.)시작, 끝에 (.java)를 포함하는 형식
. #00 abs 0 virt 000000000071f347 _kDartIsolateSnapshotInstructions+0x3e28a7 (.java)
. #01 abs 0 virt 00000000008dc247 _kDartIsolateSnapshotInstructions+0x59f7a7 (.java)
. #02 abs 0 virt 000000000071f317 _kDartIsolateSnapshotInstructions+0x3e2877 (.java)
. #03 abs 0 virt 000000000071f417 _kDartIsolateSnapshotInstructions+0x3e2977 (.java)
. #04 abs 0 virt 000000000065224f _kDartIsolateSnapshotInstructions+0x3157af (.java)
. #05 abs 0 virt 000000000081161b _kDartIsolateSnapshotInstructions+0x4d4b7b (.java)
. #06 abs 0 virt 00000000008113eb _kDartIsolateSnapshotInstructions+0x4d494b (.java)
. #07 abs 0 virt 00000000005ce2ff _kDartIsolateSnapshotInstructions+0x29185f (.java)
. #08 abs 0 virt 00000000005ce29b _kDartIsolateSnapshotInstructions+0x2917fb (.java)
. #09 abs 0 virt 00000000003b1673 _kDartIsolateSnapshotInstructions+0x74bd3 (.java)
. #10 abs 0 virt 00000000003a4a0f _kDartIsolateSnapshotInstructions+0x67f6f (.java)
. #11 abs 0 virt 00000000003a43b3 _kDartIsolateSnapshotInstructions+0x67913 (.java)
. #12 abs 0 virt 00000000003a437b _kDartIsolateSnapshotInstructions+0x678db (.java)
. #13 abs 0 virt 00000000003b4f2f _kDartIsolateSnapshotInstructions+0x7848f (.java)
. #14 abs 0 virt 00000000003b4e6b _kDartIsolateSnapshotInstructions+0x783cb (.java)
. #15 abs 0 virt 00000000003b4bc3 _kDartIsolateSnapshotInstructions+0x78123 (.java)
. #16 abs 0 virt 00000000003b4a93 _kDartIsolateSnapshotInstructions+0x77ff3 (.java)
. #17 abs 0 virt 000000000034593f _kDartIsolateSnapshotInstructions+0x8e9f (.java)
. #18 abs 0 virt 0000000000345a2b _kDartIsolateSnapshotInstructions+0x8f8b (.java)
. #19 abs 0 virt 000000000086d99b _kDartIsolateSnapshotInstructions+0x530efb (.java)
. #20 abs 0 virt 000000000086eb4b _kDartIsolateSnapshotInstructions+0x5320ab (.java)
. #21 abs 0 virt 00000000003535ab _kDartIsolateSnapshotInstructions+0x16b0b (.java)
. #22 abs 0 virt 0000000000357d7b _kDartIsolateSnapshotInstructions+0x1b2db (.java)
. #23 abs 0 virt 0000000000357d37 _kDartIsolateSnapshotInstructions+0x1b297 (.java)
. #24 abs 0 virt 0000000000357df3 _kDartIsolateSnapshotInstructions+0x1b353 (.java)
???0x0(null)을 시작합니다.형식
0  ???                            0x0 (null).    #00 abs 0 _kDartIsolateSnapshotInstructions+0x1f0913
1  ???                            0x0 (null).    #01 abs 0 _kDartIsolateSnapshotInstructions+0x3edb83
2  ???                            0x0 (null).    #02 abs 0 _kDartIsolateSnapshotInstructions+0x3eacf3
3  ???                            0x0 (null).    #03 abs 0 _kDartIsolateSnapshotInstructions+0x2ce2eb
4  ???                            0x0 (null).    #04 abs 0 _kDartIsolateSnapshotInstructions+0x2cee47
5  ???                            0x0 (null).    #05 abs 0 _kDartIsolateSnapshotInstructions+0x2bf5f3
6  ???                            0x0 (null).    #06 abs 0 _kDartIsolateSnapshotInstructions+0x2bf3af
7  ???                            0x0 (null).    #07 abs 0 _kDartIsolateSnapshotInstructions+0x5ecfb
8  ???                            0x0 (null).    #08 abs 0 _kDartIsolateSnapshotInstructions+0x5e953
9  ???                            0x0 (null).    #09 abs 0 _kDartIsolateSnapshotInstructions+0x5e8a3
10 ???                            0x0 (null).    #10 abs 0 _kDartIsolateSnapshotInstructions+0x799eb
11 ???                            0x0 (null).    #11 abs 0 _kDartIsolateSnapshotInstructions+0x79db3
12 ???                            0x0 (null).    #12 abs 0 _kDartIsolateSnapshotInstructions+0x79ccf
13 ???                            0x0 (null).    #13 abs 0 _kDartIsolateSnapshotInstructions+0x81b3
14 ???                            0x0 (null).    #14 abs 0 _kDartIsolateSnapshotInstructions+0x8437
15 ???                            0x0 (null).    #15 abs 0 _kDartIsolateSnapshotInstructions+0x531c5b
16 ???                            0x0 (null).    #16 abs 0 _kDartIsolateSnapshotInstructions+0x532103
17 ???                            0x0 (null).    #17 abs 0 _kDartIsolateSnapshotInstructions+0x166bf
18 ???                            0x0 (null).    #18 abs 0 _kDartIsolateSnapshotInstructions+0x1b5b3
19 ???                            0x0 (null).    #19 abs 0 _kDartIsolateSnapshotInstructions+0x1b537

2. 파일 디버그 준비


읽기 어려울 때 --split-debug-info 태그를 통해 디버그 파일을 출력할 수 있습니다.이번에 디코딩하고자 하는 Crashlytics 보고서의 생성 생성 번호에 대응하는 디버깅 파일은 수중에 준비되어 있습니다. (디버깅 파일을 저장할 때 잊어버린 경우 디버깅할 수 없습니다.)디버그 파일은 CPU별로 출력되어야 합니다.
https://docs.flutter.dev/deployment/obfuscate#obfuscating-your-app
flutter build apk --obfuscate --split-debug-info=/<project-name>/<directory>

Crashlytics 로그 디코딩


공식 문서처럼 flutter symbolize 명령은 맞아야 하지만, 이렇게 하면 Crashlytics 로그는 디코딩되지 않습니다.다음 설명에 따라 읽기 어려운 창고 추적에서 디코딩을 해서 읽기 어려운 창고 추적을 얻을 수 있습니다.
https://github.com/FirebaseExtended/flutterfire/issues/2644#issuecomment-746217634
요점은 두 시입니다.
  • 각 행의 마침표(.),(.java) 등 불필요한 요소를 제거하고 각 행#에서 성형
  • 을 시작한다.
  • 각 행의 시작 부분에 4개의 공백 추가
  • 적당한 편집기에서 이것들을 성형한 후 아래 명령을 누르면 디코딩에 성공할 수 있습니다.
    $ flutter symbolize -i <stack trace file> -d /out/android/app.android-arm64.symbols
    

    스크립트 디코딩 사용


    매번 편집할 때마다 상술한 성형을 할 수 있지만 몇 번 반복하면 귀찮아서 스크립트를 준비해 집행한다.스크립트는 Dartgrinder | Dart Package를 사용합니다.
    ※ grinder의 설치와 실행 방법은 이번엔 생략할 예정이니 아래 내용을 참고하시기 바랍니다.
    https://qiita.com/0maru/items/b134c5ee319e3cac2a99
    성형 전 창고 추적 파일 ((ピリオド(.)(.java) 등), 디버깅 파일을 원하는 디렉터리에 저장한 후 다음 명령을 실행합니다.
    (나는 /symbolize라는 디렉터리를 만들었는데git 관리 대상에 속하지 않는다)
    $ flutter pub run grinder symbolize-obfuscated-stack-trace --os=android
    
    ※ 명령을 실행할 때 필요에 따라 CPU 구조를 지정합니다(지정하지 않을 때arm64 선택).
    ※ Crashlytics 로그를 출력하는 터미널과 다른 CPU 구조를 지정하면 디코딩에 성공할 수 없습니다(엄밀히 말하면 언뜻 보면 디코딩에 성공한 것 같지만 확인 내용은 다름)

    symbolze 명령의 디코딩 결과


    디코딩 결과는 컨트롤러 로그로 출력됩니다.일부 디코딩 결과는 숨겨졌지만 실제로는 원본 코드의 해당 줄을 출력했기 때문에 해당하는 부분을 알게 되었다.이렇게 하면 크래쉬리틱스의 대응이 진전된다.
    디코딩 결과
    ※ 일부는 채워져 있다.
    #0      AsyncValueX|get#value.<anonymous closure> (package:riverpod/src/common.dart:206:21)
    #1      AsyncError._map (package:riverpod/src/common.dart:391:17)
    #2      AsyncValueX|get#value (package:riverpod/src/common.dart:203:12)
    #3      xxxxxxx
    #4      _ConsumerState.build (package:flutter_riverpod/src/consumer.dart:371:19)
    #5      StatefulElement.build (package:flutter/src/widgets/framework.dart:4782:27)
    #6      ConsumerStatefulElement.build (package:flutter_riverpod/src/consumer.dart:431:20)
    #7      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4665:15)
    #8      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4840:11)
    #9      Element.rebuild (package:flutter/src/widgets/framework.dart:4355:5)
    #10     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2620:33)
    #11     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
    #12     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:319:5)
    #13     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart)
    #14     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1143:15)
    #15     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1080:9)
    #16     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:996:5)
    #17     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart)
    #18     _rootRun (dart:async/zone.dart:1428:13)
    #19     _rootRun (dart:async/zone.dart)
    #20     _CustomZone.run (dart:async/zone.dart:1328:19)
    #21     _CustomZone.runGuarded (dart:async/zone.dart:1236:7)
    #22     _invoke (dart:ui/hooks.dart:166:10)
    #23     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:270:5)
    #24     _drawFrame (dart:ui/hooks.dart:129:31)
    #25     _drawFrame (dart:ui/hooks.dart)
    
    다음은 실행에 사용할 원본 코드입니다.
    grind.dart
    // 難読化されたCrashlyticsレポートをsymbolizeするタスクです。
    @Task('symbolize-obfuscated-stack-trace')
    void symbolizeObfuscatedStackTrace() {
      final args = context.invocation.arguments;
      final os = args.getOption('os') ?? 'android';
      final cpu = args.getOption('cpu') ?? 'arm64';
      const pathPrefix = './tool/symbolize';
      final stackTraceRawFile = File('$pathPrefix/errors.txt');
      if (!stackTraceRawFile.existsSync()) {
        log('Could not find stack trace file named by `errors.txt`.');
        return;
      }
      final lines = stackTraceRawFile.readAsLinesSync();
    
      // StackTraceをsymbolizeできるよう整形
      // ref. https://github.com/FirebaseExtended/flutterfire/issues/2644#issuecomment-746217634
      // - 各行の`#`前を取り除く
      // - `(.java)`をが存在する場合は取り除く
      // - 各行の頭に4つの空白を追加
      final formattedStackTrace = lines.map(
        (line) {
          final l = line.split('#').last.replaceAll('(.java)', '').trim();
          return '    #$l';
        },
      ).toList();
    
      final buffer = StringBuffer();
      formattedStackTrace.forEach(buffer.writeln);
    
      final formattedStackTraceFile = File('$pathPrefix/errors_formatted.txt')
        ..writeAsStringSync(
          buffer.toString(),
        )
        ..createSync();
    
      final dartSymbols = File('$pathPrefix/app.$os-$cpu.symbols');
      if (!dartSymbols.existsSync()) {
        log('Could not dart symbols file.');
        return;
      }
      final result = Process.runSync(
        'flutter',
        [
          'symbolize',
          '-i',
          formattedStackTraceFile.path,
          '-d',
          dartSymbols.path,
        ],
      );
      log(result.stdout.toString());
    }
    

    최후


    이 글처럼 크래쉬리틱스 보고서 발송을 계기로 기릿허브 이슈의 스크립트가 제작된 만큼 이 근처를 사용하면서 이 글의 디코딩 처리(flutter 명령을 사용할 수 없는 경우에는 포맷이라도 가능)까지 함께 자동화하면 진전이 있을 것으로 보인다.
    https://qiita.com/koishi/items/84e44a54f4ab75bd23ea
    https://github.com/kevalpatel2106/github-issue-cloud-function
    위와 같은 내용을 사용하지 않더라도 콘텐츠는 클라우드 펀션의 크래쉬리틱스 트리거를 사용해 구현한 것으로, 취향에 맞게 맞춤형으로 제작하는 것도 좋다.
    functions.crashlytics.issue().onNew(async (issue) => {
      // do something...
    });
    
    https://medium.com/google-cloud/understanding-firebase-cloud-functions-and-triggers-️-85a8fe89be3c#:~:text=Firebase Crashlytics triggers,issue for the first time
  • 맞춤형 베로성 경보
  • 참고 자료

  • Obfuscating Dart code | Flutter
  • [firebase_crashlytics] No stack traces for issues on Crashlytics dashboard · Issue #2644 · FirebaseExtended/flutterfire
  • 좋은 웹페이지 즐겨찾기