R8 で 難読化 された スタックトレース を元に戻す


minifyEnabled true

のとき、実機にインストールして起動した瞬間にクラッシュします。


2022-04-02 23:21:04.099 5125-5125/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 5125
    java.lang.NullPointerException
        at w5.c$c.b(:112)
        at w5.c$c.f(:154)
        at v5.f.d(:233)
        at e7.x$a.c(:46)
        at v5.b$a.a(:38)
        at e7.x$a.b(:43)
        at w5.b$b.a(:25)
        at w5.b$b.add(:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

難読化されてるので原因を特定するのがツラいです !!

難読化を元に戻してみましょう。

 

手順

1. R8 ルールを追加する。

proguard-rules.pro に以下を追加することで再構成がより確実になります。


-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile

2. 再度クラッシュさせる。

ルール追加したので再度クラッシュさせてスタックトレースを出力します。


2022-04-04 02:44:01.054 2001-2001/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 2001
    java.lang.NullPointerException
        at w5.c$c.b(SourceFile:112)
        at w5.c$c.f(SourceFile:154)
        at v5.f.d(SourceFile:233)
        at e7.x$a.c(SourceFile:46)
        at v5.b$a.a(SourceFile:38)
        at e7.x$a.b(SourceFile:43)
        at w5.b$b.a(SourceFile:25)
        at w5.b$b.add(SourceFile:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

少しスタックトレースが変化しました。

これは、クリップボードにコピーして使います。

3. 難読化を解除して元に戻す。 - retrace

「スタックトレース」 ( クリップボード内 ) と

「マッピングファイル」
( <module>/build/outputs/mapping/<build>/mapping.txt )

をデータとして retrace ツールを実行します。


~/Example
❯ pbpaste | java -jar ~/Library/Android/sdk/tools/proguard/lib/retrace.jar -verbose \
app/build/outputs/mapping/debug/mapping.txt
 
2022-04-04 02:44:01.054 2001-2001/com.benigumo.example E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.benigumo.example, PID: 2001
    java.lang.NullPointerException
        at curtains.internal.WindowCallbackWrapper$Companion.android.view.Window$Callback getJetpackWrapped(android.view.Window$Callback)(SourceFile:112)
        at curtains.internal.WindowCallbackWrapper$Companion.android.view.Window$Callback unwrap(android.view.Window$Callback)(SourceFile:154)
        at curtains.WindowsKt.android.view.Window$Callback getWrappedCallback(android.view.Window$Callback)(SourceFile:233)
        at leakcanary.RootViewWatcher$listener$1.void onRootViewAdded(android.view.View)(SourceFile:46)
        at curtains.OnRootViewAddedListener$DefaultImpls.void onRootViewsChanged(curtains.OnRootViewAddedListener,android.view.View,boolean)(SourceFile:38)
        at leakcanary.RootViewWatcher$listener$1.void onRootViewsChanged(android.view.View,boolean)(SourceFile:43)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.boolean add(android.view.View)(SourceFile:25)
        at curtains.internal.RootViewsSpy$delegatingViewList$1.boolean add(java.lang.Object)(SourceFile:23)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:393)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:133)
        at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:4847)
        at android.app.servertransaction.ResumeActivityItem.execute(ResumeActivityItem.java:54)
        at android.app.servertransaction.ActivityTransactionItem.execute(ActivityTransactionItem.java:45)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2214)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:201)
        at android.os.Looper.loop(Looper.java:288)
        at android.app.ActivityThread.main(ActivityThread.java:7842)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1003)

全クラス名が丸出しになりました !!

👉 R8 retrace  |  Android デベロッパー  |  Android Developers 

4. Android Studio でスタックトレースを分析します。


[Analyze] - [Stack Trace or Thread Dump ...]

[Analyze] - [Stack Trace or Thread Dump ...]

[Analyze] - [Stack Trace or Thread Dump ...]

ソースファイルへのクリック可能なリンクも表示されています。

👉 スタック トレースを分析する  |  Android デベロッパー  |  Android Developers 

ちなみに、今回のクラッシュの原因は、Leakcanary まわりでした !


Proguard GUI

同様に、同梱の Proguard GUI を使っても retrace はできます。


