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」で難読化されたスタックトレースを解除してみた 


Google「Closure Compiler」 (クロージャコンパイラ) の使い方

「Closure Compiler」 (クロージャコンパイラ) は、JavaScript の圧縮、最適化、およびバグの検出を行う Java コマンドラインユーティリティです。

👉 Getting Started with the Closure Compiler Application  

簡単な JavaScript で 「Closure Compiler」 を試すことができます。

 

1. Closure Compiler をダウンロードする。

Mavan リポジトリから最新の jar ファイルをダウンロードします。

google closure_compiler

👉 Maven Repository: com.google.javascript » closure-compiler 

 

2. JavaScript ファイルを作成する。

次の JavaScript を含む hello.js ファイルを作成します。


function hello(longName) {
  alert('Hello, ' + longName);
}
hello('New User');

 

3. JavaScript ファイルをコンパイルします。

次のコマンドを実行します。


java -jar compiler.jar --js hello.js --js_output_file hello-compiled.js

コンパイラはコメント、空白、および不要なセミコロンを削除していることに注意してください。コンパイラーは、パラメーター longName を短い名前に置き換えました。

その結果、はるかに小さい JavaScript ファイルが作成されます。


~
❯ java -jar closure-compiler-v20220301.jar --js hello.js --js_output_file hello-compiled.js

~
❯ cat hello-compiled.js
function hello(a){alert("Hello, "+a)}hello("New User");


<html>
  <head><title>Hello World</title></head>
  <body>
    <script src="hello-compiled.js"></script>
  </body>
</html>

HTMLファイルからブラウザにロードして表示できます。

 

まとめ

そもそもは、ブックマークレットを圧縮したかった。

なのでそのまとめとして。

以下の、稼働確認済みブックマークレットをコンパイルして圧縮する。


javascript:(
  function(){

    function hello(longName) {
      alert('Hello, ' + longName);
    }
    hello('New User');

  }
)();

対象コードをクリップボードにコピーしてから、以下のようにを実行する。


❯ pbpaste | java -jar closure-compiler-v20220301.jar | tee >(pbcopy)
The compiler is waiting for input via stdin.
(function(){alert("Hello, New User")})();

あとは、javascript: と頭に付けてブックマークに登録すればよし。


javascript:(function(){alert("Hello, New User")})();

Google ClosureCompiler (クロージャコンパイラ) の使い方

Google ClosureCompiler (クロージャコンパイラ) の使い方

👉 GitHub Gist に置いた Javascript をブックマークレットとして実行する 
👉 【macOS】コピーしているテキストをHTMLエスケープする 
👉 Closure Compiler  |  Google Developers 
👉 iPhone Android アプリ横断検索 


highlight.js に「AndroidStudio」スタイルが登場してた件

WEBサイトでコードをシンタックス・ハイライトするなら「highlight.js」がおすすめです。

highlight.js Android Studio style

👉 highlight.js 

「スタイル」と呼ばれるきれいなテーマが今では 246個 もあるようです。

Androidアプリ向けIDE「AndroidStudio」というスタイルも登場していました。

ダークなほうの「Darcula」ですね、これ。

highlight.js Android Studio style

その他、いくつかメジャーどころを貼っておきます。

github

github-dark

github-dark-dimmed

stackoverflow-light

stackoverflow-dark

使い方

HTMLヘッダーの中に表示したいスタイル「androidstudio」のCSSファイルと、プログラミング言語を認識するJavaScriptファイルについて記述しておきます。


<link rel="stylesheet" href="/path/to/styles/androidstudio.min.css">
<script src="/path/to/highlight.min.js"></script>
<script>hljs.highlightAll();</script>

<pre><code class="language-kotlin"> ~ </code></pre> 内にコードを差し込みます。

言語はここで指定しなくても自動検出してくれるようですが、短いコードなどもあるので、念のため記述しておいたほうが良いでしょう。

実体参照にエスケープしたコードを以下のように差し込み貼り付けます。


<pre><code class="language-kotlin">
import kotlinx.serialization.Serializable
import kotlin.random.Random

interface Building

@Serializable
class House(
    private val rooms: Int? = 3,
    val name: String = &quot;Palace&quot;
) : Building {
    var residents: Int = 4
        get() {
            println(&quot;Current residents: $field&quot;)
            return field
        }

    fun burn(evacuation: (people: Int) -&gt; Boolean) {
        rooms ?: return
        if (evacuation((0..residents).random()))
            residents = 0
    }
}

fun main() {
    val house = House(name = &quot;Skyscraper 1&quot;)
    house.burn {
        Random.nextBoolean()
    }
}

</code></pre>

👉 【macOS】コピーしているテキストをHTMLエスケープする 

ブラウザでアクセスすると以下のように表示されています。


import kotlinx.serialization.Serializable
import kotlin.random.Random

interface Building

@Serializable
class House(
    private val rooms: Int? = 3,
    val name: String = "Palace"
) : Building {
    var residents: Int = 4
        get() {
            println("Current residents: $field")
            return field
        }

    fun burn(evacuation: (people: Int) -> Boolean) {
        rooms ?: return
        if (evacuation((0..residents).random()))
            residents = 0
    }
}

fun main() {
    val house = House(name = "Skyscraper 1")
    house.burn {
        Random.nextBoolean()
    }
}

👉 How to use highlight.js 

このサイトでも使っていましたが6年ぶりに更新してみましたので 🙆