java -jar ~/Library/Android/sdk/tools/proguard/lib/proguardgui.jar

GUI上でマッピングファイルのパスを入力して、スタックトレースは貼り付けます。

proguardgui.jar

 

日本語

さて、「難読化 (obfuscation)」の反対語は何なのでしょうか。

英語だと

「deobfuscation」

ですが、公式リファレンスの日本語を見ているといろいろな単語が使われています。

「デコード」

「難読化の解除」

「解読」

日本語は難しいですね !

 

まとめ

proguard-rules.pro に追記して、コピーしたスタックトレースを2つのツールどちらかで retrace する。


# To de-obfuscate stacktrace
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile


pbpaste | java -jar ~/Library/Android/sdk/tools/proguard/lib/retrace.jar -verbose \
app/build/outputs/mapping/debug/mapping.txt | tee >(pbcopy)


java -jar ~/Library/Android/sdk/tools/proguard/lib/proguardgui.jar

alias にしとくと良いでしょうか。

👉 アプリの圧縮、難読化、最適化  |  Android デベロッパー  |  Android Developers 
👉 【Android Studio】プラグイン「Proguard Retrace Unscrambler」で難読化されたスタックトレースを解除してみた 


iPhone Android アプリ横断検索 をつくってみた

作ってみました javascript でブックマークレットを!

なんかパソコンで探したかったので!

iPhone Android アプリ横断検索

iPhone と Android アプリを横断で検索します。


(() => {
  const word = window.prompt("Enter the keyword.");
  if (word) {
    const q = encodeURIComponent(`${word} site:play.google.com OR site:apps.apple.com`)
    const url = `https://www.google.com/search?q=${q}`;
    window.open(url, "_blank");
  }
})();

以下、圧縮版ブックマーク登録用。


(()=>{var a=window.prompt("Enter the keyword.");a&&(a=encodeURIComponent(`${a} site:play.google.com OR site:apps.apple.com`),window.open(`https://www.google.com/search?q=${a}`,"_blank"))})();

ボタンに付けてみた。押してみてね!

気に入ったアプリを見つけたら、ブラウザの機能を使って、その端末へそのまま送ると良いです。

気に入ったアプリを見つけたら、ブラウザの機能を使って、その端末へそのまま送ると良いです。
👉 スマホのChromeで閲覧中のWebページをパソコンへ送信する方法【iPhone & Android】 

👉 GitHub Gist に置いた Javascript をブックマークレットとして実行する 
👉 Google「Closure Compiler」 (クロージャコンパイラ) の使い方 


GitHub Gist に置いた Javascript をブックマークレットとして実行する

普通に、document で読み込んだら使えませんし。

"Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec."

ちょっとしたコードをシェアするのにGistは便利ですが、HTMLとしてそのままブラウザに表示したり、JSONとして利用するには少々難があります。レスポンスヘッダのContent-Typeがtext/plain固定なので、Rawデータにアクセスしてもソース表示に。

👉 GistのHTMLを直接表示する方法 - Qiita 

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/plain". Strict MIME type checking is enforced for module scripts per HTML spec.

禁断の eval() 使います。

👉 eval() - JavaScript | MDN 


(
  () => {
    fetch('https://gist.githubusercontent.com/path/to/raw/script.js')
      .then(response => response.text())
      .then(script => eval(script))
  }
)();

通信部分は Future な Promise でシンプルに書くことができます。

👉 Fetch の使用 - Web API | MDN 

思ってたより良い進化をしてる javascript に驚きました。

まずは、一つの Gist を書き換えながら、ブックマークをポチポチしながら作りますか。

おまけ

ついでに、Gist の URL をまとめておきます。


[top]
https://{main_host}/{user_id}/{gist_id}

[edit] *
https://{main_host}/{user_id}/{gist_id}/edit

[raw latest] *
https://{raw_host}/{user_id}/{gist_id}/raw/[{file_name}]

[raw permanent]
https://{raw_host}/{user_id}/{gist_id}/raw/{revision_id}/[{file_name}]

そのうちまた変わるのでしょうがね。

👉 iPhone Android アプリ横断検索 
👉 Google「Closure Compiler」 (クロージャコンパイラ) の使い方